Xiaomi Mi Flora plant monitor is a cool high tech device for measuring plant’s soil moisture, and fertility, as well as light and temperature. The data is accessible via Bluetooth using the mobile app. The device is waterproof, the battery is said to last at least a year, and it costs about $17 on Amazon or Ebay.
Why the project? There are issues with the mobile app – no way to share the readings with your significant other, no comparison charts between multiple sensors, and the app has to be manually synced up from time to time.
Reading sensors automatically and displaying the data through the super-flexible Grafana dashboarding tool makes the investment in plant monitors to go so much farther.
Setting up your own backyard soil monitoring dashboard
You will need a Raspberry Pi Zero W, or two, if you want to monitor both backyard and frontyard, as Bluetooth Low Energy signal does not reach very far. A Raspberry Pi can pull backyard device data from a yard-facing windowsill, but not from across the house.
Amazon has them with a power supply, case and an SD card for about $33 shipped. Make sure you get the “W” version, as the other one has no Bluetooth or Wi-Fi.
You will need to install:
- Raspbian OS
- InfluxDB measurement database engine
- Grafana dashboarding tool
- Polling code from https://github.com/sergem155/miflora-influxdb
Installing Raspbian OS
Refer to official guide here and, to enable SSH and Wi-Fi, Stackexchange recommendation here
Once complete and booted, log in using SSH, username pi and default password raspberry.
Installing InfluxDB
Great reference article is here. One thing to note it that Pi Zero W shares CPU architecture with Raspberry Pi1, not more recent Pi2 or Pi3.
On Raspberry Pi:
$ sudo apt-get install apt-transport-https curl $ curl -sL https://repos.influxdata.com/influxdb.key | sudo apt-key add - $ echo "deb https://repos.influxdata.com/debian stretch stable" | sudo tee /etc/apt/sources.list.d/influxdb.list $ sudo apt-get update $ sudo apt install influxdb
edit /etc/influxdb/influxdb.conf
$ sudo service influxdb start $ influx > CREATE DATABASE plant_monitors
Installing Grafana
On Raspberry Pi:
$ sudo vi /etc/apt/sources.list.d/grafana.list $ echo "deb https://dl.bintray.com/fg2it/deb-rpi-1b stretch main" | sudo tee /etc/apt/sources.list.d/grafana.list $ sudo apt update $ sudo apt install grafana
edit /etc/grafana/grafana.ini
$ sudo service grafana-server restart
Installing polling code
On Raspberry Pi:
$ sudo apt update $ sudo apt install git $ sudo apt install python3-pip $ sudo pip3 install bluepy $ git clone https://github.com/sergem155/miflora-influxdb.git $ cp config-example.py config.py
Edit config.py to reflect the addresses of your plant monitors and the location of your influxdb.
Use sudo scan.py to detect new plant monitors. Hint – to avoid confusion, turn them on one by one between scans and mark them with a sharpie 🙂
Set up to run poll-insert.py from cron one a day
You will also need to make sure your Raspberry Pi’s Bluetooth supervision timeout is set to at least 1sec (1000ms), otherwise the connection might get interrupted by Pi’s controller while the plant monitor is in deep thought. You will want to update your Bluetooth supervision timeout periodically (it tends to reset itself)
$ sudo crontab -e 50 * * * * /home/pi/miflora-influxdb/set-supervision-timeout
Making dashboards
Go to grafana, e.g. http://flowers:3000/ in my case.
Create a new dashboard.
Edit query. One example is below, in both GUI and text representation:
Where the protocol comes from
The protocol between the phone app and the device is not published. Past attempts to figure it out (article) have resulted in code that still works, but only does a one-time immediate reading. It would have to be repeated periodically, arguably resulting in shorter battery life.
The app is able to pull past hourly data, so it was possible to figure the protocol by looking at Android bluetooth capture log file using Wireshark, consulting Android system log, and doing some Googling.
The code works with firmware versions 2.7.0 and 3.1.9. Warning: running your new device with a mobile phone app will most likely result in its firmware upgraded.
So how does the protocol look like?
- It performs a cute encrypted handshake. Without it, the device will hang up in the middle of hours retrieval.
- It gets device’s internal timer (seconds), as well as Pi’s timestamp as a reference
- It get the count of readings and then
- Reads hourly data, by asking to prepare the data for hour X and then reading it upon receiving a notification that it’s ready
More details are in the code
Cute handshake
The handshake, the hardest part to comprehend, consists of a fixed start command, a challenge, an encoded response and an encoded session finish command.
Upon XORing challenge and response, we can see that it the response is the challenge XORed by the same byte string that varies only from device to device. Looking at the alogcat system log from Android we can see that “generated token”, that is then sent as a challenge, is also XORed by the same byte sequence that varies only from device to device. What would I use for battery-cheap XOR-based encryption? RC4 stream cipher is the king of spreading randomness while doing very little. We just need to find the keys. They are different from device to device, so that must involve some serial number, most likely MAC.
Looking up the internets for “miio-bluetooth” from the alogcat output leads us to the couple of interesting pieces of code: one, that confirms that the key is the mix of MAC and product id, and two that shows the MAC is reversed and the key is 8 bytes long. Figuring out the order in which the bytes are mixed, having the loadable library with native functions lying nearby in the same git repository, is trivial.
Good and thorough read. I am getting the following when running “python3 poll-insert.py” in terminal:
connecting: A @ C4:7C:8D:6A:3A:22
connected
handshake finished, getting hours
count: 0
done
Any thoughts?
Looks like it just has no data yet. It should return “count: 1” or more and then post data after about hour.