// 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 "zigbeenode.h"
#include "zigbeeutils.h"
#include "zigbeenetwork.h"
#include "loggingcategory.h"
#include
ZigbeeNode::ZigbeeNode(ZigbeeNetwork *network, quint16 shortAddress, const ZigbeeAddress &extendedAddress, QObject *parent) :
QObject(parent),
m_network(network),
m_shortAddress(shortAddress),
m_extendedAddress(extendedAddress)
{
m_deviceObject = new ZigbeeDeviceObject(m_network, this, this);
}
ZigbeeNode::State ZigbeeNode::state() const
{
return m_state;
}
bool ZigbeeNode::reachable() const
{
return m_reachable;
}
QUuid ZigbeeNode::networkUuid() const
{
return m_network->networkUuid();
}
ZigbeeDeviceObject *ZigbeeNode::deviceObject() const
{
return m_deviceObject;
}
quint16 ZigbeeNode::shortAddress() const
{
return m_shortAddress;
}
ZigbeeAddress ZigbeeNode::extendedAddress() const
{
return m_extendedAddress;
}
QList ZigbeeNode::endpoints() const
{
return m_endpoints;
}
bool ZigbeeNode::hasEndpoint(quint8 endpointId) const
{
return getEndpoint(endpointId) != nullptr;
}
ZigbeeNodeEndpoint *ZigbeeNode::getEndpoint(quint8 endpointId) const
{
foreach (ZigbeeNodeEndpoint *ep, m_endpoints) {
if (ep->endpointId()== endpointId) {
return ep;
}
}
return nullptr;
}
QString ZigbeeNode::manufacturerName() const
{
return m_manufacturerName;
}
QString ZigbeeNode::modelName() const
{
return m_modelName;
}
QString ZigbeeNode::version() const
{
return m_version;
}
quint8 ZigbeeNode::lqi() const
{
return m_lqi;
}
QDateTime ZigbeeNode::lastSeen() const
{
return m_lastSeen;
}
ZigbeeDeviceProfile::NodeDescriptor ZigbeeNode::nodeDescriptor() const
{
return m_nodeDescriptor;
}
bool ZigbeeNode::nodeDescriptorAvailable() const
{
return m_nodeDescriptorAvailable;
}
ZigbeeDeviceProfile::MacCapabilities ZigbeeNode::macCapabilities() const
{
return m_macCapabilities;
}
ZigbeeDeviceProfile::PowerDescriptor ZigbeeNode::powerDescriptor() const
{
return m_powerDescriptor;
}
bool ZigbeeNode::powerDescriptorAvailable() const
{
return m_powerDescriptorAvailable;
}
QList ZigbeeNode::bindingTableRecords() const
{
return m_bindingTableRecords;
}
QList ZigbeeNode::neighborTableRecords() const
{
return m_neighborTableRecords.values();
}
QList ZigbeeNode::routingTableRecords() const
{
return m_routingTableRecords.values();
}
void ZigbeeNode::setState(ZigbeeNode::State state)
{
if (m_state == state)
return;
qCDebug(dcZigbeeNode()) << "State changed" << this << state;
m_state = state;
emit stateChanged(m_state);
}
void ZigbeeNode::setReachable(bool reachable)
{
if (m_reachable == reachable)
return;
if (!reachable) {
qCWarning(dcZigbeeNode()) << "Reachable changed" << this << reachable;
} else {
qCDebug(dcZigbeeNode()) << "Reachable changed" << this << reachable;
}
m_reachable = reachable;
emit reachableChanged(m_reachable);
}
void ZigbeeNode::startInitialization()
{
setState(StateInitializing);
/* Node initialisation steps (sequentially)
* - Node descriptor
* - Power descriptor
* - Active endpoints
* - for each endpoint do:
* - Simple descriptor request
* - for each endpoint
* - read basic cluster
* - Read binding table
*/
initNodeDescriptor();
}
ZigbeeReply *ZigbeeNode::removeAllBindings()
{
ZigbeeReply *reply = new ZigbeeReply(this);
ZigbeeReply *readBindingsReply = readBindingTableEntries();
connect(readBindingsReply, &ZigbeeReply::finished, reply, [=](){
if (readBindingsReply->error()) {
qCWarning(dcZigbeeNode()) << "Failed to remove all bindings because the current bindings could not be fetched from node" << this;
reply->finishReply(readBindingsReply->error());
return;
}
qCDebug(dcZigbeeNode()) << "Current binding table records:";
foreach (const ZigbeeDeviceProfile::BindingTableListRecord &binding, m_bindingTableRecords) {
qCDebug(dcZigbeeNode()) << binding;
}
// Remove bindings sequentially and finish reply if error occures or all bindings removed
removeNextBinding(reply);
});
return reply;
}
ZigbeeReply *ZigbeeNode::readBindingTableEntries()
{
ZigbeeReply *reply = new ZigbeeReply(this);
readBindingTableChunk(reply, 0);
return reply;
}
ZigbeeReply *ZigbeeNode::readLqiTableEntries()
{
ZigbeeReply *reply = new ZigbeeReply(this);
readNeighborTableChunk(reply, 0);
return reply;
}
ZigbeeReply *ZigbeeNode::readRoutingTableEntries()
{
ZigbeeReply *reply = new ZigbeeReply(this);
readRoutingTableChunk(reply, 0);
return reply;
}
ZigbeeReply *ZigbeeNode::addBinding(quint8 sourceEndpointId, quint16 clusterId, const ZigbeeAddress &destinationAddress, quint8 destinationEndpoint)
{
ZigbeeReply *reply = new ZigbeeReply(this);
ZigbeeDeviceObjectReply *zdoReply = deviceObject()->requestBindIeeeAddress(sourceEndpointId, clusterId, destinationAddress, destinationEndpoint);
connect(zdoReply, &ZigbeeDeviceObjectReply::finished, reply, [this, zdoReply, reply](){
if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Failed to configure binding on node" << this;
reply->finishReply(ZigbeeReply::ErrorZigbeeError);
return;
}
qCDebug(dcZigbeeNode) << "Binding added";
reply->finishReply();
readBindingTableEntries();
});
return reply;
}
ZigbeeReply *ZigbeeNode::addBinding(quint8 sourceEndpointId, quint16 clusterId, quint16 destinationGroupAddress)
{
ZigbeeReply *reply = new ZigbeeReply(this);
ZigbeeDeviceObjectReply *zdoReply = deviceObject()->requestBindGroupAddress(sourceEndpointId, clusterId, destinationGroupAddress);
connect(zdoReply, &ZigbeeDeviceObjectReply::finished, reply, [this, zdoReply, reply](){
if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Failed to configure binding on node" << this;
reply->finishReply(ZigbeeReply::ErrorZigbeeError);
return;
}
qCDebug(dcZigbeeNode) << "Binding added";
reply->finishReply();
readBindingTableEntries();
});
return reply;
}
ZigbeeReply *ZigbeeNode::removeBinding(const ZigbeeDeviceProfile::BindingTableListRecord &binding)
{
ZigbeeReply *reply = new ZigbeeReply(this);
ZigbeeDeviceObjectReply *zdoReply = deviceObject()->requestUnbind(binding);
connect(zdoReply, &ZigbeeDeviceObjectReply::finished, reply, [this, zdoReply, reply](){
if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Failed to rebinding binding on node" << this;
reply->finishReply(ZigbeeReply::ErrorZigbeeError);
return;
}
qCDebug(dcZigbeeNode) << "Binding removed";
reply->finishReply();
readBindingTableEntries();
});
return reply;
}
void ZigbeeNode::initNodeDescriptor()
{
qCDebug(dcZigbeeNode()) << "Requesting node descriptor from" << this;
ZigbeeDeviceObjectReply *reply = deviceObject()->requestNodeDescriptor();
connect(reply, &ZigbeeDeviceObjectReply::finished, this, [this, reply](){
if (reply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Error occured during initialization of" << this << "Failed to read node descriptor" << reply->error();
m_requestRetry++;
if (m_requestRetry < m_requestRetriesMax) {
qCDebug(dcZigbeeNode()) << "Retrying to request node descriptor" << m_requestRetry << "/" << m_requestRetriesMax;
QTimer::singleShot(500, this, [=](){ initNodeDescriptor(); });
} else {
qCWarning(dcZigbeeNode()) << "Failed to read node descriptor from" << this << "after" << m_requestRetriesMax << "attempts.";
m_requestRetry = 0;
qCWarning(dcZigbeeNode()) << this << "is out of spec. A device must implement the node descriptor. Continue anyways with the power decriptor...";
initPowerDescriptor();
}
return;
}
qCDebug(dcZigbeeNode()) << this << "reading node descriptor finished successfully.";
m_nodeDescriptor = ZigbeeDeviceProfile::parseNodeDescriptor(reply->responseAdpu().payload);
qCDebug(dcZigbeeNode()) << m_nodeDescriptor;
m_nodeDescriptorAvailable = true;
m_requestRetry = 0;
// Continue with the power descriptor
initPowerDescriptor();
});
}
void ZigbeeNode::initPowerDescriptor()
{
qCDebug(dcZigbeeNode()) << "Request power descriptor from" << this;
ZigbeeDeviceObjectReply *reply = deviceObject()->requestPowerDescriptor();
connect(reply, &ZigbeeDeviceObjectReply::finished, this, [this, reply](){
if (reply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Error occured during initialization of" << this << "Failed to read power descriptor" << reply->error();
if (m_requestRetry < m_requestRetriesMax) {
m_requestRetry++;
qCDebug(dcZigbeeNode()) << "Retry to request power descriptor from" << this << m_requestRetry << "/" << m_requestRetriesMax << "attempts.";
QTimer::singleShot(500, this, [=](){ initPowerDescriptor(); });
} else {
qCWarning(dcZigbeeNode()) << "Failed to read power descriptor from" << this << "after" << m_requestRetriesMax << "attempts. Giving up reading power descriptor.";
qCWarning(dcZigbeeNode()) << this << "is out of spec. A device must implement the power descriptor. Continue anyways with the endpoint initialization...";
initEndpoints();
}
return;
}
qCDebug(dcZigbeeNode()) << this << "reading power descriptor finished successfully.";
QDataStream stream(reply->responseAdpu().payload);
stream.setByteOrder(QDataStream::LittleEndian);
quint16 powerDescriptorFlag = 0;
stream >> powerDescriptorFlag;
m_powerDescriptor = ZigbeeDeviceProfile::parsePowerDescriptor(powerDescriptorFlag);
qCDebug(dcZigbeeNode()) << m_powerDescriptor;
m_powerDescriptorAvailable = true;
m_requestRetry = 0;
// Continue with endpoint fetching
initEndpoints();
});
}
void ZigbeeNode::initEndpoints()
{
qCDebug(dcZigbeeNode()) << "Request active endpoints from" << this;
ZigbeeDeviceObjectReply *reply = deviceObject()->requestActiveEndpoints();
connect(reply, &ZigbeeDeviceObjectReply::finished, this, [this, reply](){
if (reply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Error occured during initialization of" << this << "Failed to read active endpoints" << reply->error();
if (m_requestRetry < m_requestRetriesMax) {
m_requestRetry++;
qCDebug(dcZigbeeNode()) << "Retry to request active endpoints from" << this << m_requestRetry << "/" << m_requestRetriesMax << "attempts.";
QTimer::singleShot(500, this, [=](){ initEndpoints(); });
} else {
qCWarning(dcZigbeeNode()) << "Failed to read active endpoints from" << this << "after" << m_requestRetriesMax << "attempts. Giving up reading endpoints.";
m_requestRetry = 0;
setState(StateInitialized);
}
return;
}
qCDebug(dcZigbeeNode()) << this << "reading active endpoints finished successfully.";
QDataStream stream(reply->responseAdpu().payload);
stream.setByteOrder(QDataStream::LittleEndian);
quint8 endpointCount = 0;
m_uninitializedEndpoints.clear();
stream >> endpointCount;
for (int i = 0; i < endpointCount; i++) {
quint8 endpoint = 0;
stream >> endpoint;
m_uninitializedEndpoints.append(endpoint);
}
qCDebug(dcZigbeeNode()) << "Endpoints (" << endpointCount << ")";
for (int i = 0; i < m_uninitializedEndpoints.count(); i++) {
qCDebug(dcZigbeeNode()) << " -" << ZigbeeUtils::convertByteToHexString(m_uninitializedEndpoints.at(i));
}
m_requestRetry = 0;
// If there a no endpoints or all endpoints have already be initialized, continue with reading the basic cluster information
if (m_uninitializedEndpoints.isEmpty()) {
initBasicCluster();
return;
}
// Start reading simple descriptors sequentially
initEndpoint(m_uninitializedEndpoints.first());
});
}
void ZigbeeNode::initEndpoint(quint8 endpointId)
{
qCDebug(dcZigbeeNode()) << "Read simple descriptor of endpoint" << ZigbeeUtils::convertByteToHexString(endpointId);
ZigbeeDeviceObjectReply *reply = deviceObject()->requestSimpleDescriptor(endpointId);
connect(reply, &ZigbeeDeviceObjectReply::finished, this, [this, reply, endpointId](){
if (reply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Error occured during initialization of" << this << "Failed to read simple descriptor for endpoint" << endpointId << reply->error();
if (m_requestRetry < m_requestRetriesMax) {
m_requestRetry++;
qCDebug(dcZigbeeNode()) << "Retry to request simple descriptor from" << this << ZigbeeUtils::convertByteToHexString(endpointId) << m_requestRetry << "/" << m_requestRetriesMax << "attempts.";
QTimer::singleShot(500, this, [=](){ initEndpoint(endpointId); });
} else {
qCWarning(dcZigbeeNode()) << "Failed to read simple descriptor from" << this << ZigbeeUtils::convertByteToHexString(endpointId) << "after" << m_requestRetriesMax << "attempts. Giving up initializing endpoint" << endpointId;
m_requestRetry = 0;
if (m_uninitializedEndpoints.isEmpty()) {
// Continue with the basic cluster attributes
initBasicCluster();
} else {
// Fetch next endpoint
initEndpoint(m_uninitializedEndpoints.first());
}
}
return;
}
qCDebug(dcZigbeeNode()) << this << "reading simple descriptor for endpoint" << endpointId << "finished successfully.";
m_requestRetry = 0;
quint8 length = 0; quint8 endpointId = 0; quint16 profileId = 0; quint16 deviceId = 0; quint8 deviceVersion = 0;
quint8 inputClusterCount = 0; quint8 outputClusterCount = 0;
QList inputClusters;
QList outputClusters;
QDataStream stream(reply->responseAdpu().payload);
stream.setByteOrder(QDataStream::LittleEndian);
stream >> length >> endpointId >> profileId >> deviceId >> deviceVersion >> inputClusterCount;
qCDebug(dcZigbeeNode()) << "Node endpoint simple descriptor:";
qCDebug(dcZigbeeNode()) << " Lenght:" << ZigbeeUtils::convertByteToHexString(length);
qCDebug(dcZigbeeNode()) << " End Point:" << ZigbeeUtils::convertByteToHexString(endpointId);
qCDebug(dcZigbeeNode()) << " Profile:" << ZigbeeUtils::profileIdToString(static_cast(profileId));
if (profileId == Zigbee::ZigbeeProfileLightLink) {
qCDebug(dcZigbeeNode()) << " Device ID:" << ZigbeeUtils::convertUint16ToHexString(deviceId) << static_cast(deviceId);
} else if (profileId == Zigbee::ZigbeeProfileHomeAutomation) {
qCDebug(dcZigbeeNode()) << " Device ID:" << ZigbeeUtils::convertUint16ToHexString(deviceId) << static_cast(deviceId);
} else if (profileId == Zigbee::ZigbeeProfileGreenPower) {
qCDebug(dcZigbeeNode()) << " Device ID:" << ZigbeeUtils::convertUint16ToHexString(deviceId) << static_cast(deviceId);
}
qCDebug(dcZigbeeNode()) << " Device version:" << ZigbeeUtils::convertByteToHexString(deviceVersion);
// Create endpoint
ZigbeeNodeEndpoint *endpoint = nullptr;
if (!hasEndpoint(endpointId)) {
endpoint = new ZigbeeNodeEndpoint(m_network, this, endpointId, this);
m_endpoints.append(endpoint);
} else {
endpoint = getEndpoint(endpointId);
}
endpoint->setProfile(static_cast(profileId));
endpoint->setDeviceId(deviceId);
endpoint->setDeviceVersion(deviceVersion);
// Parse and add server clusters
qCDebug(dcZigbeeNode()) << " Input clusters: (" << inputClusterCount << ")";
for (int i = 0; i < inputClusterCount; i++) {
quint16 clusterId = 0;
stream >> clusterId;
if (!endpoint->hasInputCluster(static_cast(clusterId))) {
endpoint->addInputCluster(endpoint->createCluster(static_cast(clusterId), ZigbeeCluster::Server));
}
qCDebug(dcZigbeeNode()) << " Cluster ID:" << ZigbeeUtils::convertUint16ToHexString(clusterId) << ZigbeeUtils::clusterIdToString(static_cast(clusterId));
}
// Parse and add client clusters
stream >> outputClusterCount;
qCDebug(dcZigbeeNode()) << " Output clusters: (" << outputClusterCount << ")";
for (int i = 0; i < outputClusterCount; i++) {
quint16 clusterId = 0;
stream >> clusterId;
if (!endpoint->hasOutputCluster(static_cast(clusterId))) {
endpoint->addOutputCluster(endpoint->createCluster(static_cast(clusterId), ZigbeeCluster::Client));
}
qCDebug(dcZigbeeNode()) << " Cluster ID:" << ZigbeeUtils::convertUint16ToHexString(clusterId) << ZigbeeUtils::clusterIdToString(static_cast(clusterId));
}
m_uninitializedEndpoints.removeAll(endpointId);
endpoint->m_initialized = true;
setupEndpointInternal(endpoint);
if (m_uninitializedEndpoints.isEmpty()) {
// Note: if we are initializing the coordinator, we can stop here
if (m_shortAddress == 0) {
setState(StateInitialized);
return;
}
// Continue with the basic cluster attributes
initBasicCluster();
} else {
// Fetch next endpoint
initEndpoint(m_uninitializedEndpoints.first());
}
});
}
void ZigbeeNode::removeNextBinding(ZigbeeReply *reply)
{
// If we have no bindings left, finish the given reply
if (m_bindingTableRecords.isEmpty()) {
reply->finishReply();
return;
}
ZigbeeDeviceProfile::BindingTableListRecord record = m_bindingTableRecords.last();
ZigbeeDeviceObjectReply *zdoReply = m_deviceObject->requestUnbind(record);
connect(zdoReply, &ZigbeeDeviceObjectReply::finished, reply, [=](){
if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Failed to remove" << record << zdoReply->error();
reply->finishReply(ZigbeeReply::ErrorZigbeeError);
return;
}
// Successfully removed
m_bindingTableRecords.removeLast();
emit bindingTableRecordsChanged();
removeNextBinding(reply);
});
}
void ZigbeeNode::readBindingTableChunk(ZigbeeReply *reply, quint8 startIndex)
{
ZigbeeDeviceObjectReply *zdoReply = deviceObject()->requestMgmtBind(startIndex);
connect(zdoReply, &ZigbeeDeviceObjectReply::finished, reply, [=](){
if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Failed to read binding table" << zdoReply->error();
if (zdoReply->zigbeeDeviceObjectStatus() == ZigbeeDeviceProfile::StatusNotSupported) {
// Not an error, merely a valid response code that this optional request is not supported.
reply->finishReply(ZigbeeReply::ErrorNoError);
} else {
reply->finishReply(ZigbeeReply::ErrorZigbeeError);
}
return;
}
qCDebug(dcZigbeeNetwork()) << "Binding table reply from" << ZigbeeUtils::convertUint16ToHexString(shortAddress()) << zdoReply->responseData().toHex();
ZigbeeDeviceProfile::BindingTable bindingTable = ZigbeeDeviceProfile::parseBindingTable(zdoReply->responseData());
if (bindingTable.startIndex == 0) {
m_bindingTable = bindingTable;
} else if (m_bindingTable.records.count() == bindingTable.startIndex) {
m_bindingTable.records.append(bindingTable.records);
} else {
qCWarning(dcZigbeeNode()) << "Error reading binding table. Indices not matching. Starting from scratch.";
readBindingTableChunk(reply, 0);
return;
}
if (m_bindingTable.records.count() < m_bindingTable.tableSize) {
qCDebug(dcZigbeeNode) << "Binding table not complete yet (" << m_bindingTable.records.count() << "/" << m_bindingTable.tableSize << "). Fetching next chunk...";
readBindingTableChunk(reply, m_bindingTable.records.count());
return;
}
bool changed = false;
QMutableListIterator it(m_bindingTableRecords);
while (it.hasNext()) {
ZigbeeDeviceProfile::BindingTableListRecord oldRecord = it.next();
bool found = false;
foreach (const ZigbeeDeviceProfile::BindingTableListRecord &record, m_bindingTable.records) {
if (record.sourceAddress == oldRecord.sourceAddress
&& record.sourceEndpoint == oldRecord.sourceEndpoint
&& record.clusterId == oldRecord.clusterId
&& record.destinationAddressMode == oldRecord.destinationAddressMode
&& record.destinationShortAddress == oldRecord.destinationShortAddress
&& record.destinationIeeeAddress == oldRecord.destinationIeeeAddress
&& record.destinationEndpoint == oldRecord.destinationEndpoint) {
found = true;
break;
}
}
if (!found) {
it.remove();
changed = true;
}
}
foreach (const ZigbeeDeviceProfile::BindingTableListRecord &record, m_bindingTable.records) {
qCDebug(dcZigbeeNode()) << "Binding table record fetched" << record;
bool found = false;
foreach (const ZigbeeDeviceProfile::BindingTableListRecord &oldRecord, m_bindingTableRecords) {
if (record.sourceAddress == oldRecord.sourceAddress
&& record.sourceEndpoint == oldRecord.sourceEndpoint
&& record.clusterId == oldRecord.clusterId
&& record.destinationAddressMode == oldRecord.destinationAddressMode
&& record.destinationShortAddress == oldRecord.destinationShortAddress
&& record.destinationIeeeAddress == oldRecord.destinationIeeeAddress
&& record.destinationEndpoint == oldRecord.destinationEndpoint) {
found = true;
break;
}
}
if (!found) {
qCDebug(dcZigbeeNode()) << "New binding table record:" << record;
m_bindingTableRecords.append(record);
changed = true;
}
}
if (changed) {
emit bindingTableRecordsChanged();
}
reply->finishReply(ZigbeeReply::ErrorNoError);
});
}
void ZigbeeNode::readNeighborTableChunk(ZigbeeReply *reply, quint8 startIndex)
{
ZigbeeDeviceObjectReply *zdoReply = deviceObject()->requestMgmtLqi(startIndex);
connect(zdoReply, &ZigbeeDeviceObjectReply::finished, reply, [=](){
if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Failed to read neighbor table" << zdoReply->error();
if (zdoReply->zigbeeDeviceObjectStatus() == ZigbeeDeviceProfile::StatusNotSupported) {
// Not an error, merely a valid response code that this optional request is not supported.
reply->finishReply(ZigbeeReply::ErrorNoError);
} else {
reply->finishReply(ZigbeeReply::ErrorZigbeeError);
}
return;
}
// qCDebug(dcZigbeeNode()) << "LQI neighbor table reply:" << zdoReply->responseData().toHex();
ZigbeeDeviceProfile::NeighborTable neighborTable = ZigbeeDeviceProfile::parseNeighborTable(zdoReply->responseData());
if (neighborTable.startIndex == 0) {
m_neighborTable = neighborTable;
} else if (m_neighborTable.records.count() == neighborTable.startIndex) {
m_neighborTable.records.append(neighborTable.records);
} else {
qCWarning(dcZigbeeNode()) << "Error processing neighbor table. Indices not matching. Starting from scratch.";
readNeighborTableChunk(reply, 0);
return;
}
if (m_neighborTable.records.count() < m_neighborTable.tableSize) {
qCDebug(dcZigbeeNode) << "Neighbor table not complete yet. Fetching next chunk";
readNeighborTableChunk(reply, m_neighborTable.records.count());
return;
}
bool changed = false;
QList removedRecords = m_neighborTableRecords.keys();
foreach(const ZigbeeDeviceProfile::NeighborTableListRecord &record, m_neighborTable.records) {
qCDebug(dcZigbeeNode()).nospace() << "LQI neighbor for node: " << ZigbeeUtils::convertUint16ToHexString(m_shortAddress) << ": " << record;
removedRecords.removeAll(record.shortAddress);
if (m_neighborTableRecords.contains(record.shortAddress)) {
ZigbeeDeviceProfile::NeighborTableListRecord existingRecord = m_neighborTableRecords.value(record.shortAddress);
if (existingRecord.relationship != record.relationship
|| existingRecord.permitJoining != record.permitJoining
|| existingRecord.depth != record.depth
|| existingRecord.lqi != record.lqi) {
m_neighborTableRecords[record.shortAddress] = record;
changed = true;
}
} else {
m_neighborTableRecords.insert(record.shortAddress, record);
changed = true;
}
}
foreach (quint16 removedRecord, removedRecords) {
m_neighborTableRecords.remove(removedRecord);
changed = true;
}
if (changed) {
emit neighborTableRecordsChanged();
}
reply->finishReply(ZigbeeReply::ErrorNoError);
});
}
void ZigbeeNode::readRoutingTableChunk(ZigbeeReply *reply, quint8 startIndex)
{
ZigbeeDeviceObjectReply *zdoReply = deviceObject()->requestMgmtRtg(startIndex);
connect(zdoReply, &ZigbeeDeviceObjectReply::finished, reply, [=](){
if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Failed to read routing table" << zdoReply->error();
if (zdoReply->zigbeeDeviceObjectStatus() == ZigbeeDeviceProfile::StatusNotSupported) {
// Not an error, merely a valid response code that this optional request is not supported.
reply->finishReply(ZigbeeReply::ErrorNoError);
} else {
reply->finishReply(ZigbeeReply::ErrorZigbeeError);
}
return;
}
// qCDebug(dcZigbeeNetwork()) << "Routing table reply from" << ZigbeeUtils::convertUint16ToHexString(shortAddress()) << zdoReply->responseData().toHex();
ZigbeeDeviceProfile::RoutingTable routingTable = ZigbeeDeviceProfile::parseRoutingTable(zdoReply->responseData());
if (routingTable.startIndex == 0) {
m_routingTable = routingTable;
} else if (m_routingTable.records.count() == routingTable.startIndex) {
m_routingTable.records.append(routingTable.records);
} else {
qCWarning(dcZigbeeNode()) << "Error processing routing table. Indices not matching. Starting from scratch.";
readRoutingTableChunk(reply, 0);
return;
}
if (m_routingTable.records.count() < m_routingTable.tableSize) {
qCDebug(dcZigbeeNode) << "Routing table not complete yet. Fetching next chunk";
readRoutingTableChunk(reply, m_routingTable.records.count());
return;
}
bool changed = false;
QList removedRecords = m_routingTableRecords.keys();
foreach(const ZigbeeDeviceProfile::RoutingTableListRecord &record, m_routingTable.records) {
qCDebug(dcZigbeeNode()).nospace() << "Route entry for node: " << ZigbeeUtils::convertUint16ToHexString(m_shortAddress) << ": " << record;
removedRecords.removeAll(record.destinationAddress);
if (m_routingTableRecords.contains(record.destinationAddress)) {
ZigbeeDeviceProfile::RoutingTableListRecord existingRecord = m_routingTableRecords.value(record.destinationAddress);
if (existingRecord.status != record.status
|| existingRecord.memoryConstrained != record.memoryConstrained
|| existingRecord.manyToOne != record.manyToOne
|| existingRecord.routeRecordRequired != record.routeRecordRequired
|| existingRecord.nextHopAddress != record.nextHopAddress) {
m_routingTableRecords[record.destinationAddress] = record;
changed = true;
}
} else {
m_routingTableRecords.insert(record.destinationAddress, record);
changed = true;
}
}
foreach (quint16 removedRecord, removedRecords) {
m_routingTableRecords.remove(removedRecord);
changed = true;
}
if (changed) {
emit routingTableRecordsChanged();
}
reply->finishReply(ZigbeeReply::ErrorNoError);
});
}
void ZigbeeNode::setupEndpointInternal(ZigbeeNodeEndpoint *endpoint)
{
// Connect after initialization for out of spec nodes
connect(endpoint, &ZigbeeNodeEndpoint::inputClusterAdded, this, &ZigbeeNode::clusterAdded);
connect(endpoint, &ZigbeeNodeEndpoint::outputClusterAdded, this, &ZigbeeNode::clusterAdded);
connect(endpoint, &ZigbeeNodeEndpoint::clusterAttributeChanged, this, [this, endpoint](ZigbeeCluster *cluster, const ZigbeeClusterAttribute &attribute){
if (cluster->clusterId() == ZigbeeClusterLibrary::ClusterIdBasic && attribute.id() == ZigbeeClusterBasic::AttributeManufacturerName) {
bool valueOk = false;
QString manufacturerName = attribute.dataType().toString(&valueOk);
if (valueOk) {
if (m_manufacturerName != manufacturerName) {
m_manufacturerName = manufacturerName;
emit manufacturerNameChanged(m_manufacturerName);
endpoint->m_manufacturerName = manufacturerName;
emit endpoint->manufacturerNameChanged(m_manufacturerName);
}
} else {
qCWarning(dcZigbeeNode()) << "Could not convert manufacturer name attribute data to string" << attribute.dataType();
}
}
if (cluster->clusterId() == ZigbeeClusterLibrary::ClusterIdBasic && attribute.id() == ZigbeeClusterBasic::AttributeModelIdentifier) {
bool valueOk = false;
QString modelName = attribute.dataType().toString(&valueOk);
if (valueOk) {
if (m_modelName != modelName) {
m_modelName = modelName;
emit modelNameChanged(m_modelName);
endpoint->m_modelIdentifier = modelName;
emit endpoint->modelIdentifierChanged(m_modelName);
}
} else {
qCWarning(dcZigbeeNode()) << "Could not convert model identifier attribute data to string" << attribute.dataType();
}
}
if (cluster->clusterId() == ZigbeeClusterLibrary::ClusterIdBasic && attribute.id() == ZigbeeClusterBasic::AttributeSwBuildId) {
bool valueOk = false;
QString version = attribute.dataType().toString(&valueOk);
if (valueOk) {
if (m_version != version) {
m_version = version;
emit versionChanged(m_version);
endpoint->m_softwareBuildId = version;
emit endpoint->softwareBuildIdChanged(m_version);
}
} else {
qCWarning(dcZigbeeNode()) << "Could not convert software build id attribute data to string" << attribute.dataType();
}
}
emit endpointClusterAttributeChanged(endpoint, cluster, attribute);
});
}
void ZigbeeNode::initBasicCluster()
{
// Get the first endpoint which implements the basic cluster
ZigbeeNodeEndpoint *endpoint = nullptr;
foreach (ZigbeeNodeEndpoint *ep, endpoints()) {
if (ep->hasInputCluster(ZigbeeClusterLibrary::ClusterIdBasic)) {
endpoint = ep;
break;
}
}
if (!endpoint) {
qCWarning(dcZigbeeNode()) << "Could not find any endpoint contiaining the basic cluster on" << this << "Set the node to initialized anyways.";
setState(StateInitialized);
return;
}
ZigbeeClusterBasic *basicCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdBasic);
if (!basicCluster) {
qCWarning(dcZigbeeNode()) << "Could not find basic cluster on" << this << "Set the node to initialized anyways.";
// Set the device initialized any ways since this ist just for convenience
setState(StateInitialized);
return;
}
// Start reading basic cluster attributes sequentially
readManufacturerName(basicCluster);
}
void ZigbeeNode::readManufacturerName(ZigbeeClusterBasic *basicCluster)
{
ZigbeeClusterBasic::Attribute attributeId = ZigbeeClusterBasic::AttributeManufacturerName;
if (basicCluster->hasAttribute(attributeId)) {
// Note: only read the basic cluster information if we don't have them already from an indication.
// Some devices (Lumi/Aquara) send cluster information containing different payload than a read attribute returns.
// This is bad device stack implementation, but we want to make it work either way without destroying the correct
// workflow as specified by the stack.
qCDebug(dcZigbeeNode()) << "The manufacturer name has already been set" << this << "Continue with model identifier";
bool valueOk = false;
QString manufacturerName = basicCluster->attribute(attributeId).dataType().toString(&valueOk);
if (valueOk) {
endpoints().first()->m_manufacturerName = manufacturerName;
m_manufacturerName = manufacturerName;
emit manufacturerNameChanged(m_manufacturerName);
} else {
qCWarning(dcZigbeeNode()) << "Could not convert manufacturer name attribute data to string" << basicCluster->attribute(attributeId).dataType();
}
readModelIdentifier(basicCluster);
return;
}
qCDebug(dcZigbeeNode()) << "Reading attribute" << attributeId;
ZigbeeClusterReply *reply = basicCluster->readAttributes({static_cast(attributeId)});
connect(reply, &ZigbeeClusterReply::finished, this, [this, basicCluster, reply, attributeId](){
if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Error occured during initialization of" << this << "Failed to read basic cluster attribute" << attributeId << reply->error();
if (m_requestRetry < m_requestRetriesMax) {
m_requestRetry++;
qCDebug(dcZigbeeNode()) << "Retry to read manufacturer name from" << this << basicCluster << m_requestRetry << "/" << m_requestRetriesMax << "attempts.";
QTimer::singleShot(500, this, [=](){ readManufacturerName(basicCluster); });
} else {
qCWarning(dcZigbeeNode()) << "Failed to read manufacturer name from" << this << basicCluster << "after" << m_requestRetriesMax << "attempts. Giving up and continue...";
m_requestRetry = 0;
readModelIdentifier(basicCluster);
}
return;
}
qCDebug(dcZigbeeNode()) << "Reading basic cluster attributes finished successfully";
QList attributeStatusRecords = ZigbeeClusterLibrary::parseAttributeStatusRecords(reply->responseFrame().payload);
if (!attributeStatusRecords.isEmpty()) {
ZigbeeClusterLibrary::ReadAttributeStatusRecord attributeStatusRecord = attributeStatusRecords.first();
qCDebug(dcZigbeeNode()) << attributeStatusRecord;
basicCluster->setAttribute(ZigbeeClusterAttribute(static_cast(attributeId), attributeStatusRecord.dataType));
bool valueOk = false;
QString manufacturerName = attributeStatusRecord.dataType.toString(&valueOk);
if (valueOk) {
endpoints().first()->m_manufacturerName = manufacturerName;
m_manufacturerName = manufacturerName;
emit manufacturerNameChanged(m_manufacturerName);
} else {
qCWarning(dcZigbeeNode()) << "Could not convert manufacturer name attribute data to string" << attributeStatusRecord.dataType;
}
}
// Continue eiterh way with attribute reading
m_requestRetry = 0;
readModelIdentifier(basicCluster);
});
}
void ZigbeeNode::readModelIdentifier(ZigbeeClusterBasic *basicCluster)
{
ZigbeeClusterBasic::Attribute attributeId = ZigbeeClusterBasic::AttributeModelIdentifier;
if (basicCluster->hasAttribute(attributeId)) {
// Note: only read the basic cluster information if we don't have them already from an indication.
// Some devices (Lumi/Aquara) send cluster information containing different payload than a read attribute returns.
// This is bad device stack implementation, but we want to make it work either way without destroying the correct
// workflow as specified by the stack.
qCDebug(dcZigbeeNode()) << "The model identifier has already been set" << this << "Continue with software build ID.";
bool valueOk = false;
QString modelIdentifier = basicCluster->attribute(attributeId).dataType().toString(&valueOk);
if (valueOk) {
endpoints().first()->m_modelIdentifier= modelIdentifier;
m_modelName = modelIdentifier;
emit modelNameChanged(m_modelName);
} else {
qCWarning(dcZigbeeNode()) << "Could not convert model identifier attribute data to string" << basicCluster->attribute(attributeId).dataType();
}
readSoftwareBuildId(basicCluster);
return;
}
qCDebug(dcZigbeeNode()) << "Reading attribute" << attributeId;
ZigbeeClusterReply *reply = basicCluster->readAttributes({static_cast(attributeId)});
connect(reply, &ZigbeeClusterReply::finished, this, [this, basicCluster, reply, attributeId](){
if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Error occured during initialization of" << this << "Failed to read basic cluster attribute" << attributeId << reply->error();
if (m_requestRetry < m_requestRetriesMax) {
m_requestRetry++;
qCDebug(dcZigbeeNode()) << "Retry to read model identifier from" << this << basicCluster << m_requestRetry << "/" << m_requestRetriesMax << "attempts.";
QTimer::singleShot(500, this, [=](){ readModelIdentifier(basicCluster); });
} else {
qCWarning(dcZigbeeNode()) << "Failed to read model identifier from" << this << basicCluster << "after" << m_requestRetriesMax << "attempts. Giving up and continue...";
m_requestRetry = 0;
readSoftwareBuildId(basicCluster);
}
return;
}
qCDebug(dcZigbeeNode()) << "Reading basic cluster attributes finished successfully";
QList attributeStatusRecords = ZigbeeClusterLibrary::parseAttributeStatusRecords(reply->responseFrame().payload);
if (!attributeStatusRecords.isEmpty()) {
ZigbeeClusterLibrary::ReadAttributeStatusRecord attributeStatusRecord = attributeStatusRecords.first();
qCDebug(dcZigbeeNode()) << attributeStatusRecord;
basicCluster->setAttribute(ZigbeeClusterAttribute(static_cast(attributeId), attributeStatusRecord.dataType));
bool valueOk = false;
QString modelIdentifier = attributeStatusRecord.dataType.toString(&valueOk);
if (valueOk) {
endpoints().first()->m_modelIdentifier = modelIdentifier;
m_modelName = modelIdentifier;
emit modelNameChanged(m_modelName);
} else {
qCWarning(dcZigbeeNode()) << "Could not convert model identifier attribute data to string" << attributeStatusRecord.dataType;
}
}
// Continue eiterh way with attribute reading
m_requestRetry = 0;
readSoftwareBuildId(basicCluster);
});
}
void ZigbeeNode::readSoftwareBuildId(ZigbeeClusterBasic *basicCluster)
{
ZigbeeClusterBasic::Attribute attributeId = ZigbeeClusterBasic::AttributeSwBuildId;
qCDebug(dcZigbeeNode()) << "Reading attribute" << attributeId;
ZigbeeClusterReply *reply = basicCluster->readAttributes({static_cast(attributeId)});
connect(reply, &ZigbeeClusterReply::finished, this, [this, basicCluster, reply, attributeId](){
if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
qCWarning(dcZigbeeNode()) << "Error occured during initialization of" << this << "Failed to read basic cluster attribute" << attributeId << reply->error();
if (m_requestRetry < m_requestRetriesMax) {
m_requestRetry++;
qCDebug(dcZigbeeNode()) << "Retry to read model identifier from" << this << basicCluster << m_requestRetry << "/" << m_requestRetriesMax << "attempts.";
QTimer::singleShot(500, this, [=](){ readSoftwareBuildId(basicCluster); });
} else {
qCWarning(dcZigbeeNode()) << "Failed to read model identifier from" << this << basicCluster << "after" << m_requestRetriesMax << "attempts. Giving up and continue...";
m_requestRetry = 0;
setState(StateInitialized);
}
return;
}
qCDebug(dcZigbeeNode()) << "Reading basic cluster attributes finished successfully";
QList attributeStatusRecords = ZigbeeClusterLibrary::parseAttributeStatusRecords(reply->responseFrame().payload);
if (!attributeStatusRecords.isEmpty()) {
ZigbeeClusterLibrary::ReadAttributeStatusRecord attributeStatusRecord = attributeStatusRecords.first();
qCDebug(dcZigbeeNode()) << attributeStatusRecord;
basicCluster->setAttribute(ZigbeeClusterAttribute(static_cast(attributeId), attributeStatusRecord.dataType));
bool valueOk = false;
QString softwareBuildId = attributeStatusRecord.dataType.toString(&valueOk);
if (valueOk) {
endpoints().first()->m_softwareBuildId = softwareBuildId;
m_version = softwareBuildId;
emit versionChanged(m_version);
} else {
qCWarning(dcZigbeeNode()) << "Could not convert software build id attribute data to string" << attributeStatusRecord.dataType;
}
}
// Finished with reading basic cluster, the node is initialized.
setState(StateInitialized);
// Reading binding table. If this fails, it's not critical, so setting the node to initialized before this is fine.
readBindingTableEntries();
});
}
void ZigbeeNode::handleDataIndication(const Zigbee::ApsdeDataIndication &indication)
{
if (indication.lqi != m_lqi) {
m_lqi = indication.lqi;
emit lqiChanged(m_lqi);
}
// Data received from this node, it is reachable for sure
setReachable(true);
// Update the UTC timestamp of last seen for reachable verification
if (m_lastSeen != QDateTime::currentDateTimeUtc()) {
m_lastSeen = QDateTime::currentDateTimeUtc();
emit lastSeenChanged(m_lastSeen);
}
// Check if this indocation is related to any pending reply
if (indication.profileId == Zigbee::ZigbeeProfileDevice) {
deviceObject()->processApsDataIndication(indication);
return;
}
// Let the clusters handle this indication
handleZigbeeClusterLibraryIndication(indication);
}
void ZigbeeNode::handleZigbeeClusterLibraryIndication(const Zigbee::ApsdeDataIndication &indication)
{
qCDebug(dcZigbeeNode()) << "Processing ZCL indication" << indication;
// Get the endpoint
ZigbeeNodeEndpoint *endpoint = getEndpoint(indication.sourceEndpoint);
if (!endpoint) {
qCDebug(dcZigbeeNetwork()) << "Received a ZCL indication for an unrecognized endpoint. There is no such endpoint on" << this << "Creating the uninitialized endpoint";
// Create the uninitialized endpoint for now and fetch information later
endpoint = new ZigbeeNodeEndpoint(m_network, this, indication.sourceEndpoint, this);
endpoint->setProfile(static_cast(indication.profileId));
// Note: the endpoint is not initializd yet, but keep it anyways
setupEndpointInternal(endpoint);
m_endpoints.append(endpoint);
}
endpoint->handleZigbeeClusterLibraryIndication(indication);
}
QDebug operator<<(QDebug debug, ZigbeeNode *node)
{
QDebugStateSaver saver(debug);
debug.nospace().noquote() << "ZigbeeNode(" << ZigbeeUtils::convertUint16ToHexString(node->shortAddress());
debug.nospace().noquote() << ", " << node->extendedAddress().toString();
if (!node->manufacturerName().isEmpty())
debug.nospace().noquote() << ", " << node->manufacturerName() << " (" << ZigbeeUtils::convertUint16ToHexString(node->nodeDescriptor().manufacturerCode) << ")";
else
debug.nospace().noquote() << ", " << ZigbeeUtils::convertUint16ToHexString(node->nodeDescriptor().manufacturerCode);
if (!node->modelName().isEmpty())
debug.nospace().noquote() << ", " << node->modelName();
switch (node->nodeDescriptor().nodeType) {
case ZigbeeDeviceProfile::NodeTypeCoordinator:
debug.nospace().noquote() << ", Coordinator";
break;
case ZigbeeDeviceProfile::NodeTypeRouter:
debug.nospace().noquote() << ", Router";
break;
case ZigbeeDeviceProfile::NodeTypeEndDevice:
debug.nospace().noquote() << ", End device";
break;
}
debug.nospace().noquote() << ", RxOn:" << node->macCapabilities().receiverOnWhenIdle;
debug.nospace().noquote() << ")";
return debug;
}