add flowercare plugin

master
Michael Zanetti 2018-07-02 23:49:45 +02:00
parent 03d0c95df0
commit c5d826bec3
8 changed files with 587 additions and 1 deletions

View File

@ -0,0 +1,197 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* *
* This file is part of nymea. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; If not, see *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
/*
* ServieUUIDs:
* {00001204-0000-1000-8000-00805f9b34fb}
* {00001206-0000-1000-8000-00805f9b34fb}
* {00001800-0000-1000-8000-00805f9b34fb}
* {00001801-0000-1000-8000-00805f9b34fb}
* {0000fe95-0000-1000-8000-00805f9b34fb}
* {0000fef5-0000-1000-8000-00805f9b34fb}
*/
#include "plugininfo.h"
#include "devicemanager.h"
#include "hardware/bluetoothlowenergy/bluetoothlowenergymanager.h"
#include "devicepluginflowercare.h"
#include "flowercare.h"
DevicePluginFlowercare::DevicePluginFlowercare()
{
}
DevicePluginFlowercare::~DevicePluginFlowercare()
{
if (m_reconnectTimer) {
hardwareManager()->pluginTimerManager()->unregisterTimer(m_reconnectTimer);
}
}
void DevicePluginFlowercare::init()
{
}
DeviceManager::DeviceError DevicePluginFlowercare::discoverDevices(const DeviceClassId &deviceClassId, const ParamList &params)
{
Q_UNUSED(params)
Q_UNUSED(deviceClassId)
if (!hardwareManager()->bluetoothLowEnergyManager()->available())
return DeviceManager::DeviceErrorHardwareNotAvailable;
if (!hardwareManager()->bluetoothLowEnergyManager()->enabled())
return DeviceManager::DeviceErrorHardwareNotAvailable;
BluetoothDiscoveryReply *reply = hardwareManager()->bluetoothLowEnergyManager()->discoverDevices();
connect(reply, &BluetoothDiscoveryReply::finished, this, &DevicePluginFlowercare::onBluetoothDiscoveryFinished);
return DeviceManager::DeviceErrorAsync;
}
DeviceManager::DeviceSetupStatus DevicePluginFlowercare::setupDevice(Device *device)
{
qCDebug(dcFlowerCare) << "Setting up Flower care" << device->name() << device->params();
if (device->deviceClassId() == flowerCareDeviceClassId) {
QBluetoothAddress address = QBluetoothAddress(device->paramValue(flowerCareMacParamTypeId).toString());
QString name = device->paramValue(flowerCareNameParamTypeId).toString();
QBluetoothDeviceInfo deviceInfo = QBluetoothDeviceInfo(address, name, 0);
BluetoothLowEnergyDevice *bluetoothDevice = hardwareManager()->bluetoothLowEnergyManager()->registerDevice(deviceInfo, QLowEnergyController::PublicAddress);
FlowerCare *flowerCare = new FlowerCare(bluetoothDevice, this);
connect(flowerCare, &FlowerCare::finished, this, &DevicePluginFlowercare::onSensorDataReceived);
m_list.insert(device, flowerCare);
m_refreshMinutes[flowerCare] = 0;
if (!m_reconnectTimer) {
m_reconnectTimer = hardwareManager()->pluginTimerManager()->registerTimer();
connect(m_reconnectTimer, &PluginTimer::timeout, this, &DevicePluginFlowercare::onPluginTimer);
}
return DeviceManager::DeviceSetupStatusSuccess;
}
return DeviceManager::DeviceSetupStatusFailure;
}
void DevicePluginFlowercare::postSetupDevice(Device *device)
{
FlowerCare *flowerCare = m_list.value(device);
flowerCare->refreshData();
}
void DevicePluginFlowercare::deviceRemoved(Device *device)
{
FlowerCare *flowerCare = m_list.take(device);
if (!flowerCare) {
return;
}
hardwareManager()->bluetoothLowEnergyManager()->unregisterDevice(flowerCare->btDevice());
flowerCare->deleteLater();
if (m_list.isEmpty() && m_reconnectTimer) {
hardwareManager()->pluginTimerManager()->unregisterTimer(m_reconnectTimer);
m_reconnectTimer = nullptr;
}
}
DeviceManager::DeviceError DevicePluginFlowercare::executeAction(Device *device, const Action &action)
{
Q_UNUSED(device)
Q_UNUSED(action)
return DeviceManager::DeviceErrorActionTypeNotFound;
}
bool DevicePluginFlowercare::verifyExistingDevices(const QBluetoothDeviceInfo &deviceInfo)
{
foreach (Device *device, m_list.keys()) {
if (device->paramValue(flowerCareMacParamTypeId).toString() == deviceInfo.address().toString())
return true;
}
return false;
}
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);
}
}
}
void DevicePluginFlowercare::onBluetoothDiscoveryFinished()
{
BluetoothDiscoveryReply *reply = static_cast<BluetoothDiscoveryReply *>(sender());
if (reply->error() != BluetoothDiscoveryReply::BluetoothDiscoveryReplyErrorNoError) {
qCWarning(dcFlowerCare()) << "Bluetooth discovery error:" << reply->error();
reply->deleteLater();
emit devicesDiscovered(flowerCareDeviceClassId, QList<DeviceDescriptor>());
return;
}
QList<DeviceDescriptor> deviceDescriptors;
qCDebug(dcFlowerCare()) << "Discovery finished";
foreach (const QBluetoothDeviceInfo &deviceInfo, reply->discoveredDevices()) {
qCDebug(dcFlowerCare()) << "Discovered device" << deviceInfo.name();
if (deviceInfo.name().contains("Flower care")) {
if (!verifyExistingDevices(deviceInfo)) {
DeviceDescriptor descriptor(flowerCareDeviceClassId, "Flower Care", deviceInfo.address().toString());
ParamList params;
params.append(Param(flowerCareNameParamTypeId, deviceInfo.name()));
params.append(Param(flowerCareMacParamTypeId, deviceInfo.address().toString()));
descriptor.setParams(params);
deviceDescriptors.append(descriptor);
}
}
}
reply->deleteLater();
emit devicesDiscovered(flowerCareDeviceClassId, deviceDescriptors);
}
void DevicePluginFlowercare::onSensorDataReceived(quint8 batteryLevel, double degreeCelsius, double lux, double moisture, double fertility)
{
FlowerCare *flowerCare = static_cast<FlowerCare*>(sender());
Device *device = m_list.key(flowerCare);
device->setStateValue(flowerCareConnectedStateTypeId, true);
device->setStateValue(flowerCareBatteryLevelStateTypeId, batteryLevel);
device->setStateValue(flowerCareBatteryCriticalStateTypeId, batteryLevel <= 10);
device->setStateValue(flowerCareTemperatureStateTypeId, degreeCelsius);
device->setStateValue(flowerCareLightIntensityStateTypeId, lux);
device->setStateValue(flowerCareMoistureStateTypeId, moisture);
device->setStateValue(flowerCareConductivityStateTypeId, fertility);
m_refreshMinutes[flowerCare] = 20;
}

View File

@ -0,0 +1,68 @@
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* *
* Copyright (C) 2018 Michael Zanetti <michael.zanetti@guh.io> *
* *
* This file is part of nymea. *
* *
* This library is free software; you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public *
* License as published by the Free Software Foundation; either *
* version 2.1 of the License, or (at your option) any later version. *
* *
* This library is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
* Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General Public *
* License along with this library; If not, see *
* <http://www.gnu.org/licenses/>. *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#ifndef DEVICEPLUGINFLOWERCARE_H
#define DEVICEPLUGINFLOWERCARE_H
#include <QPointer>
#include <QHash>
#include "plugin/deviceplugin.h"
#include "devicemanager.h"
#include "plugintimer.h"
#include "hardware/bluetoothlowenergy/bluetoothlowenergydevice.h"
class FlowerCare;
class DevicePluginFlowercare : public DevicePlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "io.nymea.DevicePlugin" FILE "devicepluginflowercare.json")
Q_INTERFACES(DevicePlugin)
public:
explicit DevicePluginFlowercare();
~DevicePluginFlowercare();
void init() override;
DeviceManager::DeviceError discoverDevices(const DeviceClassId &deviceClassId, const ParamList &params) override;
DeviceManager::DeviceSetupStatus setupDevice(Device *device) override;
void postSetupDevice(Device *device) override;
void deviceRemoved(Device *device) override;
DeviceManager::DeviceError executeAction(Device *device, const Action &action) override;
private:
PluginTimer *m_reconnectTimer = nullptr;
QHash<Device*, FlowerCare*> m_list;
QHash<FlowerCare*, int> m_refreshMinutes;
bool verifyExistingDevices(const QBluetoothDeviceInfo &deviceInfo);
private slots:
void onPluginTimer();
void onBluetoothDiscoveryFinished();
void onSensorDataReceived(quint8 batteryLevel, double degreeCelsius, double lux, double moisture, double fertility);
};
#endif // DEVICEPLUGINFLOWERCARE_H

View File

@ -0,0 +1,102 @@
{
"displayName": "Flower Care",
"name": "flowerCare",
"id": "74e2106a-3407-4e89-a27a-1c890d78bee7",
"vendors": [
{
"id": "f037aa1a-f764-42f9-a613-338e683e4da5",
"name": "xiaomi",
"displayName": "Xiaomi",
"deviceClasses": [
{
"id": "297e2efa-168c-4eb8-bf3f-f8694dc0a2b9",
"name": "flowerCare",
"displayName": "Flower Care",
"createMethods": ["discovery"],
"interfaces": ["temperaturesensor", "lightsensor", "moisturesensor", "conductivitysensor", "connectable", "batterylevel"],
"paramTypes": [
{
"id": "65f95e3c-59fe-47d6-ba4a-2469e5f554e2",
"name": "name",
"displayName": "Name",
"type": "QString",
"inputType": "TextLine"
},
{
"id": "fe2dba0f-2a47-463e-a645-bcd55cf09750",
"name": "mac",
"displayName": "MAC address",
"type": "QString",
"inputType": "MacAddress"
}
],
"stateTypes": [
{
"id": "6254103a-ff8b-4f62-afda-6b5be1c933ec",
"name": "connected",
"displayName": "Reachable",
"displayNameEvent": "Reachable changed",
"type": "bool",
"cached": false,
"defaultValue": false
},
{
"id": "801a00ab-ce3b-4b10-9236-2f845c469a26",
"name": "batteryLevel",
"displayName": "Battery level",
"displayNameEvent": "Battery level changed",
"type": "int",
"minValue": 0,
"maxValue": 100,
"defaultValue": 0
},
{
"id": "700a83e6-9fbb-4330-b00e-e9d0874b4f66",
"name": "batteryCritical",
"displayName": "Battery level critical",
"displayNameEvent": "Battery level critical changed",
"type": "bool",
"defaultValue": false
},
{
"id": "4577a032-bafd-49ca-8625-6dcf90819e88",
"name": "temperature",
"displayName": "Temperature",
"displayNameEvent": "Temperature changed",
"type": "double",
"unit": "DegreeCelsius",
"defaultValue": 0
},
{
"id": "5e3ac82a-1dda-4399-888e-529b391a814a",
"name": "moisture",
"displayName": "Soil moisture",
"displayNameEvent": "Soil moisture changed",
"type": "double",
"minValue": 0,
"maxValue": 100,
"unit": "Percentage",
"defaultValue": 0
},
{
"id": "7e0a3b3d-72f4-4547-bd1e-2711be61c1a7",
"name": "lightIntensity",
"displayName": "Light intensity",
"displayNameEvent": "Light intensity changed",
"type": "double",
"defaultValue": 0
},
{
"id": "f77608b3-8b5b-43a9-aab6-0dc35c694ec3",
"name": "conductivity",
"displayName": "Conductivity",
"displayNameEvent": "Conductivity changed",
"type": "double",
"defaultValue": 0
}
]
}
]
}
]
}

144
flowercare/flowercare.cpp Normal file
View File

@ -0,0 +1,144 @@
#include "flowercare.h"
#include "extern-plugininfo.h"
#include <QDataStream>
#include <QVersionNumber>
FlowerCare::FlowerCare(BluetoothLowEnergyDevice *device, QObject *parent):
QObject(parent),
m_bluetoothDevice(device)
{
connect(m_bluetoothDevice, &BluetoothLowEnergyDevice::connectedChanged, this, &FlowerCare::onConnectedChanged);
connect(m_bluetoothDevice, &BluetoothLowEnergyDevice::servicesDiscoveryFinished, this, &FlowerCare::onServiceDiscoveryFinished);
}
void FlowerCare::refreshData()
{
qCDebug(dcFlowerCare()) << "Connecting to device";
m_bluetoothDevice->connectDevice();
}
BluetoothLowEnergyDevice *FlowerCare::btDevice() const
{
return m_bluetoothDevice;
}
void FlowerCare::onConnectedChanged(bool connected)
{
qCDebug(dcFlowerCare()) << "Connection changed:" << connected;
if (!connected) {
m_sensorService->deleteLater();
m_sensorService = nullptr;
}
}
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);
connect(m_sensorService, &QLowEnergyService::characteristicChanged, this, &FlowerCare::onSensorServiceCharacteristicChanged);
m_sensorService->discoverDetails();
}
void FlowerCare::onSensorServiceStateChanged(const QLowEnergyService::ServiceState &state)
{
if (state != QLowEnergyService::ServiceDiscovered) {
return;
}
// printServiceDetails(m_sensorService);
QLowEnergyCharacteristic batteryFirmwareCharacteristic = m_sensorService->characteristic(batteryFirmwareCharacteristicUuid);
if (!batteryFirmwareCharacteristic.isValid()) {
qCWarning(dcFlowerCare()) << "Invalid battery/firmware characteristic.";
emit failed();
return;
}
QByteArray value = batteryFirmwareCharacteristic.value();
QDataStream stream(&value, QIODevice::ReadOnly);
stream.setByteOrder(QDataStream::LittleEndian);
stream >> m_batteryLevel;
QString firmwareVersionString = value.right(5);
qCDebug(dcFlowerCare()) << "Battery level:" << m_batteryLevel;
QVersionNumber firmwareVersion = QVersionNumber::fromString(firmwareVersionString);
qCDebug(dcFlowerCare()) << "Firmware version:" << firmwareVersion;
if (firmwareVersion >= QVersionNumber::fromString("2.6.6")) {
QLowEnergyCharacteristic sensorControlCharacteristic = m_sensorService->characteristic(sensorControlCharacteristicUuid);
m_sensorService->writeCharacteristic(sensorControlCharacteristic, QByteArray::fromHex("A01F"));
qCDebug(dcFlowerCare()) << "Wrote to handle 0x0033: A01F";
}
m_sensorDataCharacteristic = m_sensorService->characteristic(sensorDataCharacteristicUuid);
if (!m_sensorDataCharacteristic.isValid()) {
qCWarning(dcFlowerCare()) << "Invalid sensor data characteristic.";
}
// Enable notifications
QLowEnergyDescriptor notificationDescriptor = m_sensorDataCharacteristic.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);
m_sensorService->writeDescriptor(notificationDescriptor, QByteArray::fromHex("0100"));
// Read the data manually
// Sometimes if we read the light intensty right now we might get wrong values
// because the LED flashes upon connect. Let's not read it manually but instead wait for
// the values to come in with the notification
// m_sensorService->readCharacteristic(m_sensorDataCharacteristic);
}
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;
}
processSensorData(value);
}
void FlowerCare::onSensorServiceCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &value)
{
qCDebug(dcFlowerCare()) << "Notification received" << QString::number(characteristic.handle(), 16) << value.toHex();
if (characteristic != m_sensorDataCharacteristic) {
return;
}
processSensorData(value);
}
void FlowerCare::printServiceDetails(QLowEnergyService *service) const
{
foreach (const QLowEnergyCharacteristic &characteristic, service->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();
}
}
}
void FlowerCare::processSensorData(const QByteArray &data)
{
QByteArray copy = data;
QDataStream stream(&copy, 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;
qCDebug(dcFlowerCare()) << "Temperature:" << temp << "Lux:" << lux << "moisture:" << moisture << "fertility" << fertility;
m_bluetoothDevice->disconnectDevice();
emit finished(m_batteryLevel, 1.0 * temp / 10, lux, moisture, fertility);
}

58
flowercare/flowercare.h Normal file
View File

@ -0,0 +1,58 @@
#ifndef FLOWERCARE_H
#define FLOWERCARE_H
#include <QObject>
#include "hardware/bluetoothlowenergy/bluetoothlowenergydevice.h"
static QBluetoothUuid sensorServiceUuid = QBluetoothUuid(QUuid("00001204-0000-1000-8000-00805f9b34fb"));
// Contains Battery level and firmware version
static QBluetoothUuid batteryFirmwareCharacteristicUuid = QBluetoothUuid(QUuid("00001a02-0000-1000-8000-00805f9b34fb"));
// Need to write 0xA01F to this
static QBluetoothUuid sensorControlCharacteristicUuid = QBluetoothUuid(QUuid("00001a00-0000-1000-8000-00805f9b34fb"));
// contains sensor data
static QBluetoothUuid sensorDataCharacteristicUuid = QBluetoothUuid(QUuid("00001a01-0000-1000-8000-00805f9b34fb"));
class FlowerCare : public QObject
{
Q_OBJECT
public:
explicit FlowerCare(BluetoothLowEnergyDevice* device, QObject *parent = nullptr);
void refreshData();
BluetoothLowEnergyDevice* btDevice() const;
signals:
void finished(quint8 batteryLevel, double degreeCelsius, double lux, double moisture, double fertility);
void failed();
private slots:
void onConnectedChanged(bool connected);
void onServiceDiscoveryFinished();
void onSensorServiceStateChanged(const QLowEnergyService::ServiceState &state);
void onSensorServiceCharacteristicRead(const QLowEnergyCharacteristic &characteristic, const QByteArray &value);
void onSensorServiceCharacteristicChanged(const QLowEnergyCharacteristic &characteristic, const QByteArray &value);
private:
void printServiceDetails(QLowEnergyService* service) const;
void processSensorData(const QByteArray &data);
BluetoothLowEnergyDevice *m_bluetoothDevice;
// Services
QLowEnergyService *m_sensorService = nullptr;
QLowEnergyCharacteristic m_sensorDataCharacteristic;
// cache
quint8 m_batteryLevel = 0;
};
#endif // FLOWERCARE_H

13
flowercare/flowercare.pro Normal file
View File

@ -0,0 +1,13 @@
include(../plugins.pri)
QT += bluetooth
TARGET = $$qtLibraryTarget(nymea_devicepluginflowercare)
SOURCES += \
devicepluginflowercare.cpp \
flowercare.cpp
HEADERS += \
devicepluginflowercare.h \
flowercare.h

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1">
</TS>

View File

@ -37,7 +37,7 @@ PLUGIN_DIRS = \
keba \
remotessh \
dweetio \
flowercare \
CONFIG+=all