Introduction: FlowerCare and Nymea to Rescue My Plants

About: We make IoT open source. Connected products have a huge impact in our daily life: In a few months IoT generates much more data than social media. Furthermore, there are millions of products out there, which ha…

Getting hands dirty on hooking up plant care sensors to my existing open source smart home. A walkthrough on plugin development for nymea.

The story

As many other tinkerers and hackers, I'm suffering too from the issue that hacking on things takes up so much of my time that I occasionally forget to water my plants. After my Monstera Deliciosa once again suffered from dry soil, I decided to see whether I can do something about it to remind me when it's thirsty.

A quick research on the web brought my attention to the Xiaomi FlowerCare, also known as MiCare or PlantCare. It is a Bluetooth Low Energy device and some basic research revealed that its protocol seem to be quite easy to understand. While Xiaomi doesn't seem to provide any public specs, there has been quite a bit of reverse engineering on the internet for this device yet. So I decided to order one of those.

A few days later it got delivered and of course I started to play around with it right away. I briefly checked out the app that comes with it but as you probably can guess, using it in its default setup was never my plan. Of course this needs to be integrated with my existing smart home setup.
As also described here I'm using nymea as my smart home solution (Yes, you can even spot the Monstera in one of the pictures there :)). Sadly, nymea didn't support that sensor yet so firing up some IDE was in order.

Step 1: Getting a Plugin Stub Loaded

So first thing I did was to copy the existing Texas Instruments Sensor Tag plugin, it seemed similar enough to what I assumed should work for the FlowerCare device too. After the basic renaming of things in the plugininfo.json and commenting away most of the sensortag plugin's code I was ready to load the new plugin stub.

As expected, the discovery would already show the sensor right away and allow me to add it to the system. Of course it wouldn't produce any meaningful data at this point.

Step 2: Finding Data on the Sensor

As with any Bluetooth LE device, the first thing you want to do is to find out about the services it offers and their characteristics. Somewhere in there the actual data is hidden. With a quick debug print looping over all the discovered services and printing their characteristics I was at the point where I could compare the information I found on the internet with what the device actually reports.

void FlowerCare::onServiceDiscoveryFinished(){
BluetoothLowEnergyDevice *btDev = static_cast<BluetoothLowEnergyDevice*>(sender()); qCDebug(dcFlowerCare()) << "have service uuids" << btDev->serviceUuids(); m_sensorService = btDev->controller()->createServiceObject(sensorServiceUuid, this); connect(m_sensorService, &QLowEnergyService::stateChanged, this, &FlowerCare::onSensorServiceStateChanged); connect(m_sensorService, &QLowEnergyService::characteristicRead, this, &FlowerCare::onSensorServiceCharacteristicRead); m_sensorService->discoverDetails();}void FlowerCare::onSensorServiceStateChanged(const QLowEnergyService::ServiceSate &state){ if (state != QLowEnergyService::ServiceDiscovered) { return; } foreach (const QLowEnergyCharacteristic &characteristic, m_sensorService->characteristics()) { qCDebug(dcFlowerCare()).nospace() << "C: --> " << characteristic.uuid().toString() << " (" << characteristic.handle() << " Name: " << characteristic.name() << "): " << characteristic.value() << ", " << characteristic.value().toHex(); foreach (const QLowEnergyDescriptor &descriptor, characteristic.descriptors()) { qCDebug(dcFlowerCare()).nospace() << "D: --> " << descriptor.uuid().toString() << " (" << descriptor.handle() << " Name: " << descriptor.name() << "): " << descriptor.value() << ", " << descriptor.value().toHex(); } }}

The firmware version and the battery level were easy. I could already see the according values printed in this very first attempt of listing the data. The actual sensor values are hidden a little bit deeper in there, but combining it with the data from the internet immediately pointed out where to find it and especially how to read it.

void FlowerCare::onSensorServiceCharacteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value){<br>    qCDebug(dcFlowerCare()) << "Characteristic read" << QString::number(characteristic.handle(), 16) << value.toHex();    if (characteristic != m_sensorDataCharacteristic) {        return;    }    QDataStream stream(value, QIODevice::ReadOnly);    stream.setByteOrder(QDataStream::LittleEndian);    quint16 temp;    stream >> temp;    qint8 skip;    stream >> skip;    quint32 lux;    stream >> lux;    qint8 moisture;    stream >> moisture;    qint16 fertility;    stream >> fertility;    emit finished(m_batteryLevel, 1.0 * temp / 10, lux, moisture, fertility);}

Putting this together, the plugin already started to produce meaningful data.

Step 3: Finishing Touches

So it basically worked now, however, one issue was still left there. The FlowerCare sensor would, in contrary to the Texas Instruments SensorTag, drop the Bluetooth connection after a few seconds. Considering the use case though, this doesn't seem to be an issue as it is quite reliable in responding to connection attempts. Given that normally a plant doesn't suck up a litre of water within minutes, but rather days, it doesn't seem necessariy to stay connected all the time. Also this would drain the battery quite a lot. So I decided to add a PluginTimer which would reconnect the sensor every 20 minutes and fetch data from it. If, for some reason, the sensor would not respond to the connection attempt, the code will start another timer which tries to reconnect every minute from that point on until it manages to get the data. Then it would go back to fetch data on the 20 minutes interval again. If the device fails to connect twice in a row (meaning, after 20 + 1 minutes), it would be marked offline in the system and the user can be alerted about it.

void DevicePluginFlowercare::onPluginTimer()
{ foreach (FlowerCare *flowerCare, m_list) { if (--m_refreshMinutes[flowerCare] <= 0) { qCDebug(dcFlowerCare()) << "Refreshing" << flowerCare->btDevice()->address(); flowerCare->refreshData(); } else { qCDebug(dcFlowerCare()) << "Not refreshing" << flowerCare->btDevice()->address() << " Next refresh in" << m_refreshMinutes[flowerCare] << "minutes"; } // If we had 2 or more failed connection attempts, mark it as disconnected if (m_refreshMinutes[flowerCare] < -2) { qCDebug(dcFlowerCare()) << "Failed to refresh for"<< (m_refreshMinutes[flowerCare] * -1) << "minutes. Marking as unreachable"; m_list.key(flowerCare)->setStateValue(flowerCareConnectedStateTypeId, false); } }}

With this strategy nymea now seemed to deliver perfectly reliable data from this sensor.

Step 4: Using It in the Bigger Context

Just getting values from the sensor isn't that much useful though, I could also have used the original app for that. Now let's do some smart things with it.

Nymea supports sending push notifications, either to phones with nymea:app installed, or via PushBullet. So the obvious thing to do is to send myself some push notifications whenever the soil moisture falls below 15%. It's rather easy to set that up in the app. As prerequisite you either need an account in nymea:cloud or on PushBullet. For nymea:cloud based push notifications it is enough to enable nymea:cloud on the nymea:core and in nymea:app. As soon as both are connected, a notification thing will automatically appear. For PushBullet add a new thing in the system, you'll find PushBullet in the list there. It will ask you for the API key you get when signing up with PushBullet. Once you have a push notification thing in nymea, you can create a rule.

Of course you can do whatever else you want... Can also turn on some light in order to reflect sensor values, or use the HTTP commander plugin to post sensor values to a server on the internet for example.
I don't have a water valve which can be controlled digitally (yet) but of course, if you have such a thing and it's not supported by nymea yet, adding a plugin for that would be rather similar than this.

Step 5: Closing Words

The flowercare plugin has been accepted upstream by now and if you have one of those it's ready to be used with nymea now. However, I hope this article might be of interest if someone wants to add support for other devices. It should be a walkthrough on how to build your own plugin for nymea.

If you want to just build this setup at your home, all you need is the FlowerCare sensor, a Raspberry Pi, the nymea community image (it includes the flower care plugin by now), and nymea:app which is available in app stores. Also, so far my Monstera Deliciosa is happy again and as you might have seen in the screenshots, I've gotten myself a second one of those sensors to track health of my lemon tree too. For that one I'm sending myself push notification whenever it's freezing outside so I can bring it through the winter safely.