// SPDX-License-Identifier: LGPL-3.0-or-later /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * nymea-zigbee * Zigbee integration module for nymea * * Copyright (C) 2013 - 2024, nymea GmbH * Copyright (C) 2024 - 2025, chargebyte austria GmbH * * This file is part of nymea-zigbee. * * nymea-zigbee 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 3 * of the License, or (at your option) any later version. * * nymea-zigbee 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 nymea-zigbee. If not, see . * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ #include "zigbeenodenxp.h" #include "loggingcategory.h" #include "zigbeeutils.h" #include "zigbeenodeendpointnxp.h" #include /* Node initialisation steps (sequentially) * - Authenticate * - Node descriptor * - Power descriptor * - Active endpoints * - for each endpoint do: * - Simple descriptor request * - for each endpoint * - read basic cluster */ ZigbeeNodeNxp::ZigbeeNodeNxp(ZigbeeBridgeControllerNxp *controller, QObject *parent): ZigbeeNode(parent), m_controller(controller) { } void ZigbeeNodeNxp::leaveNetworkRequest(bool rejoin, bool removeChildren) { ZigbeeInterfaceReply *reply = m_controller->commandManagementLeaveRequest(shortAddress(), extendedAddress(), rejoin, removeChildren); connect(reply, &ZigbeeInterfaceReply::finished, this, [reply](){ reply->deleteLater(); if (reply->status() != ZigbeeInterfaceReply::Success) { qCWarning(dcZigbeeController()) << "Could not" << reply->request().description() << reply->status() << reply->statusErrorMessage(); // TODO: check error handling //return; } qCDebug(dcZigbeeController()) << reply->request().description() << "finished successfully"; }); } void ZigbeeNodeNxp::setInitState(ZigbeeNodeNxp::InitState initState) { m_initState = initState; switch (m_initState) { case InitStateNone: m_initStateRetry = 0; break; case InitStateError: break; case InitStateNodeDescriptor: { qCDebug(dcZigbeeNode()) << "Request node descriptor for" << this; ZigbeeInterfaceReply *reply = m_controller->commandNodeDescriptorRequest(shortAddress()); connect(reply, &ZigbeeInterfaceReply::finished, this, [this, reply](){ reply->deleteLater(); if (reply->status() != ZigbeeInterfaceReply::Success) { qCWarning(dcZigbeeController()) << "Could not" << reply->request().description() << reply->status() << reply->statusErrorMessage(); m_initStateRetry++; if (m_initStateRetry > 3) { qCWarning(dcZigbeeNode()) << "Failed to get node descriptor after 3 retries. This node will not be added to the network."; setInitState(InitStateError); return; } else { setInitState(InitStateNodeDescriptor); return; } } m_initStateRetry = 0; setNodeDescriptorRawData(reply->additionalMessage().data()); setInitState(InitStatePowerDescriptor); }); break; } case InitStatePowerDescriptor: { qCDebug(dcZigbeeNode()) << "Request power descriptor for" << this; ZigbeeInterfaceReply *reply = m_controller->commandPowerDescriptorRequest(shortAddress()); connect(reply, &ZigbeeInterfaceReply::finished, this, [this, reply](){ reply->deleteLater(); if (reply->status() != ZigbeeInterfaceReply::Success) { qCWarning(dcZigbeeController()) << "Could not" << reply->request().description() << reply->status() << reply->statusErrorMessage(); m_initStateRetry++; if (m_initStateRetry > 3) { qCWarning(dcZigbeeNode()) << "Failed to get power descriptor after 3 retries. This node will not be added to the network."; setInitState(InitStateError); return; } else { setInitState(InitStatePowerDescriptor); return; } } m_initStateRetry = 0; QByteArray data = reply->additionalMessage().data(); quint8 sequenceNumber = 0; quint8 status = 0; quint16 powerDescriptorFlag = 0; QDataStream stream(&data, QIODevice::ReadOnly); stream >> sequenceNumber >> status >> powerDescriptorFlag; setPowerDescriptorFlag(powerDescriptorFlag); setInitState(InitStateActiveEndpoints); }); break; } case InitStateActiveEndpoints: { qCDebug(dcZigbeeNode()) << "Request active endpoints for" << this; ZigbeeInterfaceReply *reply = m_controller->commandActiveEndpointsRequest(shortAddress()); connect(reply, &ZigbeeInterfaceReply::finished, this, [this, reply](){ reply->deleteLater(); if (reply->status() != ZigbeeInterfaceReply::Success) { qCWarning(dcZigbeeController()) << "Could not" << reply->request().description() << reply->status() << reply->statusErrorMessage(); m_initStateRetry++; if (m_initStateRetry > 3) { qCWarning(dcZigbeeNode()) << "Failed to get active endpoints after 3 retries. This node will not be added to the network."; setInitState(InitStateError); return; } else { setInitState(InitStateActiveEndpoints); return; } } m_initStateRetry = 0; QByteArray data = reply->additionalMessage().data(); quint8 sequenceNumber = 0; quint8 status = 0; quint16 shortAddress = 0; quint8 endpointCount = 0; QDataStream stream(&data, QIODevice::ReadOnly); stream >> sequenceNumber >> status >> shortAddress >> endpointCount; qCDebug(dcZigbeeNode()) << "Active endpoint list received:"; qCDebug(dcZigbeeNode()) << "Sequence number" << sequenceNumber; qCDebug(dcZigbeeNode()) << "Status:" << status; qCDebug(dcZigbeeNode()) << "Short address:" << ZigbeeUtils::convertUint16ToHexString(shortAddress); qCDebug(dcZigbeeNode()) << "Endpoint count:" << endpointCount; for (int i = 0; i < endpointCount; i++) { quint8 endpointId = 0; stream >> endpointId; m_uninitializedEndpoints.append(endpointId); qCDebug(dcZigbeeNode()) << " - " << ZigbeeUtils::convertByteToHexString(endpointId); } setInitState(InitStateSimpleDescriptors); }); break; } case InitStateSimpleDescriptors: for (int i = 0; i < m_uninitializedEndpoints.count(); i++) { quint8 endpointId = m_uninitializedEndpoints.at(i); qCDebug(dcZigbeeNode()) << "Request active endpoints for" << this << ZigbeeUtils::convertByteToHexString(endpointId); ZigbeeInterfaceReply *reply = m_controller->commandSimpleDescriptorRequest(shortAddress(), endpointId); connect(reply, &ZigbeeInterfaceReply::finished, this, [this, reply, endpointId](){ reply->deleteLater(); if (reply->status() != ZigbeeInterfaceReply::Success) { qCWarning(dcZigbeeController()) << "Could not" << reply->request().description() << reply->status() << reply->statusErrorMessage(); } else { // Create endpoint qCDebug(dcZigbeeController()) << reply->request().description() << "finished successfully"; qCDebug(dcZigbeeController()) << reply->additionalMessage(); QByteArray data = reply->additionalMessage().data(); quint8 sequenceNumber = 0; quint8 status = 0; quint16 shortAddress = 0; quint8 length = 0; quint8 endpointId = 0; quint16 profileId = 0; quint16 deviceId = 0; quint8 bitField = 0; quint8 inputClusterCount = 0; quint8 outputClusterCount = 0; QDataStream stream(&data, QIODevice::ReadOnly); stream >> sequenceNumber >> status >> shortAddress >> length; stream >> endpointId >> profileId >> deviceId >> bitField; qCDebug(dcZigbeeNetwork()) << "Node endpoint simple descriptor:"; qCDebug(dcZigbeeNetwork()) << " Sequence number:" << ZigbeeUtils::convertByteToHexString(sequenceNumber); qCDebug(dcZigbeeNetwork()) << " Status:" << ZigbeeUtils::convertByteToHexString(status); qCDebug(dcZigbeeNetwork()) << " Nwk address:" << ZigbeeUtils::convertUint16ToHexString(shortAddress); qCDebug(dcZigbeeNetwork()) << " Lenght:" << ZigbeeUtils::convertByteToHexString(length); qCDebug(dcZigbeeNetwork()) << " End Point:" << ZigbeeUtils::convertByteToHexString(endpointId); qCDebug(dcZigbeeNetwork()) << " Profile:" << ZigbeeUtils::profileIdToString(static_cast(profileId)); if (profileId == Zigbee::ZigbeeProfileLightLink) { qCDebug(dcZigbeeNetwork()) << " Device ID:" << ZigbeeUtils::convertUint16ToHexString(deviceId) << static_cast(deviceId); } else if (profileId == Zigbee::ZigbeeProfileHomeAutomation) { qCDebug(dcZigbeeNetwork()) << " Device ID:" << ZigbeeUtils::convertUint16ToHexString(deviceId) << static_cast(deviceId); } else if (profileId == Zigbee::ZigbeeProfileGreenPower) { qCDebug(dcZigbeeNetwork()) << " Device ID:" << ZigbeeUtils::convertUint16ToHexString(deviceId) << static_cast(deviceId); } quint8 deviceVersion = (bitField >> 4); qCDebug(dcZigbeeNetwork()) << " Bit field:" << ZigbeeUtils::convertByteToHexString(bitField) << deviceVersion; ZigbeeNodeEndpointNxp *endpoint = qobject_cast(getEndpoint(endpointId)); if (!endpoint) { endpoint = new ZigbeeNodeEndpointNxp(m_controller, this, endpointId, this); m_endpoints.append(endpoint); } endpoint->setProfile(static_cast(profileId)); endpoint->setDeviceId(deviceId); endpoint->setDeviceVersion(deviceVersion); stream >> inputClusterCount; qCDebug(dcZigbeeNetwork()) << " Input clusters: (" << inputClusterCount << ")"; for (int i = 0; i < inputClusterCount; i+=1) { quint16 clusterId = 0; stream >> clusterId; if (!endpoint->hasInputCluster(static_cast(clusterId))) { endpoint->addInputCluster(new ZigbeeCluster(static_cast(clusterId), ZigbeeCluster::Input, endpoint)); } qCDebug(dcZigbeeNetwork()) << " Cluster ID:" << ZigbeeUtils::convertUint16ToHexString(clusterId) << ZigbeeUtils::clusterIdToString(static_cast(clusterId)); } stream >> outputClusterCount; qCDebug(dcZigbeeNetwork()) << " Output clusters: (" << outputClusterCount << ")"; for (int i = 0; i < outputClusterCount; i+=1) { if (stream.atEnd()) { qCWarning(dcZigbeeNode()) << "Data stream already at the end but more data expected. Looks like the firmware doesn't provide more data."; break; } quint16 clusterId = 0; stream >> clusterId; if (!endpoint->hasOutputCluster(static_cast(clusterId))) { endpoint->addOutputCluster(new ZigbeeCluster(static_cast(clusterId), ZigbeeCluster::Output, endpoint)); } qCDebug(dcZigbeeNetwork()) << " Cluster ID:" << ZigbeeUtils::convertUint16ToHexString(clusterId) << ZigbeeUtils::clusterIdToString(static_cast(clusterId)); } } m_uninitializedEndpoints.removeAll(endpointId); if (m_uninitializedEndpoints.isEmpty()) { qCDebug(dcZigbeeNode()) << "All endpoints fetched."; setInitState(InitStateReadBasicClusterAttributes); } }); } break; case InitStateReadBasicClusterAttributes: if (shortAddress() == 0x0000) { qCDebug(dcZigbeeNode()) << "No need to read the endpoint baisc clusters of the coordinator node"; setState(StateInitialized); break; } foreach (ZigbeeNodeEndpoint *endpoint, m_endpoints) { ZigbeeNodeEndpointNxp *endpointNxp = qobject_cast(endpoint); // Read basic cluster qCDebug(dcZigbeeNode()) << "Read basic cluster for endpoint" << endpoint; ZigbeeCluster *basicCluster = endpoint->getInputCluster(Zigbee::ClusterIdBasic); if (!basicCluster) { qCDebug(dcZigbeeNode()) << "This endpoint has no basic cluster" << endpoint; continue; } m_uninitializedEndpoints.clear(); m_uninitalizedBasicClusterAttributes.append(ZigbeeCluster::BasicAttributeZclVersion); m_uninitalizedBasicClusterAttributes.append(ZigbeeCluster::BasicAttributeManufacturerName); // Note: some devices inform about the model identifier trough attribute report and the cluster contains different information // Read the model identifier only if we don't have it yet. This is out of spec but required for some strange endpoints. if (!basicCluster->hasAttribute(ZigbeeCluster::BasicAttributeModelIdentifier)) m_uninitalizedBasicClusterAttributes.append(ZigbeeCluster::BasicAttributeModelIdentifier); m_uninitalizedBasicClusterAttributes.append(ZigbeeCluster::BasicAttributePowerSource); m_uninitalizedBasicClusterAttributes.append(ZigbeeCluster::BasicAttributeSwBuildId); // Note: for having smoother flow request each attribute sequentially, not all at once for (int i = 0; i < m_uninitalizedBasicClusterAttributes.count(); i++) { quint16 attributeId = m_uninitalizedBasicClusterAttributes.at(i); ZigbeeInterfaceReply *reply = m_controller->commandReadAttributeRequest(0x02, shortAddress(), 0x01, endpoint->endpointId(), basicCluster, { attributeId }, false, manufacturerCode()); connect(reply, &ZigbeeInterfaceReply::finished, this, [this, reply, endpointNxp, attributeId](){ reply->deleteLater(); if (reply->status() != ZigbeeInterfaceReply::Success) { qCWarning(dcZigbeeController()) << "Could not" << reply->request().description() << reply->status() << reply->statusErrorMessage(); } else { ZigbeeClusterAttributeReport report = ZigbeeUtils::parseAttributeReport(reply->additionalMessage().data()); qCDebug(dcZigbeeNode()) << "Reading basic cluster attributes finished successfully for" << endpointNxp << report; if (report.attributeStatus == Zigbee::ZigbeeStatusSuccess) { if (attributeId == ZigbeeCluster::BasicAttributeManufacturerName) { endpointNxp->setManufacturerName(QString::fromUtf8(report.data)); } else if (attributeId == ZigbeeCluster::BasicAttributeModelIdentifier) { endpointNxp->setModelIdentifier(QString::fromUtf8(report.data)); } else if (attributeId == ZigbeeCluster::BasicAttributeSwBuildId) { endpointNxp->setSoftwareBuildId(QString::fromUtf8(report.data)); } } } m_uninitalizedBasicClusterAttributes.removeAll(attributeId); if (m_uninitalizedBasicClusterAttributes.isEmpty()) { setState(StateInitialized); } }); } break; } } } void ZigbeeNodeNxp::startInitialization() { qCDebug(dcZigbeeNode()) << "Start initialization" << this; setState(StateInitializing); setInitState(InitStateNodeDescriptor); } ZigbeeNodeEndpoint *ZigbeeNodeNxp::createNodeEndpoint(quint8 endpointId, QObject *parent) { return new ZigbeeNodeEndpointNxp(m_controller, this, endpointId, parent); } void ZigbeeNodeNxp::setClusterAttributeReport(const ZigbeeClusterAttributeReport &report) { if (report.attributeStatus != Zigbee::ZigbeeStatusSuccess) { qCWarning(dcZigbeeNode()) << this << "Got incalid status report" << report; return; } ZigbeeNodeEndpointNxp *endpoint = qobject_cast(getEndpoint(report.endpointId)); if (!endpoint) { qCDebug(dcZigbeeNetwork()) << "Recived attribute report but there is no endpoint on this node yet. Create it..."; endpoint = new ZigbeeNodeEndpointNxp(m_controller, this, report.endpointId, this); m_endpoints.append(endpoint); } endpoint->setClusterAttribute(report.clusterId, ZigbeeClusterAttribute(report.attributeId, report.dataType, report.data)); }