// 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 "zdo/zigbeedeviceprofile.h" #include "zigbeenetworkti.h" #include "loggingcategory.h" #include "zigbeeutils.h" #include "zigbeenetworkdatabase.h" #include #include ZigbeeNetworkTi::ZigbeeNetworkTi(const QUuid &networkUuid, QObject *parent) : ZigbeeNetwork(networkUuid, parent) { m_controller = new ZigbeeBridgeControllerTi(this); connect(m_controller, &ZigbeeBridgeControllerTi::availableChanged, this, &ZigbeeNetworkTi::onControllerAvailableChanged); connect(m_controller, &ZigbeeBridgeControllerTi::controllerStateChanged, this, &ZigbeeNetworkTi::onControllerStateChanged); connect(m_controller, &ZigbeeBridgeControllerTi::firmwareVersionChanged, this, &ZigbeeNetworkTi::firmwareVersionChanged); connect(m_controller, &ZigbeeBridgeControllerTi::permitJoinStateChanged, this, &ZigbeeNetworkTi::onPermitJoinStateChanged); connect(m_controller, &ZigbeeBridgeControllerTi::deviceIndication, this, &ZigbeeNetworkTi::onDeviceIndication); connect(m_controller, &ZigbeeBridgeControllerTi::apsDataIndicationReceived, this, &ZigbeeNetworkTi::onApsDataIndicationReceived); connect(m_controller, &ZigbeeBridgeControllerTi::apsDataConfirmReceived, this, &ZigbeeNetworkTi::onApsDataConfirmReceived); connect(m_controller, &ZigbeeBridgeControllerTi::nodeLeft, this, &ZigbeeNetworkTi::onNodeLeaveIndication); } ZigbeeBridgeController *ZigbeeNetworkTi::bridgeController() const { if (!m_controller) return nullptr; return m_controller; } Zigbee::ZigbeeBackendType ZigbeeNetworkTi::backendType() const { return Zigbee::ZigbeeBackendTypeTi; } ZigbeeNetworkReply *ZigbeeNetworkTi::sendRequest(const ZigbeeNetworkRequest &request) { ZigbeeNetworkReply *reply = createNetworkReply(request); // Finish the reply right away if the network is offline if (!m_controller->available() || state() == ZigbeeNetwork::StateOffline || state() == ZigbeeNetwork::StateStopping) { finishNetworkReply(reply, ZigbeeNetworkReply::ErrorNetworkOffline); return reply; } if (state() == ZigbeeNetwork::StateStarting) { m_requestQueue.append(reply); return reply; } ZigbeeInterfaceTiReply *interfaceReply = m_controller->requestSendRequest(request); connect(interfaceReply, &ZigbeeInterfaceTiReply::finished, reply, [this, reply, interfaceReply](){ if (interfaceReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeController()) << "Could not send request to controller." << interfaceReply->statusCode(); finishNetworkReply(reply, ZigbeeNetworkReply::ErrorInterfaceError); return; } finishNetworkReply(reply, ZigbeeNetworkReply::ErrorNoError); }); return reply; } void ZigbeeNetworkTi::setPermitJoining(quint8 duration, quint16 address) { if (duration > 0) { qCDebug(dcZigbeeNetwork()) << "Set permit join for" << duration << "s on" << ZigbeeUtils::convertUint16ToHexString(address); } else { qCDebug(dcZigbeeNetwork()) << "Disable permit join on"<< ZigbeeUtils::convertUint16ToHexString(address); } ZigbeeInterfaceTiReply *requestPermitJoinReply = m_controller->requestPermitJoin(duration, address); connect(requestPermitJoinReply, &ZigbeeInterfaceTiReply::finished, this, [=](){ if (requestPermitJoinReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeNetwork()) << "Could not set permit join to" << duration << ZigbeeUtils::convertUint16ToHexString(address) << requestPermitJoinReply->statusCode(); return; } qCDebug(dcZigbeeNetwork()) << "Permit join request finished successfully:" << duration; setPermitJoiningState(true, duration); // Opening the green power network too // Todo: This should probably be somewhere else, but not yet sure how other backeds deal with this QByteArray payload; QDataStream stream(&payload, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::LittleEndian); stream << static_cast(0x0b); // options stream << static_cast(duration); // Build ZCL frame control ZigbeeClusterLibrary::FrameControl frameControl; frameControl.frameType = ZigbeeClusterLibrary::FrameTypeClusterSpecific; frameControl.manufacturerSpecific = false; frameControl.direction = ZigbeeClusterLibrary::DirectionServerToClient; frameControl.disableDefaultResponse = true; // Build ZCL header ZigbeeClusterLibrary::Header header; header.frameControl = frameControl; header.command = 0x02;// TODO: ZigbeeClusterGreenPower::ClusterCommandCommissioningMode; header.transactionSequenceNumber = generateSequenceNumber(); // Build ZCL frame ZigbeeClusterLibrary::Frame frame; frame.header = header; frame.payload = payload; ZigbeeNetworkRequest request; request.setRequestId(generateSequenceNumber()); request.setDestinationAddressMode(Zigbee::DestinationAddressModeShortAddress); request.setDestinationShortAddress(0xFFFD); request.setDestinationEndpoint(242); // Green Power Endpoint request.setProfileId(Zigbee::ZigbeeProfileDevice); // ZDP request.setClusterId(0x21); request.setTxOptions(static_cast(0x00)); // FIXME: There should be TxOptionsNone I guess... request.setSourceEndpoint(242); // Green Power Endpoint request.setRadius(30); // FIXME: There should be a more clever way to figure out the radius request.setAsdu(ZigbeeClusterLibrary::buildFrame(frame)); m_controller->requestSendRequest(request); }); } void ZigbeeNetworkTi::initController() { qCDebug(dcZigbeeNetwork()) << "Initializing controller"; setState(StateStarting); setError(ErrorNoError); ZigbeeInterfaceTiReply *initReply = m_controller->init(); connect(initReply, &ZigbeeInterfaceTiReply::finished, this, [=](){ if (initReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeNetwork()) << "Error initializing controller"; setState(StateUninitialized); setError(ErrorZigbeeError); return; } }); } void ZigbeeNetworkTi::commissionController() { qCDebug(dcZigbeeNetwork()) << "Commissioning controller"; quint16 panId = ZigbeeUtils::generateRandomPanId(); ZigbeeInterfaceTiReply *commissionReply = m_controller->commission(Ti::DeviceLogicalTypeCoordinator, panId, channelMask()); connect(commissionReply, &ZigbeeInterfaceTiReply::finished, this, [=](){ if (commissionReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeNetwork()) << "Error commissioning controller."; setState(StateUninitialized); setError(ErrorZigbeeError); return; } qCDebug(dcZigbeeNetwork()) << "Controller commissioned"; startControllerNetwork(); setMacAddress(m_controller->networkConfiguration().ieeeAddress); }); } void ZigbeeNetworkTi::startControllerNetwork() { setState(StateStarting); qCDebug(dcZigbeeNetwork()) << "Starting network on controller..."; ZigbeeInterfaceTiReply *startReply = m_controller->start(); connect(startReply, &ZigbeeInterfaceTiReply::finished, this, [=]() { if (startReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeNetwork()) << "Error starting network."; setState(StateOffline); setError(ErrorZigbeeError); return; } }); } void ZigbeeNetworkTi::processGreenPowerFrame(const Zigbee::ApsdeDataIndication &indication) { ZigbeeClusterLibrary::Frame inputFrame = ZigbeeClusterLibrary::parseFrameData(indication.asdu); QDataStream inputStream(inputFrame.payload); inputStream.setByteOrder(QDataStream::LittleEndian); quint8 commandId, payloadSize; quint16 options; quint32 srcId, frameCounter; inputStream >> options >> srcId >> frameCounter >> commandId >> payloadSize; QByteArray commandFrame = inputFrame.payload.right(payloadSize); qCWarning(dcZigbeeNetwork()) << "Green Power frame:" << options << srcId << frameCounter << commandId << payloadSize << commandFrame.toHex(); if (commandId == 0xE0) { qCWarning(dcZigbeeNetwork()) << "Green power commissioning"; QDataStream commandStream(commandFrame); commandStream.setByteOrder(QDataStream::LittleEndian); quint8 deviceId, inputOptions, extendedOptions; QByteArray securityKey; quint32 keyMic; quint32 outgoingCounter; commandStream >> deviceId >> inputOptions >> extendedOptions; for (int i = 0; i < 16; i++) { quint8 byte; commandStream >> byte; securityKey.append(byte); } commandStream >> keyMic >> outgoingCounter; // Send commissioning reply QByteArray payload; QDataStream stream(&payload, QIODevice::WriteOnly); stream.setByteOrder(QDataStream::LittleEndian); ZigbeeDataType options = ZigbeeDataType(static_cast(0x00e548), Zigbee::Uint24); // options for (int i = 0; i < options.data().length(); i++) { stream << static_cast(options.data().at(i)); } stream << srcId; stream << static_cast(0x0b84); // Green Power group as configured during startup stream << deviceId; stream << outgoingCounter; payload.append(encryptSecurityKey(srcId, securityKey)); // Build ZCL frame control ZigbeeClusterLibrary::FrameControl frameControl; frameControl.frameType = ZigbeeClusterLibrary::FrameTypeClusterSpecific; frameControl.manufacturerSpecific = false; frameControl.direction = ZigbeeClusterLibrary::DirectionServerToClient; frameControl.disableDefaultResponse = true; // Build ZCL header ZigbeeClusterLibrary::Header header; header.frameControl = frameControl; header.command = 0x01; // TODO: ZigbeeClusterGreenPower::ClusterCommandPairing; header.transactionSequenceNumber = generateSequenceNumber() - 1; // Build ZCL frame ZigbeeClusterLibrary::Frame frame; frame.header = header; frame.payload = payload; ZigbeeNetworkRequest request; request.setRequestId(generateSequenceNumber() + 1); request.setDestinationAddressMode(Zigbee::DestinationAddressModeShortAddress); request.setDestinationShortAddress(0xFFFD); request.setDestinationEndpoint(242); // Green Power Endpoint request.setProfileId(Zigbee::ZigbeeProfileDevice); // ZDP request.setClusterId(0x21); request.setTxOptions(static_cast(0x00)); // FIXME: There should be TxOptionsNone I guess... request.setSourceEndpoint(242); // Green Power Endpoint request.setRadius(30); // FIXME: There should be a more clever way to figure out the radius request.setAsdu(ZigbeeClusterLibrary::buildFrame(frame)); m_controller->requestSendRequest(request); QStringList l; for (int i = 0; i < request.asdu().length(); i++) { l.append(QString::number(static_cast(request.asdu().at(i)))); } // TODO: create the GreenPower Node here once GreenPower support is added properly // emit greenPowerDeviceJoined(srcId, deviceId, indication.sourceIeeeAddress()) } } QByteArray ZigbeeNetworkTi::encryptSecurityKey(quint32 sourceId, const QByteArray &securityKey) { #if (QCA_VERSION >= QCA_VERSION_CHECK(2, 2, 0)) QByteArray sourceIdArray; sourceIdArray.append(static_cast(sourceId & 0x000000ff)); sourceIdArray.append(static_cast((sourceId & 0x0000ff00) >> 8)); sourceIdArray.append(static_cast((sourceId & 0x00ff0000) >> 16)); sourceIdArray.append(static_cast((sourceId & 0xff000000) >> 24)); QByteArray nonce(13, Qt::Uninitialized); for (int i = 0; i < 3; i++) { for (int j = 0; j < 4; j++) { nonce[4 * i + j] = sourceIdArray.at(j); } } nonce[12] = 0x05; QByteArray zigbeeLinkKey = securityConfiguration().globalTrustCenterLinkKey().toByteArray(); QCA::Initializer init; QCA::Cipher cipher("aes128", QCA::Cipher::CCM, QCA::Cipher::DefaultPadding, QCA::Encode, zigbeeLinkKey, nonce); QByteArray encrypted = cipher.update(securityKey).toByteArray(); encrypted.append(cipher.final().toByteArray()); return encrypted; #else Q_UNUSED(sourceId) qCWarning(dcZigbeeNetwork()) << "Greenpower encryption requires AES-128-CCM (requires qca-qt5-2 >= 2.2.0)"; return securityKey; #endif } void ZigbeeNetworkTi::onControllerAvailableChanged(bool available) { if (!available) { qCWarning(dcZigbeeNetwork()) << "Hardware controller is not available any more."; setError(ErrorHardwareUnavailable); setPermitJoiningState(false); setState(StateOffline); setError(ErrorHardwareUnavailable); } else { setPermitJoiningState(false); setState(StateOffline); setError(ErrorNoError); qCDebug(dcZigbeeNetwork()) << "Hardware controller is now available."; initController(); } } void ZigbeeNetworkTi::onControllerStateChanged(ZigbeeBridgeControllerTi::ControllerState state) { switch (state) { case ZigbeeBridgeControllerTi::ControllerStateDown: setState(StateOffline); break; case ZigbeeBridgeControllerTi::ControllerStateInitialized: // The mac address of the controller will be stored locally when the coordinator node replies // to the node descriptor request which includes the IEEE address. // If we don't know the mac address yet, it means that we've never had the zigbee network up and running // => start the commissioning procedure, else, directly start the network // Note: if the user messes around with the stick by provisioning stuff with another instance/software, it // will not be detected currently and the "wrong" network starts up. qCDebug(dcZigbeeNetwork()) << "Controller initialized"; qCDebug(dcZigbeeNetwork()) << "Stored MAC address:" << macAddress() << "Controller MAC address:" << m_controller->networkConfiguration().ieeeAddress; if (macAddress() != ZigbeeAddress("00:00:00:00:00:00:00:00") && m_controller->networkConfiguration().ieeeAddress != macAddress()) { qCWarning(dcZigbeeNetwork()) << "The controller MAC address changed. Please connect the original controller or create a new network."; setState(StateUninitialized); setError(ErrorHardwareModuleChanged); stopNetwork(); return; } if (macAddress() == ZigbeeAddress("00:00:00:00:00:00:00")) { // Controller network is not commissioned yet qCDebug(dcZigbeeNetwork()) << "Controller is not comissioned yet. Comissioning now..."; commissionController(); return; } qCDebug(dcZigbeeNetwork()) << "Controller is ready and commissioned."; startControllerNetwork(); break; case ZigbeeBridgeControllerTi::ControllerStateRunning: { qCDebug(dcZigbeeNetwork()) << "Controller network running. Registering endpoints on controller.."; setPanId(m_controller->networkConfiguration().panId); setExtendedPanId(m_controller->networkConfiguration().extendedPanId); setChannel(m_controller->networkConfiguration().currentChannel); // TODO: This should be public API of libnymea-zigbee so that the application layer (e.g. nymea-plugins) // can register the endpoints it needs for the particular application/device. // Fow now we're registering HomeAutomation, LightLink and GreenPower endpoints. m_controller->registerEndpoint(1, Zigbee::ZigbeeProfileHomeAutomation, 5, 0, {ZigbeeClusterLibrary::ClusterIdOtaUpgrade}); m_controller->registerEndpoint(12, Zigbee::ZigbeeProfileLightLink, 5, 0); // The Green Power endpoing is a bit special, it also needs to be added to a group ZigbeeInterfaceTiReply *registerLLEndpointReply = m_controller->registerEndpoint(242, Zigbee::ZigbeeProfileGreenPower, 5, 0); connect(registerLLEndpointReply, &ZigbeeInterfaceTiReply::finished, this, [=]() { if (registerLLEndpointReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeNetwork()) << "Error registering GreenPower endpoint."; setState(StateOffline); return; } qCDebug(dcZigbeeNetwork()) << "Registered GreenPower endpoint on coordinator node."; ZigbeeInterfaceTiReply *addGEndpointGroupReply = m_controller->addEndpointToGroup(242, 0x0b84); connect(addGEndpointGroupReply, &ZigbeeInterfaceTiReply::finished, this, [=]() { if (addGEndpointGroupReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeNetwork()) << "Error adding GreenPower endpoint to group."; setState(StateOffline); return; } // Now we're done. If this is a first start (no coordinator node loaded from configs) we'll add // ourselves now and start the inspection. if (!m_coordinatorNode) { qCDebug(dcZigbeeNetwork()) << "Initializing coordinator node:" << m_controller->networkConfiguration(); ZigbeeNode *coordinatorNode = createNode(m_controller->networkConfiguration().shortAddress, m_controller->networkConfiguration().ieeeAddress, this); m_coordinatorNode = coordinatorNode; addUnitializedNode(coordinatorNode); } ZigbeeInterfaceTiReply *ledReply = m_controller->setLed(false); connect(ledReply, &ZigbeeInterfaceTiReply::finished, this, [=]() { setState(StateRunning); // Introspecing ourselves on every start. Most of the times this wouldn't be needed, but if the above // endpoints are changed (e.g. on a future upgrade), we'll want to refresh. m_coordinatorNode->startInitialization(); connect(m_coordinatorNode, &ZigbeeNode::stateChanged, this, [=](ZigbeeNode::State state){ if (state == ZigbeeNode::StateInitialized) { setNodeInformation(m_coordinatorNode, "z-Stack", "", bridgeController()->firmwareVersion()); } }); while (!m_requestQueue.isEmpty()) { ZigbeeNetworkReply *reply = m_requestQueue.takeFirst(); ZigbeeInterfaceTiReply *interfaceReply = m_controller->requestSendRequest(reply->request()); connect(interfaceReply, &ZigbeeInterfaceTiReply::finished, reply, [this, reply, interfaceReply](){ if (interfaceReply->statusCode() != Ti::StatusCodeSuccess) { qCWarning(dcZigbeeController()) << "Could send request to controller." << interfaceReply->statusCode(); finishNetworkReply(reply, ZigbeeNetworkReply::ErrorInterfaceError); return; } finishNetworkReply(reply, ZigbeeNetworkReply::ErrorNoError); }); } }); }); }); break; } } } void ZigbeeNetworkTi::onPermitJoinStateChanged(quint8 duration) { setPermitJoiningState(duration > 0, duration); m_controller->setLed(duration > 0); } void ZigbeeNetworkTi::onDeviceIndication(quint16 shortAddress, const ZigbeeAddress &ieeeAddress, quint8 macCapabilities) { onDeviceAnnounced(shortAddress, ieeeAddress, macCapabilities); } void ZigbeeNetworkTi::onNodeLeaveIndication(const ZigbeeAddress &ieeeAddress, bool request, bool remove, bool rejoin) { qCDebug(dcZigbeeNetwork()) << "Received node leave indication" << ieeeAddress.toString() << "request:" << request << "remove:" << remove << "rejoining:" << rejoin; if (!hasNode(ieeeAddress)) { qCDebug(dcZigbeeNetwork()) << "Node left the network" << ieeeAddress.toString(); return; } ZigbeeNode *node = getZigbeeNode(ieeeAddress); qCDebug(dcZigbeeNetwork()) << node << "left the network"; removeNode(node); } void ZigbeeNetworkTi::onApsDataConfirmReceived(const Zigbee::ApsdeDataConfirm &confirm) { qCDebug(dcZigbeeNetwork()) << "Data confirm received:" << confirm; } void ZigbeeNetworkTi::onApsDataIndicationReceived(const Zigbee::ApsdeDataIndication &indication) { // If it's for the green power endpoint, we'll have to do a commissioning. // TODO: This should probably be in ZigbeeNode or ZigbeeDeviceObject, but not yet sure how other backends deal with it if (indication.destinationEndpoint == 242) { processGreenPowerFrame(indication); } // Check if this indocation is related to any pending reply if (indication.profileId == Zigbee::ZigbeeProfileDevice) { handleZigbeeDeviceProfileIndication(indication); return; } // Else let the node handle this indication handleZigbeeClusterLibraryIndication(indication); } void ZigbeeNetworkTi::startNetwork() { loadNetwork(); if (!m_controller->enable(serialPortName(), serialBaudrate())) { setPermitJoiningState(false); setState(StateOffline); setError(ErrorHardwareUnavailable); return; } setPermitJoiningState(false); } void ZigbeeNetworkTi::stopNetwork() { setState(StateStopping); m_controller->disable(); setState(StateOffline); } void ZigbeeNetworkTi::reset() { qCDebug(dcZigbeeNetwork()) << "Resetting controller."; m_controller->reset(); } void ZigbeeNetworkTi::factoryResetNetwork() { qCDebug(dcZigbeeNetwork()) << "Factory resetting network and forget all information. This cannot be undone."; ZigbeeInterfaceTiReply *reply = m_controller->factoryReset(); connect(reply, &ZigbeeInterfaceTiReply::finished, this, [=](){ qCDebug(dcZigbeeNetwork()) << "Factory reset reply finished" << reply->statusCode(); m_controller->disable(); clearSettings(); setMacAddress(ZigbeeAddress("00:00:00:00:00:00")); setState(StateOffline); setError(ErrorNoError); qCDebug(dcZigbeeNetwork()) << "The factory reset is finished. Restarting with a fresh network."; startNetwork(); }); } void ZigbeeNetworkTi::destroyNetwork() { qCDebug(dcZigbeeNetwork()) << "Destroy network and delete the database"; m_controller->disable(); clearSettings(); }