From b48e1245ab98d64e995e99b4f480d3fb8acc4c52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Thu, 19 Nov 2020 15:41:17 +0100 Subject: [PATCH] Add trafri plugin with on/off switch basic structure --- debian/control | 18 +- nymea-plugins.pro | 1 + .../integrationpluginzigbee-tradfri.json | 128 ++++++++++ .../integrationpluginzigbeetradfri.cpp | 226 ++++++++++++++++++ .../integrationpluginzigbeetradfri.h | 73 ++++++ ...343be-9fd6-4015-9ff5-38542651c534-en_US.ts | 1 + zigbee-tradfri/zigbee-tradfri.pro | 12 + 7 files changed, 458 insertions(+), 1 deletion(-) create mode 100644 zigbee-tradfri/integrationpluginzigbee-tradfri.json create mode 100644 zigbee-tradfri/integrationpluginzigbeetradfri.cpp create mode 100644 zigbee-tradfri/integrationpluginzigbeetradfri.h create mode 100644 zigbee-tradfri/translations/6a4343be-9fd6-4015-9ff5-38542651c534-en_US.ts create mode 100644 zigbee-tradfri/zigbee-tradfri.pro diff --git a/debian/control b/debian/control index 770c31f1..4fc83b76 100644 --- a/debian/control +++ b/debian/control @@ -1061,6 +1061,21 @@ Description: nymea.io zigbee plugin for different generic recognizable lights . This package will install the nymea.io plugin for generic recognizable lights +Package: nymea-plugin-zigbee-tradfri +Architecture: any +Depends: ${shlibs:Depends}, + ${misc:Depends}, + libnymea-zigbee1, + nymea-plugins-translations, +Description: nymea.io zigbee plugin for Ikea TRADRFI devices + The nymea daemon is a plugin based IoT (Internet of Things) server. The + server works like a translator for devices, things and services and + allows them to interact. + With the powerful rule engine you are able to connect any device available + in the system and create individual scenes and behaviors for your environment. + . + This package will install the nymea.io plugin for Ikea TRADFRI zigbee devices + Package: nymea-plugins-translations Section: misc @@ -1133,7 +1148,8 @@ Section: libs Architecture: all Depends: nymea-plugin-zigbee-lumi, nymea-plugin-zigbee-generic, - nymea-plugin-zigbee-generic-lights + nymea-plugin-zigbee-generic-lights, + nymea-plugin-zigbee-tradfri Description: Zigbee plugins for nymea IoT server - meta package for all zigbee replated plugins The nymea daemon is a plugin based IoT (Internet of Things) server. The server works like a translator for devices, things and services and diff --git a/nymea-plugins.pro b/nymea-plugins.pro index 13b45998..bb7c08e1 100644 --- a/nymea-plugins.pro +++ b/nymea-plugins.pro @@ -67,6 +67,7 @@ PLUGIN_DIRS = \ zigbee-generic \ zigbee-generic-lights \ zigbee-lumi \ + zigbee-tradfri \ message(============================================) diff --git a/zigbee-tradfri/integrationpluginzigbee-tradfri.json b/zigbee-tradfri/integrationpluginzigbee-tradfri.json new file mode 100644 index 00000000..05d430e1 --- /dev/null +++ b/zigbee-tradfri/integrationpluginzigbee-tradfri.json @@ -0,0 +1,128 @@ +{ + "name": "ZigbeeTradfri", + "displayName": "Zigbee TRÅDFRI", + "id": "6a4343be-9fd6-4015-9ff5-38542651c534", + "vendors": [ + { + "name": "ikeaTradfri", + "displayName": "IKEA TRÅDFRI", + "id": "1a4fdeb3-2eab-47f2-9775-569552988a88", + "thingClasses": [ + { + "name": "onOffSwitch", + "displayName": "TRÅDFRI On/Off switch", + "id": "2007e8ae-4849-47f6-98d6-ea6769c8ebd9", + "setupMethod": "JustAdd", + "createMethods": [ "Auto" ], + "interfaces": [ "longpressmultibutton", "batterylevel", "wirelessconnectable" ], + "paramTypes": [ + { + "id": "8b33f5cb-d1a3-4ae6-837a-d88e77252185", + "name": "ieeeAddress", + "displayName": "IEEE adress", + "type": "QString", + "defaultValue": "00:00:00:00:00:00:00:00" + }, + { + "id": "37fae6ef-bbbe-4eb8-a199-9ed718224b70", + "name": "networkUuid", + "displayName": "Zigbee network UUID", + "type": "QString", + "defaultValue": "" + }, + { + "id": "4dafffae-af3b-40c0-8a61-8a15b9d24f64", + "name": "endpointId", + "displayName": "Endpoint ID", + "type": "uint", + "defaultValue": 1 + } + ], + "stateTypes": [ + { + "id": "b5abd47e-95f1-4e35-94fa-be87c396073a", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "cached": false, + "defaultValue": false + }, + { + "id": "21889797-32c0-4f1b-903d-8af71981882b", + "name": "signalStrength", + "displayName": "Signal strength", + "displayNameEvent": "Signal strength changed", + "defaultValue": 0, + "maxValue": 100, + "minValue": 0, + "type": "uint", + "unit": "Percentage" + }, + { + "id": "41c74445-4d2d-46dc-8a63-c98f1df8e742", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "QString", + "cached": true, + "defaultValue": "" + }, + { + "id": "103fba1d-79a6-4b48-b7a1-f91090f1c699", + "name": "batteryLevel", + "displayName": "Battery", + "displayNameEvent": "Battery changed", + "type": "int", + "unit": "Percentage", + "defaultValue": 0, + "minValue": 0, + "maxValue": 100 + }, + { + "id": "a47e27ce-f443-4c9a-96fc-d59ec4989b49", + "name": "batteryCritical", + "displayName": "Battery critical", + "displayNameEvent": "Battery critical changed", + "type": "bool", + "defaultValue": false + } + ], + "actionTypes": [ + + ], + "eventTypes": [ + { + "id": "20a75127-21ec-43b5-adb0-4f33e03303f7", + "name": "pressed", + "displayName": "Button pressed", + "paramTypes": [ + { + "id": "1b2abb21-f60e-410b-9cfc-57ca991fb5af", + "name": "buttonName", + "displayName": "Button name", + "type": "QString", + "allowedValues": ["ON", "OFF"] + } + ] + }, + { + "id": "496c4d7f-ad90-427b-8363-56add5d64304", + "name": "longPressed", + "displayName": "Button longpressed", + "paramTypes": [ + { + "id": "c6bde301-634e-404f-baed-80c5a50a3d46", + "name": "buttonName", + "displayName": "Button name", + "type": "QString", + "allowedValues": ["ON", "OFF"] + } + ] + } + ] + } + ] + } + ] +} diff --git a/zigbee-tradfri/integrationpluginzigbeetradfri.cpp b/zigbee-tradfri/integrationpluginzigbeetradfri.cpp new file mode 100644 index 00000000..5f9caa63 --- /dev/null +++ b/zigbee-tradfri/integrationpluginzigbeetradfri.cpp @@ -0,0 +1,226 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io + +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "integrationpluginzigbeetradfri.h" +#include "plugininfo.h" +#include "hardware/zigbee/zigbeehardwareresource.h" + + +IntegrationPluginZigbeeTradfri::IntegrationPluginZigbeeTradfri() +{ + m_ieeeAddressParamTypeIds[onOffSwitchThingClassId] = onOffSwitchThingIeeeAddressParamTypeId; + + m_networkUuidParamTypeIds[onOffSwitchThingClassId] = onOffSwitchThingNetworkUuidParamTypeId; + + m_endpointIdParamTypeIds[onOffSwitchThingClassId] = onOffSwitchThingEndpointIdParamTypeId; + + m_connectedStateTypeIds[onOffSwitchThingClassId] = onOffSwitchConnectedStateTypeId; + + m_signalStrengthStateTypeIds[onOffSwitchThingClassId] = onOffSwitchSignalStrengthStateTypeId; + + m_versionStateTypeIds[onOffSwitchThingClassId] = onOffSwitchVersionStateTypeId; +} + +QString IntegrationPluginZigbeeTradfri::name() const +{ + return "Ikea TRADFRI"; +} + +bool IntegrationPluginZigbeeTradfri::handleNode(ZigbeeNode *node, const QUuid &networkUuid) +{ + // Make sure this is from ikea 0x117C + if (node->nodeDescriptor().manufacturerCode != Zigbee::Ikea) + return false; + + bool handled = false; + foreach (ZigbeeNodeEndpoint *endpoint, node->endpoints()) { + if (endpoint->profile() == Zigbee::ZigbeeProfileHomeAutomation && + endpoint->deviceId() == Zigbee::HomeAutomationDeviceNonColourController) { + qCDebug(dcZigbeeTradfri()) << "Handeling TRADFRI on/off switch" << node << endpoint; + ThingDescriptor descriptor(onOffSwitchThingClassId); + QString deviceClassName = supportedThings().findById(onOffSwitchThingClassId).displayName(); + descriptor.setTitle(deviceClassName); + + ParamList params; + params.append(Param(onOffSwitchThingNetworkUuidParamTypeId, networkUuid.toString())); + params.append(Param(onOffSwitchThingIeeeAddressParamTypeId, node->extendedAddress().toString())); + descriptor.setParams(params); + emit autoThingsAppeared({descriptor}); + handled = true; + } + } + + return handled; +} + +void IntegrationPluginZigbeeTradfri::handleRemoveNode(ZigbeeNode *node, const QUuid &networkUuid) +{ + Q_UNUSED(networkUuid) + Thing *thing = m_thingNodes.key(node); + if (thing) { + qCDebug(dcZigbeeTradfri()) << node << "for" << thing << "has left the network."; + emit autoThingDisappeared(thing->id()); + + // Removing it from our map to prevent a loop that would ask the zigbee network to remove this node (see thingRemoved()) + m_thingNodes.remove(thing); + } +} + +void IntegrationPluginZigbeeTradfri::init() +{ + hardwareManager()->zigbeeResource()->registerHandler(this, ZigbeeHardwareResource::HandlerTypeVendor); +} + +void IntegrationPluginZigbeeTradfri::setupThing(ThingSetupInfo *info) +{ + Thing *thing = info->thing(); + QUuid networkUuid = thing->paramValue(m_networkUuidParamTypeIds.value(thing->thingClassId())).toUuid(); + ZigbeeAddress zigbeeAddress = ZigbeeAddress(thing->paramValue(m_ieeeAddressParamTypeIds.value(thing->thingClassId())).toString()); + ZigbeeNode *node = hardwareManager()->zigbeeResource()->claimNode(this, networkUuid, zigbeeAddress); + if (!node) { + qCWarning(dcZigbeeTradfri()) << "Zigbee node for" << info->thing()->name() << "not found."; + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } + m_thingNodes.insert(thing, node); + + ZigbeeNodeEndpoint *endpoint = findEndpoint(thing); + if (!endpoint) { + qCWarning(dcZigbeeTradfri()) << "Could not find endpoint for" << thing; + info->finish(Thing::ThingErrorSetupFailed); + return; + } + + // Update connected state + thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), node->reachable()); + connect(node, &ZigbeeNode::reachableChanged, thing, [thing, this](bool reachable){ + thing->setStateValue(m_connectedStateTypeIds.value(thing->thingClassId()), reachable); + }); + + // Update signal strength + thing->setStateValue(m_signalStrengthStateTypeIds.value(thing->thingClassId()), qRound(node->lqi() * 100.0 / 255.0)); + connect(node, &ZigbeeNode::lqiChanged, thing, [this, thing](quint8 lqi){ + uint signalStrength = qRound(lqi * 100.0 / 255.0); + qCDebug(dcZigbeeTradfri()) << thing << "signal strength changed" << signalStrength << "%"; + thing->setStateValue(m_signalStrengthStateTypeIds.value(thing->thingClassId()), signalStrength); + }); + + // Set the version + thing->setStateValue(m_versionStateTypeIds.value(thing->thingClassId()), endpoint->softwareBuildId()); + + if (thing->thingClassId() == onOffSwitchThingClassId) { + + // Receive on/off commands + ZigbeeClusterOnOff *onOffCluster = endpoint->outputCluster(ZigbeeClusterLibrary::ClusterIdOnOff); + if (!onOffCluster) { + qCWarning(dcZigbeeTradfri()) << "Could not find on/off client cluster on" << thing << endpoint; + } else { + connect(onOffCluster, &ZigbeeClusterOnOff::commandSent, thing, [=](ZigbeeClusterOnOff::Command command){ + qCDebug(dcZigbeeTradfri()) << thing << "button pressed" << command; + if (command == ZigbeeClusterOnOff::CommandOn) { + qCDebug(dcZigbeeTradfri()) << thing << "pressed ON"; + emit emitEvent(Event(onOffSwitchPressedEventTypeId, thing->id(), ParamList() << Param(onOffSwitchLongPressedEventButtonNameParamTypeId, "ON"))); + } else if (command == ZigbeeClusterOnOff::CommandOff) { + qCDebug(dcZigbeeTradfri()) << thing << "pressed OFF"; + emit emitEvent(Event(onOffSwitchPressedEventTypeId, thing->id(), ParamList() << Param(onOffSwitchLongPressedEventButtonNameParamTypeId, "OFF"))); + } + }); + } + + // Receive level cntrol commands + ZigbeeClusterLevelControl *levelCluster = endpoint->outputCluster(ZigbeeClusterLibrary::ClusterIdLevelControl); + if (!levelCluster) { + qCWarning(dcZigbeeTradfri()) << "Could not find level client cluster on" << thing << endpoint; + } else { + connect(levelCluster, &ZigbeeClusterLevelControl::commandSent, thing, [=](ZigbeeClusterLevelControl::Command command, const QByteArray &payload){ + qCDebug(dcZigbeeTradfri()) << thing << "button pressed" << command << payload.toHex(); + switch (command) { + case ZigbeeClusterLevelControl::CommandMoveWithOnOff: + qCDebug(dcZigbeeTradfri()) << thing << "long pressed ON"; + emit emitEvent(Event(onOffSwitchLongPressedEventTypeId, thing->id(), ParamList() << Param(onOffSwitchLongPressedEventButtonNameParamTypeId, "ON"))); + break; + case ZigbeeClusterLevelControl::CommandMove: + qCDebug(dcZigbeeTradfri()) << thing << "long pressed OFF"; + emit emitEvent(Event(onOffSwitchLongPressedEventTypeId, thing->id(), ParamList() << Param(onOffSwitchLongPressedEventButtonNameParamTypeId, "OFF"))); + break; + default: + break; + } + }); + } + + ZigbeeClusterPowerConfiguration *powerCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdPowerConfiguration); + if (!powerCluster) { + qCWarning(dcZigbeeTradfri()) << "Could not find power configuration cluster on" << thing << endpoint; + } else { + // Only set the initial state if the attribute already exists + if (powerCluster->hasAttribute(ZigbeeClusterPowerConfiguration::AttributeBatteryPercentageRemaining)) { + thing->setStateValue(onOffSwitchBatteryLevelStateTypeId, powerCluster->batteryPercentage()); + thing->setStateValue(onOffSwitchBatteryCriticalStateTypeId, (powerCluster->batteryPercentage() < 10.0)); + } + + connect(powerCluster, &ZigbeeClusterPowerConfiguration::batteryPercentageChanged, thing, [=](double percentage){ + qCDebug(dcZigbeeTradfri()) << "Battery percentage changed" << percentage << "%" << thing; + thing->setStateValue(onOffSwitchBatteryLevelStateTypeId, percentage); + thing->setStateValue(onOffSwitchBatteryCriticalStateTypeId, (percentage < 10.0)); + }); + } + } + + info->finish(Thing::ThingErrorNoError); +} + +void IntegrationPluginZigbeeTradfri::executeAction(ThingActionInfo *info) +{ + info->finish(Thing::ThingErrorUnsupportedFeature); +} + +void IntegrationPluginZigbeeTradfri::thingRemoved(Thing *thing) +{ + ZigbeeNode *node = m_thingNodes.take(thing); + if (node) { + QUuid networkUuid = thing->paramValue(m_networkUuidParamTypeIds.value(thing->thingClassId())).toUuid(); + hardwareManager()->zigbeeResource()->removeNodeFromNetwork(networkUuid, node); + } +} + +ZigbeeNodeEndpoint *IntegrationPluginZigbeeTradfri::findEndpoint(Thing *thing) +{ + ZigbeeNode *node = m_thingNodes.value(thing); + if (!node) { + qCWarning(dcZigbeeTradfri()) << "Could not find the node for" << thing; + return nullptr; + } + + quint8 endpointId = thing->paramValue(m_endpointIdParamTypeIds.value(thing->thingClassId())).toUInt(); + return node->getEndpoint(endpointId); +} + diff --git a/zigbee-tradfri/integrationpluginzigbeetradfri.h b/zigbee-tradfri/integrationpluginzigbeetradfri.h new file mode 100644 index 00000000..e40afb1d --- /dev/null +++ b/zigbee-tradfri/integrationpluginzigbeetradfri.h @@ -0,0 +1,73 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2020, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project 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 project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#ifndef INTEGRATIONPLUGINZIGBEETRADFRI_H +#define INTEGRATIONPLUGINZIGBEETRADFRI_H + +#include "integrations/integrationplugin.h" +#include "hardware/zigbee/zigbeehandler.h" +#include "plugintimer.h" + +#include + +class IntegrationPluginZigbeeTradfri: public IntegrationPlugin, public ZigbeeHandler +{ + Q_OBJECT + + Q_PLUGIN_METADATA(IID "io.nymea.IntegrationPlugin" FILE "integrationpluginzigbee-tradfri.json") + Q_INTERFACES(IntegrationPlugin) + +public: + explicit IntegrationPluginZigbeeTradfri(); + + QString name() const override; + bool handleNode(ZigbeeNode *node, const QUuid &networkUuid) override; + void handleRemoveNode(ZigbeeNode *node, const QUuid &networkUuid) override; + + void init() override; + void setupThing(ThingSetupInfo *info) override; + void executeAction(ThingActionInfo *info) override; + void thingRemoved(Thing *thing) override; + +private: + QHash m_thingNodes; + + QHash m_ieeeAddressParamTypeIds; + QHash m_networkUuidParamTypeIds; + QHash m_endpointIdParamTypeIds; + + QHash m_connectedStateTypeIds; + QHash m_signalStrengthStateTypeIds; + QHash m_versionStateTypeIds; + + ZigbeeNodeEndpoint *findEndpoint(Thing *thing); +}; + +#endif // INTEGRATIONPLUGINZIGBEETRADFRI_H diff --git a/zigbee-tradfri/translations/6a4343be-9fd6-4015-9ff5-38542651c534-en_US.ts b/zigbee-tradfri/translations/6a4343be-9fd6-4015-9ff5-38542651c534-en_US.ts new file mode 100644 index 00000000..dcd02922 --- /dev/null +++ b/zigbee-tradfri/translations/6a4343be-9fd6-4015-9ff5-38542651c534-en_US.ts @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/zigbee-tradfri/zigbee-tradfri.pro b/zigbee-tradfri/zigbee-tradfri.pro new file mode 100644 index 00000000..f3b747ac --- /dev/null +++ b/zigbee-tradfri/zigbee-tradfri.pro @@ -0,0 +1,12 @@ +include(../plugins.pri) + +PKGCONFIG += nymea-zigbee + +SOURCES += \ + integrationpluginzigbeetradfri.cpp + +HEADERS += \ + integrationpluginzigbeetradfri.h + + +