The MI32-driver focuses on the passive observation of BLE sensors of the Xiaomi/Mijia universe, thus only needing a small memory footprint. This allows to additionally run a dedicated software bridge to Apples HomeKit with very few additional configuration steps. Berry can be used to create generic applications.
Currently supported are the original ESP32, the ESP32-C3 and the ESP32-S3.
This driver is not part of any standard build. To self compile it is recommended to add build environments to platformio_tasmota_cenv.ini
. This file needs to be created first.
Add these sections:
[env:tasmota32-mi32-homebridge]
extends = env:tasmota32_base
build_flags = ${env:tasmota32_base.build_flags}
-DUSE_MI_ESP32
-DUSE_MI_EXT_GUI
-DUSE_MI_HOMEKIT=1 ; 1 to enable; 0 to disable
lib_extra_dirs = lib/libesp32, lib/libesp32_div, lib/lib_basic, lib/lib_i2c, lib/lib_div, lib/lib_ssl
lib_ignore = ESP8266Audio
ESP8266SAM
TTGO TWatch Library
Micro-RTSP
epdiy
esp32-camera
[env:tasmota32c3-mi32-homebridge]
extends = env:tasmota32c3
build_flags = ${env:tasmota32_base.build_flags}
-DUSE_MI_ESP32
-DUSE_MI_EXT_GUI
-DUSE_MI_HOMEKIT=1 ; 1 to enable; 0 to disable
lib_extra_dirs = lib/libesp32, lib/libesp32_div, lib/lib_basic, lib/lib_i2c, lib/lib_div, lib/lib_ssl
lib_ignore = ESP8266Audio
ESP8266SAM
TTGO TWatch Library
Micro-RTSP
epdiy
esp32-camera
It is probably necessary to restart your IDE (i.e. Visual Studio Code) to see the option to build these environments.
Different vendors offer Bluetooth solutions as part of the XIAOMI family often under the MIJIA-brand (while AQUARA is the typical name for a ZigBee sensor).
The sensors supported by Tasmota use BLE (Bluetooth Low Energy) to transmit the sensor data, but they differ in their accessibilities quite substantially.
Basically all of them use of so-called „MiBeacons“ which are BLE advertisement packets with a certain data structure, which are broadcasted by the devices automatically while the device is not in an active bluetooth connection.
The frequency of these messages is set by the vendor and ranges from one per 3 seconds to one per hour (for the battery status of the LYWSD03MMC). Motion sensors and BLE remote controls start to send when an event is triggered.
These packets already contain the sensor data and can be passively received by other devices and will be published regardless if a user decides to read out the sensors via connections or not. Thus the battery life of a BLE sensor is not influenced by reading these advertisements and the big advantage is the power efficiency as no active bi-directional connection has to be established. The other advantage is, that scanning for BLE advertisements can happen nearly parallel (= very quick one after the other), while a direct connection must be established for at least a few seconds and will then block both involved devices for that time.
This is therefore the preferred option, if technically possible (= for the supported sensors).
Most of the „older“ BLE-sensor-devices use unencrypted messages, which can be read by all kinds of BLE-devices or even a NRF24L01. With the arrival of "newer" sensors came the problem of encrypted data in MiBeacons, which can be decrypted in Tasmota.
Meanwhile it is possible to get the needed "bind_key" without the need to use Xiaomis apps and server infrastructure.
At least the LYWSD03 allows the use of a simple BLE connection without any encrypted authentication and the reading of the sensor data using normal subscription methods to GATT-services (currently used on the HM-1x). This is more power hungry than the passive reading of BLE advertisements. It is not directly supported by the driver anymore, but can be realized in Berry, i.e. to read the correct battery status.
Other sensors like the MJYD2S and nearly every newer device are not usable without the "bind_key".
The idea is to provide as many automatic functions as possible. Besides the hardware setup, there are zero or very few things to configure.
The sensor namings are based on the original sensor names and shortened if appropriate (Flower care -> Flora). A part of the MAC will be added to the name as a suffix.
All sensors are treated as if they are physically connected to the ESP32 device. For motion and remote control sensors MQTT-messages will be published in (nearly) real time.
!!! note "It can not be ruled out, that changes in the device firmware may break the functionality of this driver completely!"
The naming conventions in the product range of bluetooth sensors in XIAOMI-universe can be a bit confusing. The exact same sensor can be advertised under slightly different names depending on the seller (Mijia, Xiaomi, Cleargrass, ...).
The encrypting devices will start to send advertisements with encrypted sensor data after pairing it with the official Xiaomi app. Out-of-the-box the sensors do only publish a static advertisement.
It is possible to do a pairing and get the necessary decryption key ("bind_key") right here in the Wiki. This method uses the same code base as the first published working example: https://atc1441.github.io/TelinkFlasher.html
This project also provides a custom firmware for the LYWSD03MMC, which then becomes an ATC and is supported by Tasmota too. Default ATC-setting will drain the battery more than stock firmware, because of very frequent data sending.
This key and the corresponding MAC of the sensor can be injected with the MI32key-command (or NRFMJYD2S), but the new and recommended option is to use a mi32cfg file.
It is still possible to save the whole config as RULE like that: (not recommended anymore!)
rule1 on System#Boot do backlog MI32key 00112233445566778899AABBCCDDEEFF112233445566; MI32key 00112233445566778899AABBCCDDEEFF112233445566 endon
(key for two sensors)
There are several ways to manage and save your configuration.
-
Do it on the device
Starting after a fresh install the driver will not find a configuration file and begins to search for every BLE device, that it can unterstand. Thus after a while all BLE sensors in sight should have been added to the non-persistent internal memory.
You can save them with commandMI32CFG
, which will create a JSON-file named 'mi32cfg' in the root folder of the internal flash of the ESP32. After the next reboot, the driver will read this configuration into memory and does not add more devices to this list. -
Create the mi32cfg file manually
After a fresh install you can simply create a file in the root folder of the flash file system with the name 'mi32cfg' and paste the JSON into it. Save it and reboot. -
Adding sensors including the keys directly on this page
It is recommended to paste the data of 'mi32cfg' into the next textfield, if you already have one. After that press IMPORT MI32CFG. The config will get parsed and presented in a table.
MI32CFG - nothing imported yet
Import MI32Cfg === "Table"
empty config
=== "JSON"
``` haskell
[]
```
After that you can add more sensors with the following Bind Key Generator, which will also add sensors, that do not need a key. This will only work, if your browser supports this and should work with Opera, Chrome and Edge. Safari and Firefox are not able to do this.
After successful pairing a sensor in the next step or simply connecting to a non-encrypting sensor, the JSON in the textfield above will be updated with the added new sensor at the bottom.
You can copy-paste the new JSON via the Web-GUI to the mi32cfg file on the ESP32 or save it elsewhere. For adding more sensors, repeat the whole procedure after refreshing the site (after saving your data!!).
Will disable device in MiHome
When doing an activation here the device is needed to be activated in the Mi app again when wanted to use there.
BLE device name prefix filter(s)
Reconnect Reset Page Disconnect
Mi Bind Key: MAC:
Do Activation
<button type="button" class="md-button md-button--primary"onclick="clearLog();">Clear Log
??? summary "BLE-Log:"
.
!!! tip
If you really want to read battery for LYWSD02, Flora and CGD1, consider doing it in Berry.
!!! tip
If you want to add a new BLE sensor to your config on the device, use `MI32option3 1` to add the new sensor by catching a BLE packet. Then use `MI32Cfg` to save the new config on flash.
The driver provides an extended web GUI to show the observed Xiaomi sensors in a widget style, that features a responsive design to use the screen area as effective as possible. The other advantage is, that only the widget with new data gets redrawn (indicated by a fading circle) and no unnecessary refresh operations will happen. A simple graph shows if valid data for every hour was received in the last 24h, where only one gap for the coming hour is not a sign of an error. Configured sensors with no received packet since boot or key/decryption errors are dimmed.
If activated at compile time the driver will start the HAP core (= the main task of the HomeKit framework) after successfully reading a valid mi32cfg file after the start. It will create a 'bridge accessory' presenting all configured BLE devices to HomeKit. You can add the ESP32 as such a Mi-Home-Bridge to HomeKit in the native way, like you would add a commercial product to you local HomeKit network. The setup key is derived from the Wifi MAC of your ESP32 to easily allow many ESP32 to be used as a HomeKit bridge in your local network.
Besides the driver will also manage up to four relays and sync them with HomeKit.
There is nothing more to configure, the driver will automatically translate the data packets back and forth.
It just works ... except, when it does not.
!!! danger "Known issues"
The underlying HAP framework will expose incompatible network configurations, which will likely be related to mDNS and IGMP settings. There is nothing, that the Tasmota side can fix here.
!!! danger "If it is not broken, do not upgrade"
Although the driver does not write to the NVS section and the usage of a modified HAP framework (using a NVS wrapper) the behavior of Tasmota firmware upgrades is undefined with regards to a working HomeKit installation. Similar things can also happen using much bigger projects like "Homebridge". So it might be necessary to completely erase the Mi-Bridge from your "Home" in Homekit and doing a flash erase of the ESP32 after a firmware upgrade of the ESP32. It is recommend to first create a final mi32cfg file before you add the Mi-Home-Bridge to your "Home".
This will generate a QR-Code based on the MAC address of the ESP32 which runs Tasmotas Homekit-Bridge. Use the camera of your iPhone or iPad to easily start the setup procedure.
<script src="../extra_javascript/mi32/qrcode.js"></script>After creating a valid configuration with a mi32cfg
file in the local file system, it is possible to announce all sensors to Homeassistant via MQTT discovery by using a Berry script. This will parse the mi32cfg
file and create all needed entities for Homassistant by publishing specific messages to Homeassistant.
It will not generate duplicated sensors, but instead allows to use multiple ESP's as data sources for the same BLE sensor.
The best way is to not fiddle around with the default Tasmota configuration, especially not to change the default topic name, because this will lose the ability to automatically configure everything.
One way to use it, is to save the following script disco.be to the filesystem of the ESP and the launch it at the startup.
Create autoexec.bat
if not already present and add the following line:
br load("disco")
This will create and/or init entities for every sensor and group them as a single device for every BLE device in Homeassistants MQTT integration.
In the diagnostic panel of every sensor you will see the signal strength of the BLE sensor in relation to the observing ESP, so the value will very likely differ between multiple of these BLE-ESP32-combinations. A virtual Tasmota BLE Hub device is created, that shows all contributing ESP32 nodes for a better overview.
For sensors like humidity or temperature it should not matter, how many ESP's do contribute data. For buttons of a remote control or binary sensors like motion, this could have side effects, as multiple events will be generated (in a very short time frame). The dimmer of the YLKG08 is special case, as the data of the BLE sensor are relative steps, that are combined to a so called number
entity with a range of 0 - 100. That way multiple messages from many ESP's will add up and "accelerate" the dimmer knob.
!!! tip
Use the embedded [MI32CFG Importer](#mi32cfg-importer-web-app) on this site to delete unwanted sensors and then save the result to the ESP32 of your choice.
The driver provides two Berry modules to allow extensions and interactions with the sensors. It is also possible to write generic BLE functions unrelated to Xiaomi sensors.
This module allows access and modifification of the internal data backend of the MI32 driver for the observed Xiaomi sensors.
First we need to import the module:
import MI32
We have the following methods, which are chosen to be able to replace the old commands MI32Battery, MI32Unit and MI32Time:
MI32.devices()
: returns the number of monitored Xiaomi devicesMI32.get_name(x)
: returns a string with the sensor name (internal name of the driver) at slot x in the internal sensor array of the driverMI32.get_MAC(x)
: returns a byte buffer (6 bytes) representing the MAC at slot x in the internal sensor array of the driverMI32.set_bat(x,v)
: sets the battery value to v at slot x in the internal sensor array of the driverMI32.set_hum(x,v)
: sets the humidity value to v at slot x in the internal sensor array of the driverMI32.set_temp(x,v)
: sets the temperature value to v at slot x in the internal sensor array of the driver
For generic BLE access we import the module:
import BLE
To simplify BLE access this works in the form of state machine, where you have to set some properties and then finally launch an operation. Besides we have two callback mechanisms for listening to advertisements and active sensor connections. Both need a byte buffer in Berry for data exchange and a Berry function as the callback.
To listen to advertisements inside a class (that could be a driver) we could initialize like that:
!!! example "Simple Advertisement Listener"
var buf
def init()
import BLE
self.buf = bytes(-64)
var cbp = tasmota.gen_cb(/s,m-> self.cb(s,m))
BLE.adv_cb(cbp,self.buf)
end
def cb(svc,manu)
print(buf) # simply prints out the byte buffer of an advertisement packet
if svc != 0 # if service data present
print("service data:")
var _len = self.buf[svc-2]-1
# the index points to the data part of an AD element, two position before that is length of "type + data",
# so we subtract one byte from that length to get the "pure" data length
print(self.buf[svc.._len+svc])
end
if manu != 0 # if manufacturer data present
print("manufacturer data:")
var _len = self.buf[manu-2]-1
print(self.buf[manu.._len+manu])
end
end
To stop listening call:
BLE.adv_cb(0)
We just have to provide a pointer to a (callback) function and a byte buffer. The returned data in the byte buffer uses the following proprietary format:
6 bytes - MAC
1 byte - address type
1 byte - RSSI
1 byte - length of payload
n bytes - payload data
The advertisement callback function provides 2 arguments, which are indices of the whole buffer that point to optional parts of the payload. A value of 0 means, this type of of element is not in the payload.
- svc (= service data index) - index of service data in the advertisement buffer
- manu (= manufacturer data index) - index of manufacturer data in the advertisement buffer
The payload is always provided completely, so every possibles AD type can be parsed in Berry if needed, but for convenience the two most important types for IOT applications are given in the callback.
!!! tip
The payload can be parsed according to the BLE GAP standard. It consists of AD elements of variable size in the format length-type-data, where the length byte describes the length of the two following components in bytes, the type byte is defined in the GAP and the data parts of 'length-1' bytes is interpreted according to the type.
Two methods for filtering of advertisements are provided:
BLE.adv_watch(MAC,type)
: watch BLE address exclusively, is added to a list (MAC is a 6-byte-buffer, type is optional 0-3, default is 0)
BLE.adv_block(MAC,type)
: block BLE address, is added to a list(MAC is a 6-byte-buffer, type is optional 0-3, default is 0)
!!! tip
The watchlist is more effective to avoid missing packets, than the blocklist in environments with high BLE traffic. Both methods work for the internal Xiaomi driver and the post processing with Berry.
Communicating via connections is a bit more complex. We have to start with a callback function and a byte buffer again.
# simple example for the Berry console
import BLE
cbuf = bytes(-64)
def cb(error,op,uuid)
end
cbp = tasmota.gen_cb(cb)
BLE.conn_cb(cbp,cbuf)
Error codes:
- 0 - no error
- 1 - connection error
- 2 - did disconnect
- 3 - did not get service
- 4 - did not get characteristic
- 5 - could not read value
- 6 - characteristic can not notify
- 7 - characteristic not writable
- 8 - did not write value
- 9 - timeout: did not read on notify
Op codes:
- 1 - read
- 2 - write
- 3 - subscribe - direct response after launching a run command to subscribe
- 103 - notify read - the notification with data from the BLE server
UUID:
Returns the 16 bit UUID of the characteristic as a number, that returns a value.
Internally this creates a context, that can be modified with the following methods:
Set the MAC of the device we want to connect to:
BLE.set_MAC(MAC,type)
: where MAC is a 6-byte-buffer, type is optional 0-3, default is 0
Set service and characteristic:
BLE.set_svc(string)
: where string is a 16-Bit, 32-Bit or 128-Bit service uuid
BLE.set_chr(string)
: where string is a 16-Bit, 32-Bit or 128-Bit characteristic uuid
Finally run the context with the specified properties and (if you want to get data back to Berry) have everything prepared in the callback function:
BLE.run(operation,response)
: where operation is a number (optional: boolean w/o server response to write or subscribe, default is false) , that represents an operation in a proprietary format. Values below 10 will not disconnect automatically after completion:
-
1 - read
-
2 - write
-
3 - subscribe
-
5 - disconnect
-
11 - read - then disconnect (returns 1 in the callback)
-
12 - write - then disconnect (returns 2 in the callback)
-
13 - subscribe - then disconnect after waiting for notification(returns 3 in the callback)
The buffer format for reading and writing is in the format (length - data):
1 byte - length of data in bytes
n bytes - data
Here is an implementation of the "old" MI32 commands: !!! example "removed MI32 commands in Berry"
```python
import BLE
import MI32
j = 0
sl = 0
cbuf = bytes(-64)
def cb()
if j == 0
print(cbuf)
end
if j == 1
var temp = cbuf.get(1,2)/100.0
var hum = cbuf.get(3,1)*1.0
var bat = (cbuf.get(4,2)-2100)/12
MI32.set_temp(sl,temp)
MI32.set_hum(sl,hum)
MI32.set_bat(sl,bat)
end
if j == 4
var bat = cbuf.get(1,1)
MI32.set_bat(sl,bat)
end
end
cbp = tasmota.gen_cb(cb)
BLE.conn_cb(cbp,cbuf)
def SetMACfromSlot(slot)
if slot+1>MI32.devices()
return "out of bounds"
end
sl = slot
BLE.set_MAC(MI32.get_MAC(slot))
end
def MI32Time(slot)
SetMACfromSlot(slot)
BLE.set_svc("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6")
BLE.set_chr("EBE0CCB7-7A0A-4B0C-8A1A-6FF2997DA3A6")
cbuf[0] = 5
var t = tasmota.rtc()
var utc = t.item("utc")
var tz = t.item("timezone")/60
cbuf.set(1,utc,4)
cbuf.set(5,tz,1)
j = 0
BLE.run(12,true)
end
def MI32Unit(slot,unit)
SetMACfromSlot(slot)
BLE.set_svc("EBE0CCB0-7A0A-4B0C-8A1A-6FF2997DA3A6")
BLE.set_chr("EBE0CCBE-7A0A-4B0C-8A1A-6FF2997DA3A6")
cbuf[0] = 1
cbuf[1] = unit
j = 0
BLE.run(12,true)
end
def MI32Bat(slot)
SetMACfromSlot(slot)
var name = MI32.get_name(slot)
if name == "LYWSD03"
BLE.set_svc("ebe0ccb0-7A0A-4B0C-8A1A-6FF2997DA3A6")
BLE.set_chr("ebe0ccc1-7A0A-4B0C-8A1A-6FF2997DA3A6")
j = 1
BLE.run(13)
end
if name == "MHOC401"
BLE.set_svc("ebe0ccb0-7A0A-4B0C-8A1A-6FF2997DA3A6")
BLE.set_chr("ebe0ccc1-7A0A-4B0C-8A1A-6FF2997DA3A6")
j = 1
BLE.run(13,true)
end
if name == "LYWSD02"
BLE.set_svc("ebe0ccb0-7A0A-4B0C-8A1A-6FF2997DA3A6")
BLE.set_chr("ebe0ccc1-7A0A-4B0C-8A1A-6FF2997DA3A6")
j = 2
BLE.run(11,true)
end
if name == "FLORA"
BLE.set_svc("00001204-0000-1000-8000-00805f9b34fb")
BLE.set_chr("00001a02-0000-1000-8000-00805f9b34fb")
j = 3
BLE.run(11,true)
end
if name == "CGD1"
BLE.set_svc("180F")
BLE.set_chr("2A19")
j = 4
BLE.run(11,true)
end
end
```
??? example "Govee desk lamp - pre-alpha"
```python
# control a BLE Govee desk lamp
class GOVEE : Driver
var buf
def init(MAC)
import BLE
self.buf = bytes(-21) # create a byte buffer, first byte reserved for length info
self.buf[0] = 20 # length of the data part of the buffer in bytes
self.buf[1] = 0x33 # a magic number - control byte for the Govee lamp
var cbp = tasmota.gen_cb(/e,o,u->self.cb(e,o,u)) # create a callback function pointer
BLE.conn_cb(cbp,self.buf)
BLE.set_MAC(bytes(MAC),1) # addrType: 1 (random)
end
def cb(error,op,uuid)
if error == 0
print("success!")
return
end
print(error)
end
def chksum()
var cs = 0
for i:1..19
cs ^= self.buf[i]
end
self.buf[20] = cs
end
def clr()
for i:2..19
self.buf[i] = 0
end
end
def writeBuf()
import BLE
BLE.set_svc("00010203-0405-0607-0809-0a0b0c0d1910")
BLE.set_chr("00010203-0405-0607-0809-0a0b0c0d2b11")
self.chksum()
print(self.buf)
BLE.run(12) # op: 12 (write, then disconnect)
end
end
gv = GOVEE("AABBCCDDEEFF") # MAC of the lamp
tasmota.add_driver(gv)
def gv_power(cmd, idx, payload, payload_json)
if int(payload) > 1
return 'error'
end
gv.clr()
gv.buf[2] = 1 # power cmd
gv.buf[3] = int(payload)
gv.writeBuf()
end
def gv_bright(cmd, idx, payload, payload_json)
if int(payload) > 255
return 'error'
end
gv.clr()
gv.buf[2] = 4 # brightness
gv.buf[3] = int(payload)
gv.writeBuf()
end
def gv_rgb(cmd, idx, payload, payload_json)
var rgb = bytes(payload)
print(rgb)
gv.clr()
gv.buf[2] = 5 # color
gv.buf[3] = 5 # manual ??
gv.buf[4] = rgb[3]
gv.buf[5] = rgb[0]
gv.buf[6] = rgb[1]
gv.buf[7] = rgb[2]
gv.writeBuf()
end
def gv_scn(cmd, idx, payload, payload_json)
gv.clr()
gv.buf[2] = 5 # color
gv.buf[3] = 4 # scene
gv.buf[4] = int(payload)
gv.writeBuf()
end
def gv_mus(cmd, idx, payload, payload_json)
var rgb = bytes(payload)
print(rgb)
gv.clr()
gv.buf[2] = 5 # color
gv.buf[3] = 1 # music
gv.buf[4] = rgb[0]
gv.buf[5] = 0
gv.buf[6] = rgb[1]
gv.buf[7] = rgb[2]
gv.buf[8] = rgb[3]
gv.writeBuf()
end
tasmota.add_cmd('gpower', gv_power) # only on/off
tasmota.add_cmd('bright', gv_bright) # brightness 0 - 255
tasmota.add_cmd('color', gv_rgb) # color 00FF0000 - sometimes the last byte has to be set to something greater 00, usually it should be 00
tasmota.add_cmd('scene', gv_scn) # scene 0 - ?,
tasmota.add_cmd('music', gv_mus) # music 00 - 0f + color 000000 -- does not work at all!!!
# POWER = 0x01
# BRIGHTNESS = 0x04
# COLOR = 0x05
# MANUAL = 0x02 - seems to be wrong for this lamp
# MICROPHONE = 0x01 - can not be confirmed yet
# SCENES = 0x04
```
??? example "Xbox X/S controller - proof of concept"
```python
# just a proof of concept to connect a Xbox X/S controller
# must be repaired on every connect
class XBOX : Driver
var buf
def init(MAC)
import BLE
var cbp = tasmota.gen_cb(/e,o,u->self.cb(e,o,u))
self.buf = bytes(-256)
BLE.conn_cb(cbp,self.buf)
BLE.set_MAC(bytes(bytes(MAC)),0)
self.connect()
end
def connect() # separated to call it from the berry console if needed
import BLE
BLE.set_svc("1812")
BLE.set_chr("2a4a") # the first characteristic we have to read
BLE.run(1) # read
end
def handle_read_CB(uuid) # uuid is the notifying characteristic
import BLE
# we just have to read these characteristics before we can finally subscribe
if uuid == 0x2a4a # ignore data
BLE.set_chr("2a4b")
BLE.run(1) # read next characteristic
end
if uuid == 0x2a4b # ignore data
BLE.set_chr("2a4d")
BLE.run(1) # read next characteristic
end
if uuid == 0x2a4d # ignore data
BLE.set_chr("2a4d")
BLE.run(3) # start notify
end
end
def handle_HID_notifiaction() # a very incomplete parser
if self.buf[14] == 1
print("ButtonA") # a MQTT message could actually trigger something
end
if self.buf[14] == 2
print("ButtonB")
end
if self.buf[14] == 8
print("ButtonX")
end
if self.buf[14] == 16
print("ButtonY")
end
end
def cb(error,op,uuid)
if error == 0
if op == 1
print(op,uuid)
self.handle_read_CB(uuid)
end
if op == 3
self.handle_HID_notification()
end
return
else
print(error)
end
end
end
xbox = XBOX("AABBCCDDEEFF") # xbox controller MAC
tasmota.add_driver(xbox)
```