From b4eca2a94f4a1578330f00b1e60b15b87f3fb199 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Wed, 14 Sep 2022 23:46:02 +0200 Subject: [PATCH] Add API to interact with ZigBee bindings --- libnymea-core/jsonrpc/zigbeehandler.cpp | 245 ++++++++++++++++++++++++ libnymea-core/jsonrpc/zigbeehandler.h | 11 ++ libnymea-core/zigbee/zigbeemanager.cpp | 10 +- libnymea-core/zigbee/zigbeemanager.h | 4 + tests/auto/api.json | 79 +++++++- 5 files changed, 347 insertions(+), 2 deletions(-) diff --git a/libnymea-core/jsonrpc/zigbeehandler.cpp b/libnymea-core/jsonrpc/zigbeehandler.cpp index 1cd1add7..5c462f4b 100644 --- a/libnymea-core/jsonrpc/zigbeehandler.cpp +++ b/libnymea-core/jsonrpc/zigbeehandler.cpp @@ -49,6 +49,7 @@ ZigbeeHandler::ZigbeeHandler(ZigbeeManager *zigbeeManager, QObject *parent) : registerObject(); registerEnum(); registerEnum(); + registerEnum(); // Network object describing a network instance QVariantMap zigbeeNetworkDescription; @@ -68,6 +69,15 @@ ZigbeeHandler::ZigbeeHandler(ZigbeeManager *zigbeeManager, QObject *parent) : zigbeeNetworkDescription.insert("networkState", enumRef()); registerObject("ZigbeeNetwork", zigbeeNetworkDescription); + QVariantMap zigbeeBindingTableRecordDescription; + zigbeeBindingTableRecordDescription.insert("sourceAddress", enumValueName(String)); + zigbeeBindingTableRecordDescription.insert("sourceEndpointId", enumValueName(Uint)); + zigbeeBindingTableRecordDescription.insert("clusterId", enumValueName(Uint)); + zigbeeBindingTableRecordDescription.insert("o:destinationGroupAddress", enumValueName(Uint)); + zigbeeBindingTableRecordDescription.insert("o:destinationAddress", enumValueName(String)); + zigbeeBindingTableRecordDescription.insert("o:destinationEndpointId", enumValueName(Uint)); + registerObject("ZigbeeBindingTableRecord", zigbeeBindingTableRecordDescription); + QVariantMap zigbeeNeighborTableRecordDescription; zigbeeNeighborTableRecordDescription.insert("networkAddress", enumValueName(Uint)); zigbeeNeighborTableRecordDescription.insert("relationship", enumRef()); @@ -84,6 +94,19 @@ ZigbeeHandler::ZigbeeHandler(ZigbeeManager *zigbeeManager, QObject *parent) : zigbeeRoutingTableRecordDescription.insert("memoryConstrained", enumValueName(Bool)); registerObject("ZigbeeRoutingTableRecord", zigbeeRoutingTableRecordDescription); + + QVariantMap zigbeeClusterDescription; + zigbeeClusterDescription.insert("clusterId", enumValueName(Uint)); + zigbeeClusterDescription.insert("direction", enumRef()); + registerObject("ZigbeeCluster", zigbeeClusterDescription); + + QVariantMap zigbeeNodeEndpointDescription; + zigbeeNodeEndpointDescription.insert("endpointId", enumValueName(Uint)); + zigbeeNodeEndpointDescription.insert("inputClusters", QVariantList() << objectRef("ZigbeeCluster")); + zigbeeNodeEndpointDescription.insert("outputClusters", QVariantList() << objectRef("ZigbeeCluster")); + registerObject("ZigbeeNodeEndpoint", zigbeeNodeEndpointDescription); + + // Zigbee node description QVariantMap zigbeeNodeDescription; zigbeeNodeDescription.insert("networkUuid", enumValueName(Uuid)); @@ -98,8 +121,10 @@ ZigbeeHandler::ZigbeeHandler(ZigbeeManager *zigbeeManager, QObject *parent) : zigbeeNodeDescription.insert("reachable", enumValueName(Bool)); zigbeeNodeDescription.insert("lqi", enumValueName(Uint)); zigbeeNodeDescription.insert("lastSeen", enumValueName(Uint)); + zigbeeNodeDescription.insert("endpoints", QVariantList() << objectRef("ZigbeeNodeEndpoint")); zigbeeNodeDescription.insert("neighborTableRecords", QVariantList() << objectRef("ZigbeeNeighborTableRecord")); zigbeeNodeDescription.insert("routingTableRecords", QVariantList() << objectRef("ZigbeeRoutingTableRecord")); + zigbeeNodeDescription.insert("bindingTableRecords", QVariantList() << objectRef("ZigbeeBindingTableRecord")); registerObject("ZigbeeNode", zigbeeNodeDescription); QVariantMap params, returns; @@ -216,6 +241,41 @@ ZigbeeHandler::ZigbeeHandler(ZigbeeManager *zigbeeManager, QObject *parent) : returns.insert("o:zigbeeNodes", QVariantList() << objectRef("ZigbeeNode")); registerMethod("GetNodes", description, params, returns); + // RefreshBindings + params.clear(); returns.clear(); + description = "Refresh the binding table for the given node."; + params.insert("networkUuid", enumValueName(Uuid)); + params.insert("ieeeAddress", enumValueName(String)); + returns.insert("zigbeeError", enumRef()); + registerMethod("RefreshBindings", description, params, returns); + + // CreateBinding + params.clear(); returns.clear(); + description = "Create a binding. Use destinationAddress and destinationEndpointId to create a node to node binding, or use destinationGroupAddress to create a group binding."; + params.insert("networkUuid", enumValueName(Uuid)); + params.insert("sourceAddress", enumValueName(String)); + params.insert("sourceEndpointId", enumValueName(Uint)); + params.insert("clusterId", enumValueName(Uint)); + params.insert("o:destinationAddress", enumValueName(String)); + params.insert("o:destinationEndpointId", enumValueName(Uint)); + params.insert("o:destinationGroupAddress", enumValueName(Uint)); + returns.insert("zigbeeError", enumRef()); + registerMethod("CreateBinding", description, params, returns); + + + // RemoveBinding + params.clear(); returns.clear(); + description = "Remove a binding."; + params.insert("networkUuid", enumValueName(Uuid)); + params.insert("sourceAddress", enumValueName(String)); + params.insert("sourceEndpointId", enumValueName(Uint)); + params.insert("clusterId", enumValueName(Uint)); + params.insert("o:destinationAddress", enumValueName(String)); + params.insert("o:destinationEndpointId", enumValueName(Uint)); + params.insert("o:destinationGroupAddress", enumValueName(Uint)); + returns.insert("zigbeeError", enumRef()); + registerMethod("RemoveBinding", description, params, returns); + // RemoveNode params.clear(); returns.clear(); description = "Remove a ZigBee node with the given \'ieeeAddress\' from the network with the given \'networkUuid\'. " @@ -429,6 +489,153 @@ JsonReply *ZigbeeHandler::RefreshNeighborTables(const QVariantMap ¶ms) return createReply({{"zigbeeError", enumValueName(error)}}); } +JsonReply *ZigbeeHandler::RefreshBindings(const QVariantMap ¶ms) +{ + QUuid networkUuid = params.value("networkUuid").toUuid(); + QString ieeeAddress = params.value("ieeeAddress").toString(); + ZigbeeNetwork *network = m_zigbeeManager->zigbeeNetworks().value(networkUuid); + if (!network) { + return createReply({{"zigbeeError", enumValueName(ZigbeeManager::ZigbeeErrorNetworkUuidNotFound)}}); + } + ZigbeeNode *node = m_zigbeeManager->zigbeeNetworks().value(networkUuid)->getZigbeeNode(ZigbeeAddress(ieeeAddress)); + if (!node) { + return createReply({{"zigbeeError", enumValueName(ZigbeeManager::ZigbeeErrorNodeNotFound)}}); + } + JsonReply *jsonReply = createAsyncReply("RefreshBindings"); + ZigbeeReply *reply = node->readBindingTableEntries(); + connect(reply, &ZigbeeReply::finished, jsonReply, [reply, jsonReply](){ + jsonReply->setData({{"zigbeeError", enumValueName(reply->error())}}); + jsonReply->finished(); + }); + return jsonReply; +} + +JsonReply *ZigbeeHandler::CreateBinding(const QVariantMap ¶ms) +{ + QUuid networkUuid = params.value("networkUuid").toUuid(); + ZigbeeNetwork *network = m_zigbeeManager->zigbeeNetworks().value(networkUuid); + if (!network) { + return createReply({{"zigbeeError", enumValueName(ZigbeeManager::ZigbeeErrorNetworkUuidNotFound)}}); + } + QString sourceAddress = params.value("sourceAddress").toString(); + ZigbeeNode *node = network->getZigbeeNode(ZigbeeAddress(sourceAddress)); + if (!node) { + qCWarning(dcJsonRpc()) << "No Zigbee node for sourceAddress" << sourceAddress; + return createReply({{"zigbeeError", enumValueName(ZigbeeManager::ZigbeeErrorNodeNotFound)}}); + } + + quint8 sourceEndpointId = params.value("sourceEndpointId").toUInt(); + quint16 clusterId = params.value("clusterId").toUInt(); + if (params.contains("destinationAddress") && params.contains("destinationEndpointId")) { + QString destinationAddress = params.value("destinationAddress").toString(); + quint8 destinationEndpointId = params.value("destinationEndpointId").toUInt(); + ZigbeeReply *reply = node->addBinding(sourceEndpointId, clusterId, ZigbeeAddress(destinationAddress), destinationEndpointId); + JsonReply *jsonReply = createAsyncReply("CreateBinding"); + connect(reply, &ZigbeeReply::finished, jsonReply, [reply, jsonReply](){ + ZigbeeManager::ZigbeeError error = ZigbeeManager::ZigbeeErrorNoError; + switch (reply->error()) { + case ZigbeeReply::ErrorNoError: + break; + case ZigbeeReply::ErrorTimeout: + error = ZigbeeManager::ZigbeeErrorTimeoutError; + break; + default: + error = ZigbeeManager::ZigbeeErrorNetworkError; + break; + } + jsonReply->setData({{"zigbeeError", enumValueName(static_cast(error))}}); + emit jsonReply->finished(); + }); + return jsonReply; + + } else if (params.contains("destinationGroupAddress")) { + quint16 destinationGroupAddress = params.value("destinationGroupAddress").toUInt(); + ZigbeeReply *reply = node->addBinding(sourceEndpointId, clusterId, destinationGroupAddress); + JsonReply *jsonReply = createAsyncReply("CreateBinding"); + connect(reply, &ZigbeeReply::finished, jsonReply, [reply, jsonReply](){ + ZigbeeManager::ZigbeeError error = ZigbeeManager::ZigbeeErrorNoError; + switch (reply->error()) { + case ZigbeeReply::ErrorNoError: + break; + case ZigbeeReply::ErrorTimeout: + error = ZigbeeManager::ZigbeeErrorTimeoutError; + break; + default: + error = ZigbeeManager::ZigbeeErrorNetworkError; + break; + } + jsonReply->setData({{"zigbeeError", enumValueName(static_cast(error))}}); + emit jsonReply->finished(); + }); + return jsonReply; + } + return createReply({{"zigbeeError", enumValueName(ZigbeeManager::ZigbeeErrorNodeNotFound)}}); +} + +JsonReply *ZigbeeHandler::RemoveBinding(const QVariantMap ¶ms) +{ + QUuid networkUuid = params.value("networkUuid").toUuid(); + ZigbeeNetwork *network = m_zigbeeManager->zigbeeNetworks().value(networkUuid); + if (!network) { + return createReply({{"zigbeeError", enumValueName(ZigbeeManager::ZigbeeErrorNetworkUuidNotFound)}}); + } + ZigbeeAddress sourceAddress = ZigbeeAddress(params.value("sourceAddress").toString()); + ZigbeeNode *node = network->getZigbeeNode(sourceAddress); + if (!node) { + qCWarning(dcJsonRpc()) << "No Zigbee node for sourceAddress" << sourceAddress; + return createReply({{"zigbeeError", enumValueName(ZigbeeManager::ZigbeeErrorNodeNotFound)}}); + } + + quint8 sourceEndpointId = params.value("sourceEndpointId").toUInt(); + quint16 clusterId = params.value("clusterId").toUInt(); + quint16 destinationGroupAddress = params.value("destinationGroupAddress").toUInt(); + ZigbeeAddress destinationAddress = ZigbeeAddress(params.value("destinationAddress").toString()); + quint8 destinationEndpointId = params.value("destinationEndpointId").toUInt(); + bool isGroup = params.contains("destinationGroupAddress"); + + foreach (const ZigbeeDeviceProfile::BindingTableListRecord &binding, node->bindingTableRecords()) { + bool found = false; + if (isGroup) { + if (binding.sourceAddress == sourceAddress + && binding.sourceEndpoint == sourceEndpointId + && binding.clusterId == clusterId + && binding.destinationShortAddress == destinationGroupAddress) { + found = true; + } + } else { + if (binding.sourceAddress == sourceAddress + && binding.sourceEndpoint == sourceEndpointId + && binding.clusterId == clusterId + && binding.destinationIeeeAddress == destinationAddress + && binding.destinationEndpoint == destinationEndpointId) { + found = true; + } + } + + if (found) { + ZigbeeReply *reply = node->removeBinding(binding); + JsonReply *jsonReply = createAsyncReply("RemoveBinding"); + connect(reply, &ZigbeeReply::finished, jsonReply, [reply, jsonReply](){ + ZigbeeManager::ZigbeeError error = ZigbeeManager::ZigbeeErrorNoError; + switch (reply->error()) { + case ZigbeeReply::ErrorNoError: + break; + case ZigbeeReply::ErrorTimeout: + error = ZigbeeManager::ZigbeeErrorTimeoutError; + break; + default: + error = ZigbeeManager::ZigbeeErrorNetworkError; + break; + } + jsonReply->setData({{"zigbeeError", enumValueName(static_cast(error))}}); + emit jsonReply->finished(); + }); + return jsonReply; + } + } + return createReply({{"zigbeeError", enumValueName(ZigbeeManager::ZigbeeErrorNodeNotFound)}}); +} + JsonReply *ZigbeeHandler::GetNetworks(const QVariantMap ¶ms) { Q_UNUSED(params) @@ -551,6 +758,44 @@ QVariantMap ZigbeeHandler::packNode(ZigbeeNode *node) routingTableRecords.append(recordMap); } nodeMap.insert("routingTableRecords", routingTableRecords); + QVariantList bindingTableRecords; + foreach (const ZigbeeDeviceProfile::BindingTableListRecord &record, node->bindingTableRecords()) { + QVariantMap recordMap; + recordMap.insert("sourceAddress", record.sourceAddress.toString()); + recordMap.insert("sourceEndpointId", record.sourceEndpoint); + recordMap.insert("clusterId", record.clusterId); + if (record.destinationAddressMode == Zigbee::DestinationAddressModeGroup) { + recordMap.insert("destinationGroupAddress", record.destinationShortAddress); + } else if (record.destinationAddressMode == Zigbee::DestinationAddressModeIeeeAddress) { + recordMap.insert("destinationAddress", record.destinationIeeeAddress.toString()); + recordMap.insert("destinationEndpointId", record.destinationEndpoint); + } + bindingTableRecords.append(recordMap); + } + nodeMap.insert("bindingTableRecords", bindingTableRecords); + QVariantList endpoints; + foreach (ZigbeeNodeEndpoint *endpoint, node->endpoints()) { + QVariantMap endpointMap; + endpointMap.insert("endpointId", endpoint->endpointId()); + QVariantList inputClusters; + foreach (ZigbeeCluster *cluster, endpoint->inputClusters()) { + QVariantMap clusterMap; + clusterMap.insert("clusterId", cluster->clusterId()); + clusterMap.insert("direction", enumValueName(static_cast(cluster->direction()))); + inputClusters.append(clusterMap); + } + endpointMap.insert("inputClusters", inputClusters); + QVariantList outputClusters; + foreach (ZigbeeCluster *cluster, endpoint->outputClusters()) { + QVariantMap clusterMap; + clusterMap.insert("clusterId", cluster->clusterId()); + clusterMap.insert("direction", enumValueName(static_cast(cluster->direction()))); + outputClusters.append(clusterMap); + } + endpointMap.insert("outputClusters", outputClusters); + endpoints.append(endpointMap); + } + nodeMap.insert("endpoints", endpoints); return nodeMap; } diff --git a/libnymea-core/jsonrpc/zigbeehandler.h b/libnymea-core/jsonrpc/zigbeehandler.h index 9fca2f31..02d37b32 100644 --- a/libnymea-core/jsonrpc/zigbeehandler.h +++ b/libnymea-core/jsonrpc/zigbeehandler.h @@ -63,6 +63,13 @@ public: }; Q_ENUM(ZigbeeNodeRouteStatus) + enum ZigbeeClusterDirection { + ZigbeeClusterDirectionServer, + ZigbeeClusterDirectionClient + }; + Q_ENUM(ZigbeeClusterDirection) + + explicit ZigbeeHandler(ZigbeeManager *zigbeeManager, QObject *parent = nullptr); QString name() const override; @@ -81,6 +88,10 @@ public: Q_INVOKABLE JsonReply *RefreshNeighborTables(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *RefreshBindings(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *CreateBinding(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *RemoveBinding(const QVariantMap ¶ms); + QVariantMap packNetwork(ZigbeeNetwork *network); QVariantMap packNode(ZigbeeNode *node); diff --git a/libnymea-core/zigbee/zigbeemanager.cpp b/libnymea-core/zigbee/zigbeemanager.cpp index a2aff02b..a203b2ba 100644 --- a/libnymea-core/zigbee/zigbeemanager.cpp +++ b/libnymea-core/zigbee/zigbeemanager.cpp @@ -254,7 +254,7 @@ ZigbeeManager::ZigbeeError ZigbeeManager::refreshNeighborTables(const QUuid &net qCWarning(dcZigbee()) << "No network with uuid" << networkUuid.toString(); return ZigbeeManager::ZigbeeErrorNetworkUuidNotFound; } - m_zigbeeNetworks.value(networkUuid)->refreshNeighborTable(); + m_zigbeeNetworks.value(networkUuid)->refreshNeighborTables(); return ZigbeeManager::ZigbeeErrorNoError; } @@ -704,6 +704,14 @@ void ZigbeeManager::setupNodeSignals(ZigbeeNode *node) connect(node, &ZigbeeNode::neighborTableRecordsChanged, this, [=](){ emit nodeChanged(node->networkUuid(), node); }); + + connect(node, &ZigbeeNode::routingTableRecordsChanged, this, [=](){ + emit nodeChanged(node->networkUuid(), node); + }); + + connect(node, &ZigbeeNode::bindingTableRecordsChanged, this, [=](){ + emit nodeChanged(node->networkUuid(), node); + }); } diff --git a/libnymea-core/zigbee/zigbeemanager.h b/libnymea-core/zigbee/zigbeemanager.h index 7f1ec426..fbd69674 100644 --- a/libnymea-core/zigbee/zigbeemanager.h +++ b/libnymea-core/zigbee/zigbeemanager.h @@ -64,6 +64,9 @@ public: ZigbeeErrorNodeNotFound, ZigbeeErrorForbidden, ZigbeeErrorInvalidChannel, + ZigbeeErrorNetworkError, + ZigbeeErrorTimeoutError, + ZigbeeErrorNotSupported }; Q_ENUM(ZigbeeError) @@ -96,6 +99,7 @@ public: ZigbeeError setZigbeeNetworkPermitJoin(const QUuid &networkUuid, quint16 shortAddress = Zigbee::BroadcastAddressAllRouters, uint duration = 120); ZigbeeError factoryResetNetwork(const QUuid &networkUuid); ZigbeeError refreshNeighborTables(const QUuid &networkUuid); + ZigbeeReply *createBinding(const QUuid &networkUuid, const ZigbeeAddress &sourceAddress, quint8 sourceEndpointId, quint16 clusterId, const ZigbeeAddress &destinationAddress, quint8 destinationEndpointId); private: ZigbeeAdapters m_adapters; diff --git a/tests/auto/api.json b/tests/auto/api.json index 4943ddde..7d17a838 100644 --- a/tests/auto/api.json +++ b/tests/auto/api.json @@ -490,6 +490,10 @@ "ZWaveNodeTypeSlave", "ZWaveNodeTypeRoutingSlave" ], + "ZigbeeClusterDirection": [ + "ZigbeeClusterDirectionServer", + "ZigbeeClusterDirectionClient" + ], "ZigbeeError": [ "ZigbeeErrorNoError", "ZigbeeErrorAdapterNotAvailable", @@ -500,7 +504,10 @@ "ZigbeeErrorUnknownBackend", "ZigbeeErrorNodeNotFound", "ZigbeeErrorForbidden", - "ZigbeeErrorInvalidChannel" + "ZigbeeErrorInvalidChannel", + "ZigbeeErrorNetworkError", + "ZigbeeErrorTimeoutError", + "ZigbeeErrorNotSupported" ], "ZigbeeNetworkState": [ "ZigbeeNetworkStateOffline", @@ -2158,6 +2165,22 @@ "zigbeeError": "$ref:ZigbeeError" } }, + "Zigbee.CreateBinding": { + "description": "Create a binding. Use destinationAddress and destinationEndpointId to create a node to node binding, or use destinationGroupAddress to create a group binding.", + "params": { + "clusterId": "Uint", + "networkUuid": "Uuid", + "o:destinationAddress": "String", + "o:destinationEndpointId": "Uint", + "o:destinationGroupAddress": "Uint", + "sourceAddress": "String", + "sourceEndpointId": "Uint" + }, + "permissionScope": "PermissionScopeAdmin", + "returns": { + "zigbeeError": "$ref:ZigbeeError" + } + }, "Zigbee.FactoryResetNetwork": { "description": "Factory reset the network with the given 'networkUuid'. The network does not have to be online for this procedure, and all associated nodes and things will be removed permanently.", "params": { @@ -2212,6 +2235,17 @@ "zigbeeError": "$ref:ZigbeeError" } }, + "Zigbee.RefreshBindings": { + "description": "Refresh the binding table for the given node.", + "params": { + "ieeeAddress": "String", + "networkUuid": "Uuid" + }, + "permissionScope": "PermissionScopeAdmin", + "returns": { + "zigbeeError": "$ref:ZigbeeError" + } + }, "Zigbee.RefreshNeighborTables": { "description": "Refresh the neighbor table for all nodes. Note that calling this may cause a lot of traffic in the ZigBee network.", "params": { @@ -2222,6 +2256,22 @@ "zigbeeError": "$ref:ZigbeeError" } }, + "Zigbee.RemoveBinding": { + "description": "Remove a binding.", + "params": { + "clusterId": "Uint", + "networkUuid": "Uuid", + "o:destinationAddress": "String", + "o:destinationEndpointId": "Uint", + "o:destinationGroupAddress": "Uint", + "sourceAddress": "String", + "sourceEndpointId": "Uint" + }, + "permissionScope": "PermissionScopeAdmin", + "returns": { + "zigbeeError": "$ref:ZigbeeError" + } + }, "Zigbee.RemoveNetwork": { "description": "Remove the ZigBee network with the given network uuid.", "params": { @@ -3276,6 +3326,18 @@ "ZigbeeAdapters": [ "$ref:ZigbeeAdapter" ], + "ZigbeeBindingTableRecord": { + "clusterId": "Uint", + "o:destinationAddress": "String", + "o:destinationEndpointId": "Uint", + "o:destinationGroupAddress": "Uint", + "sourceAddress": "String", + "sourceEndpointId": "Uint" + }, + "ZigbeeCluster": { + "clusterId": "Uint", + "direction": "$ref:ZigbeeClusterDirection" + }, "ZigbeeNeighborTableRecord": { "depth": "Uint", "lqi": "Uint", @@ -3300,6 +3362,12 @@ "serialPort": "String" }, "ZigbeeNode": { + "bindingTableRecords": [ + "$ref:ZigbeeBindingTableRecord" + ], + "endpoints": [ + "$ref:ZigbeeNodeEndpoint" + ], "ieeeAddress": "String", "lastSeen": "Uint", "lqi": "Uint", @@ -3319,6 +3387,15 @@ "type": "$ref:ZigbeeNodeType", "version": "String" }, + "ZigbeeNodeEndpoint": { + "endpointId": "Uint", + "inputClusters": [ + "$ref:ZigbeeCluster" + ], + "outputClusters": [ + "$ref:ZigbeeCluster" + ] + }, "ZigbeeRoutingTableRecord": { "destinationAddress": "Uint", "manyToOne": "Bool",