├── README.md ├── cmd └── miflora │ └── main.go ├── go.mod ├── miflora.go ├── miflora.jpg └── miflora_test.go /README.md: -------------------------------------------------------------------------------- 1 | # miflora 2 | 3 | Go library/command line tool to read Mi Flora bluetooth sensors. 4 | 5 | ![Xiaomi Flora](https://github.com/barnybug/miflora/raw/master/miflora.jpg "Xiaomi Flora") 6 | 7 | ## Install 8 | 9 | $ go get github.com/barnybug/miflora/cmd/miflora 10 | 11 | ## Run 12 | 13 | $ miflora 14 | 15 | ## Requirements 16 | 17 | You need bluez-utils installed for the `gatttool` utility to read bluetooth 18 | attributes. 19 | 20 | ## Caveats 21 | 22 | No retries are attempted when reading fails - an error is returned. User 23 | implementation detail. :-) 24 | 25 | `gatttool` has been deprecated and new versions of bluez-utils no longer ships 26 | with it. The alternative is to interface directly with the bluetooth stack. 27 | -------------------------------------------------------------------------------- /cmd/miflora/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/barnybug/miflora" 8 | ) 9 | 10 | func checkError(err error) { 11 | if err != nil { 12 | fmt.Println("Error:", err) 13 | os.Exit(1) 14 | } 15 | } 16 | 17 | func main() { 18 | if len(os.Args) != 3 { 19 | fmt.Println("Usage: miflora MAC HCI_DEVICE\n\neg: miflora C4:7C:8D:XX:XX:XX hci0") 20 | os.Exit(1) 21 | } 22 | 23 | mac := os.Args[1] 24 | adapter := os.Args[2] 25 | fmt.Println("Reading miflora...") 26 | dev := miflora.NewMiflora(mac, adapter) 27 | 28 | firmware, err := dev.ReadFirmware() 29 | checkError(err) 30 | fmt.Printf("Firmware: %+v\n", firmware) 31 | 32 | sensors, err := dev.ReadSensors() 33 | checkError(err) 34 | fmt.Printf("Sensors: %+v\n", sensors) 35 | } 36 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/barnybug/miflora 2 | 3 | go 1.16 4 | -------------------------------------------------------------------------------- /miflora.go: -------------------------------------------------------------------------------- 1 | package miflora 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "errors" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | type Miflora struct { 13 | mac string 14 | adapter string 15 | firmware Firmware 16 | } 17 | 18 | func NewMiflora(mac string, adapter string) *Miflora { 19 | return &Miflora{ 20 | mac: mac, 21 | adapter: adapter, 22 | } 23 | } 24 | 25 | func gattCharRead(mac string, handle string, adapter string) ([]byte, error) { 26 | cmd := exec.Command("gatttool", "-b", mac, "--char-read", "-a", handle, "-i", adapter) 27 | var out bytes.Buffer 28 | cmd.Stdout = &out 29 | err := cmd.Run() 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | // Characteristic value/descriptor: 64 10 32 2e 36 2e 32 35 | s := out.String() 36 | if !strings.HasPrefix(s, "Characteristic value/descriptor: ") { 37 | return nil, errors.New("Unexpected response") 38 | } 39 | 40 | // Decode the hex bytes 41 | r := strings.NewReplacer(" ", "", "\n", "") 42 | s = r.Replace(s[33:]) 43 | h, err := hex.DecodeString(s) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return h, nil 48 | } 49 | 50 | func gattCharWrite(mac string, handle string, value string, adapter string) error { 51 | cmd := exec.Command("gatttool", "-b", mac, "--char-write-req", "-a", handle, "-n", value, "-i", adapter) 52 | var out bytes.Buffer 53 | cmd.Stdout = &out 54 | err := cmd.Run() 55 | if err != nil { 56 | return err 57 | } 58 | 59 | s := out.String() 60 | if !strings.Contains(s, "successfully") { 61 | return errors.New("Unexpected response") 62 | } 63 | 64 | return nil 65 | } 66 | 67 | type Firmware struct { 68 | Version string 69 | Battery byte 70 | } 71 | 72 | func (m *Miflora) ReadFirmware() (Firmware, error) { 73 | data, err := gattCharRead(m.mac, "0x38", m.adapter) 74 | if err != nil { 75 | return Firmware{}, err 76 | } 77 | f := Firmware{ 78 | Version: string(data[2:]), 79 | Battery: data[0], 80 | } 81 | m.firmware = f 82 | return f, nil 83 | } 84 | 85 | type Sensors struct { 86 | Temperature float64 87 | Moisture byte 88 | Light uint16 89 | Conductivity uint16 90 | } 91 | 92 | func (m *Miflora) enableRealtimeDataReading() error { 93 | return gattCharWrite(m.mac, "0x33", "A01F", m.adapter) 94 | } 95 | 96 | func decodeSensors(data []byte) Sensors { 97 | p := bytes.NewBuffer(data) 98 | var t int16 99 | var m uint8 100 | var l, c uint16 101 | 102 | // TT TT ?? LL LL ?? ?? MM CC CC 103 | binary.Read(p, binary.LittleEndian, &t) 104 | p.Next(1) 105 | binary.Read(p, binary.LittleEndian, &l) 106 | p.Next(2) 107 | binary.Read(p, binary.LittleEndian, &m) 108 | binary.Read(p, binary.LittleEndian, &c) 109 | 110 | return Sensors{ 111 | Temperature: float64(t) / 10, 112 | Moisture: m, 113 | Light: l, 114 | Conductivity: c, 115 | } 116 | } 117 | 118 | func (m *Miflora) ReadSensors() (Sensors, error) { 119 | if m.firmware.Version >= "2.6.6" { 120 | // newer firmwares explicitly need realtime reading enabling 121 | err := m.enableRealtimeDataReading() 122 | if err != nil { 123 | return Sensors{}, err 124 | } 125 | } 126 | 127 | data, err := gattCharRead(m.mac, "0x35", m.adapter) 128 | if err != nil { 129 | return Sensors{}, err 130 | } 131 | return decodeSensors(data), nil 132 | } 133 | -------------------------------------------------------------------------------- /miflora.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/barnybug/miflora/1e7b80be41281d61c3d298f53cb99885f1c16063/miflora.jpg -------------------------------------------------------------------------------- /miflora_test.go: -------------------------------------------------------------------------------- 1 | package miflora 2 | 3 | import "testing" 4 | 5 | func TestDecodeSensors1(t *testing.T) { 6 | data := []byte{0x3e, 0x00, 0x00, 0xf7, 0x1e, 0x00, 0x00, 0x02, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} 7 | s := decodeSensors(data) 8 | if s.Temperature != 6.2 { 9 | t.Errorf("Temperature incorrect, got: %.1f, expected: %.1f", s.Temperature, 6.2) 10 | } 11 | if s.Moisture != 2 { 12 | t.Errorf("Moisture incorrect, got: %.1f, expected: %.1f", s.Moisture, 2) 13 | } 14 | if s.Light != 7927 { 15 | t.Errorf("Light incorrect, got: %.1f, expected: %.1f", s.Light, 7927) 16 | } 17 | if s.Conductivity != 22 { 18 | t.Errorf("Conductivity incorrect, got: %d, expected: %d", s.Conductivity, 22) 19 | } 20 | } 21 | 22 | func TestDecodeSensors2(t *testing.T) { 23 | data := []byte{0xfe, 0xff, 0x00, 0xf7, 0x1e, 0x00, 0x00, 0x02, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} 24 | s := decodeSensors(data) 25 | if s.Temperature != -0.2 { 26 | t.Errorf("Temperature incorrect, got: %.1f, expected: %.1f", s.Temperature, -0.2) 27 | } 28 | } 29 | --------------------------------------------------------------------------------