Talking to a Xiaomi Mijia Temperature Sensor

|

I have a colleague who showed me these lovely Xiaomi Mijia high precision temperature sensors, which can tell you the temperature and air humidity. They are battery powered and they broadcast their information through Bluetooth Low Energy (BLE).

Xiaomi Mijia Sensor

My colleague had some issues reading out the values the sensors broadcast over BLE, so I took it up as a small challenge to figure out how to read the values out in a Xamarin.Android App.

For the task I chose to use NuGet package Plugin.BLE, which I have used before for some other projects with great success. Lots of praise to Sven-Michael Stübe, who is the author behind the plugin. A lot of work has gone into making it and he made it very nice to work with, providing a an easy to use API, which works on both Xamarin.Android and Xamarin.iOS.

So first things first. In order to be allowed to scan for BLE devices on Android, you have to request the Location permission. For this I employ the trusty Permissions Plugin. Here I can request the permission like so:

private async Task<bool> RequestPermissions()
{
    if (DeviceInfo.Platform != DevicePlatform.Android)
        return true;

    try
    {
        var status = await CrossPermissions.Current.CheckPermissionStatusAsync(Permission.Location);
        if (status != PermissionStatus.Granted)
        {
            var requestStatus = await CrossPermissions.Current.RequestPermissionsAsync(Permission.Location);
            if (requestStatus.ContainsKey(Permission.Location))
            {
                return requestStatus[Permission.Location] == PermissionStatus.Granted;
            }
        }
    }
    catch (Exception ex)
    {
        //Something went wrong
    }

    return false;
}

Now that we have a way to get permission to scan for BLE devices, we can start scanning for BLE broadcasts.

var ble = CrossBluetoothLE.Current;
var adapter = CrossBluetoothLE.Current.Adapter;
adapter.DeviceDiscovered += OnDeviceDiscovered;

As you can see, in 3 lines of code we have set up our code to get discovered devices. In the OnDeviceDiscovered method we get a model describing the BLE device, including advertisement records and much more. We will use the advertisement records later to get the values from the Mijia Sensor.

Since this App is only interested in Xiaomi Mijia devices, we can provide a filter for the BLE plugin, such that we limit our search to such devices only. The Xiaomi Mijia devices I have all advertise themselves as “MJ_HT_V1”. Hence, I created a simple filter, checking for that name.

private bool DeviceFilter(IDevice device)
{
    if (device.Name?.StartsWith("MJ_HT_V1") ?? false)
        return true;

    return false;
}

Then we can start scanning for devices by calling:

adapter.ScanMode = ScanMode.LowLatency;
await adapter.StartScanningForDevicesAsync(null, DeviceFilter, cancellationToken: token);

I have ensured that the ScanMode is Low Latency, just notice that this scan mode uses a lot of power.

Scanning for devices start popping up the devices I have. Now to the fun part parsing the advertisement records the Xiaomi Mijia is sending. From reading a bunch of other clever people reverse engineering the device protocol I figured out that the Xiaomi Mijia sends out 4 different kinds of advertisement records. These can be identified by reading the 14th byte in the advertisement record. Then reading the subsequent bytes and piece them together to a value.

Byte 14 Type of value Bytes to read
0x04 Temperature 17 + 18
0x06 Humidity 17 + 18
0x0A Battery 17
0x0D Temperature and Humidity temp: 17 + 18
hum: 19 + 20

The read values have to be combined and then divided by 10.0 for battery and humidity to get the values in degrees Celsius and humidity percentage.

So typically these advertisement records would look like:

95 FE 50 20 AA 01 57 29 AE 33 34 2D 58 0D 10 04 DB 00 CC 01 which corresponds to 21.9 degrees C and 46% humidity.

95 FE 50 20 AA 01 5E 29 AE 33 34 2D 58 06 10 02 CB 01 which is 45.9% humidity.

Parsing the data I ended up with a method looking something like this:

private (double battery, double? temperature, double? humidity) ReadServiceData(byte[] data)
{
    if (data.Length < 14) return (-1, null, null);

    double battery = -1;
    double? temp = null;
    double? humidity = null;

    if (data[13] == 0x04) //temp
    {
        temp = BitConverter.ToUInt16(new byte[] { data[16], data[17] }, 0) / 10.0;
    }
    else if (data[13] == 0x06) //humidity
    {
        humidity = BitConverter.ToUInt16(new byte[] { data[16], data[17] }, 0) / 10.0;
    }
    else if (data[13] == 0x0A) //battery
    {
        battery = data[16];
    }
    else if (data[13] == 0x0D) //temp + humidity
    {
        temp = BitConverter.ToUInt16(new byte[] { data[16], data[17] }, 0) / 10.0;
        humidity = BitConverter.ToUInt16(new byte[] { data[18], data[19] }, 0) / 10.0;
    }

    if (temp.HasValue)
        System.Diagnostics.Debug.WriteLine($"Temp: {temp.Value}");
    if (humidity.HasValue)
        System.Diagnostics.Debug.WriteLine($"Humidity: {humidity.Value}");

    System.Diagnostics.Debug.WriteLine($"Battery: {battery}");

    return (battery, temp, humidity);
}

Piecing all of this together in a little MvvmCross App, yielded me something like seen in this screenshot.

Android App Screenshot

You can find the entire source code for the App on GitHub.