Blog

Company news and updates


Hacking on nymea: FlowerCare sensors plugin



Hacking on nymea takes up a lot of my time. So much, 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.

https://upload.wikimedia.org/wikipedia/commons/f/fe/Starr_080731-9572_Monstera_deliciosa.jpg

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 nymea setup.

So first thing I did was to copy the Sensor Tag plugin, it seemed similar enough to what I assumed should work for the FlowerCare 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.

So on to the next steps:

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. Somehere 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::ServiceState &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)
{
    qCDebug(dcFlowerCare()) << "Characteristic read" << QString::number(characteristic.handle(), 16) << value.toHex();
    if (characteristic != m_sensorDataCharacteristic) {
        return;
    }
    QDataStream stream(©, 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. 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 I have perfectly reliable data from this sensor so far and my Monstera Deliciosa is happy again. Whenever the soil moisture falls below 15%, my nymea setup sends me a push notification right to my phone, reminding me to water it.

All in all it took me less than 5 hours to add support for the Xiami FlowerCare plugin to nymea, from the first idea, to selecting the sensor hardware and writing the full plugin code for it.

As usual, you can find the entire plugin code on our github plugins repository.