Xiaomi sensors/appliances

Posted on
  • Hi all, just want to share a code snippet and some thoughts/ideas on how to interact with Xiaomi devices.

    There are a few Xiaomi devices that have internal bluetooth module, e.g.:

    • Mi Flora plant sensor
    • Temperature and Humidity sensor
    • Mi Smart Scale
    • Mi Smart Kettle

    Xiaomi uses its own proprietary authentication protocol in their devices. The protocol logic is encapsulated into a native shared library (Android JNI library) that is used to generate/encode/decode authentication tokens which must be exchanged with the central device. Xiaomi devices only allow a very short lived connection while they are expecting to receive an authentication token. Once authentication fails, device drops its connection. More info on the Xiaomi authentication protocol can be found here. Fortunately, Xiaomi devices actively advertise their data which can be easily received and decoded.

    It turns out that all Xiaomi devices (that I'm aware of) use a common protocol/data structure for their advertisement messages that can be easily decoded, hence no need in establishing any direct connection with the device. I've decoded them and came up with some custom GATT definitions that describes the protocol and data structure here and here.

    Here is a code snippet that shows how to decode Temperature and Humidity sensor data:

    var temp = null;
    var humidity = null;
    var battery = null;
    
    function getSensorData() {
        NRF.findDevices(function(devices) {
                if (devices[0] && devices[0].serviceData.fe95) {
                    var data = devices[0].serviceData.fe95;
                    var flag = data[11];
                    switch (flag) {
                        case 4: {
                            // temp
                            temp = (data[14] | data[15] << 8) / 10;
                            break;
                        }
                        case 6: {
                            // humidity
                            humidity = (data[14] | data[15] << 8) / 10;
                            break;
                        }
                        case 10: {
                            // battery
                            battery = devices[0].serviceData.fe95[14];
                            break;
                        }
                        case 13: {
                            // temp and humidity
                            temp = (data[14] | data[15] << 8) / 10;
                            humidity = (data[16] | data[17] << 8) / 10;
                            break;
                        }
                        default : {
                            print("Unknown flag: " + flag);
                        }
                    }
                }
                for (var i = 0; i < devices.length; i++) {
                    devices[i] = null;
                }
                devices = null;
            }, {
                filters: [
                    { id: "4c:65:a8:d0:7a:ee public" }
                ],
                timeout: 2000
            }
        );
    }
    
    setInterval(function () {
        print("Temp: " + temp + " humidity: " + humidity + " battery: " + battery);
        getSensorData();
    }, 5000);
    

    As I mentioned, all devices share the same notification data structure, hence it is easy to come up with very similar code that would decode advertisements from other Xiaomi devices by following the gatt specs that I provided.

    PS. The provided gatt spec files is a part of Gatt Parser, Java Bluetooth Manager and Eclipse SmartHome binding that I'm working on, you may find it useful too. The official discussion thread here.

    Let me know if you have any questions.

  • This is great - thanks for posting it up separately to your other stuff! I've been meaning to try one of those Xiaomi Plant moisture sensors for ages, and this gives me a good excuse :)

    If you're interested, you should be able to change the last part of NRF.findDevices to:

            }, {
                filters: [
                    {serviceData:{"fe95":{}}}
                ],
                timeout: 2000
            }
    

    which will automatically filter out only devices that advertise that service data - it's a bit faster and more efficient with memory if there are a bunch of BLE devices.

  • Hi @Gordon, hi @Vlad

    I have a Xiaomi LYWSD03MMC Thermometer / Hygrometer and with this CircuitPython library I can fetch its values:

    https://github.com/adafruit/Adafruit_Cir­cuitPython_BLE_LYWSD03MMC/blob/main/adaf­ruit_ble_lywsd03mmc.py

    Now I'd like to make everything work on Espruino thanks to your code.

    I changed filters: [{serviceData: {"fe95": {}}}] the device is found (although its serviceData should be "ebe0ccb0") but all values remain null.

    How do you think it could be solved?

    THANK YOU

  • Which device are you wanting to run this on?

    There's actually a Bangle.js app for this already at: https://github.com/espruino/BangleApps/b­lob/master/apps/miplant/app.js

    So to run on another device you could just copy var deviceInfo = {};, parseDevice and the NRF.setScan command, then check in deviceInfo after a few seconds and see if you get any data.

    The code posted above (using findDevices) isn't ideal as it doesn't catch every advertising packet. To get all the information you need you have to have it scanning for advertising packets for a while

  • Hi @Gordon, thanks for the reply.

    As soon as I have time, I try the code on my Pixl.js and tell you!

    See you soon :-)

  • I have a Xiaomi LYWSD03MMC Thermometer / Hygrometer

    Guess you already know but there is alternative firmware for this thermometer, it is pretty hackable device (Telink 8251 - 32K ram,512K flash). Was started by atc1441 and there is now nice fork here https://github.com/pvvx/ATC_MiThermomete­r with many additional features like logging data to flash. So if you would have issues with original Xiaomi firmware (like encrypted advertisement data) this one may be easier to work with. Also flashing that Telink chip is easy (both OTA and SWS over serial).

  • Hi @Gordon, I uploaded this code to Pixl.js (2v08)

    var deviceInfo = {};
    
    function parseDevice(device) {
      var d = new DataView(device.serviceData["fe95"]);
      var frame = d.getUint16(0, true);
      var offset = 5;
    
      console.log("FRAME: " + frame);
    
      if (frame&16) offset+=6; // mac address
      if (frame&32) offset+=1; // capabilitities
      if (frame&64) { // event
        var l = d.getUint8(offset+2);
        var code = d.getUint16(offset,true);
        if (!deviceInfo[device.id]) deviceInfo[device.id]={id:device.id};
        event = deviceInfo[device.id];
        switch (code) {
          case 0x1004: event.temperature = d.getInt16(offset+3,true)/10; break;
          case 0x1006: event.humidity = d.getInt16(offset+3)/10; break;
          case 0x100D:
            event.temperature = d.getInt16(offset+3,true)/10;
            event.humidity = d.getInt16(offset+5)/10; break;
          case 0x1008: event.moisture = d.getUint8(offset+3); break;
          case 0x1009: event.fertility = d.getUint16(offset+3,true)/10; break;
          // case 0x1007: break; // 3 bytes? got 84,0,0 or 68,0,0
          default: event.code = code;
            event.raw = new Uint8Array(d.buffer, offset+3, l);
            break;
        }
        print(event);
      }
    
    };
    
    setInterval(function () {
        NRF.setScan(parseDevice, { filters: [{serviceData:{"fe95":{}}}], timeout: 2000 });
    }, 5000);
    

    It always prints FRAME, and when the temperature or humidity changes it prints the event:

    FRAME: 22616
    {
      "id": "a5:c2:83:xx:xx:xx public",
      "code": 50578,
      "raw": new Uint8Array([208, 117, 0, 0, 58, 221, 132, 170, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
     }
    
    FRAME: 22616
    {
      "id": "a5:c2:83:xx:xx:xx public",
      "code": 15670,
      "raw": new Uint8Array([121, 182, 117, 0, 0, 185, 254, 166, 227, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
     }
    

    I have no idea how to extract the temperature and humidity :-)

    Can I get some useful info from this Python code (0x80, 0x7F)?

    https://github.com/adafruit/Adafruit_Cir­cuitPython_BLE_LYWSD03MMC/blob/main/adaf­ruit_ble_lywsd03mmc.py

    THANK YOU

  • Hi @fanoush, thanks for the link, I initially saw something about: https://github.com/atc1441/ATC_MiThermom­eter

    Then I saw that through Python, without any hacks, you can get the values easily.

    Maybe later, I sacrifice one in the name of science :-)

  • Maybe later, I sacrifice one in the name of science :-)

    Yes, definitely optional thing for later.

    As for sacrifices - it should be possible to restore it either over BLE or at worst it should be always recoverable via SWS interface - pin on board. This is similar debugging/programming interface to ARM SWD but thanks to clever hack you even don't need special HW for recovery, the right data for SWS can be generated in software and sent via serial uart.

    Anyway, they are pretty cheap after all (below $4/piece when getting 4 of them https://www.aliexpress.com/item/40010957­35569.html ) so even if completely broken it is not that bad :-)

  • The adafruit code appears to actually connect to the thermometer rather than just looking at advertisements, so I don't know how useful that is.

    It looks to me like the advertising is using a different format to the one other Xiaomi thermometers use. It's probably better to do:

    function parseDevice(device) {
      console.log(device.serviceData["fe95"]);­
    }
    

    And then show us everything it prints over the course of a few minutes, with a rough idea of the temperature and humidity the device is showing - and we might be able to work out how to decode it then

  • The adafruit code appears to actually connect to the thermometer rather than just looking at advertisements, so I don't know how useful that is.

    That's because data in advertisement is encrypted, see https://github.com/Magalex2x14/LYWSD03MM­C-info#encryption When connected there is no such encryption! And any device can connect without pairing so it all makes very little sense.

    That's why I suggested alternative firmware - there is no such encryption nonsense there. And the firmware was developed from scratch (with Telink SDK available on github) and is opensource.

  • Hi @fanoush, you are right, in the next few days I will hack one :-)

    See you soon!

  • Hi, with the custom firmware everything works perfectly!

    After flashing, following these directions: https://github.com/pvvx/ATC_MiThermomete­r

    @Vlad's code fetches values without problems:

    var temp = null;
        var humidity = null;
        var battery = null;
        function getSensorData() {
            NRF.findDevices(function(devices) {
                    if (devices[0] && devices[0].serviceData.fe95) {
                        var data = devices[0].serviceData.fe95;
                        // console.log(data);
                        var flag = data[11];
                        // console.log(flag);
                        
                        switch (flag) {
                            case 4: {
                                // temp
                                temp = (data[14] | data[15] << 8) / 10;
                                break;
                            }
                            case 6: {
                                // humidity
                                humidity = (data[14] | data[15] << 8) / 10;
                                break;
                            }
                            case 10: {
                                // battery
                                battery = devices[0].serviceData.fe95[14];
                                break;
                            }
                            case 13: {
                                // temp and humidity
                                temp = (data[14] | data[15] << 8) / 10;
                                humidity = (data[16] | data[17] << 8) / 10;
                                break;
                            }
                            default : {
                                print("Unknown flag: " + flag);
                            }
                        }
                    }
                    for (var i = 0; i < devices.length; i++) {
                        devices[i] = null;
                    }
                    devices = null;
                }, {
                    filters: [
                        {serviceData:{"fe95":{}}}
                    ],
                    timeout: 2000
                }
            );
        }
        setInterval(function () {
            print("Temp: " + temp + " humidity: " + humidity + " battery: " + battery);
            getSensorData();
        }, 5000);
    

    THANK YOU ALL!

  • Thanks for the update, and glad it's all working ok!

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

Xiaomi sensors/appliances

Posted by Avatar for Vlad @Vlad

Actions