From 56ed62b730f9248caddec73421ec7a7cb5ffe828 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Mon, 1 Feb 2021 00:20:53 +0100 Subject: [PATCH 1/2] Zigbee: Complete support for ZigBee Thermostats (generic) --- zigbeegeneric/README.md | 3 + .../integrationpluginzigbeegeneric.cpp | 318 +++++++++++------- .../integrationpluginzigbeegeneric.h | 4 + .../integrationpluginzigbeegeneric.json | 41 ++- .../integrationpluginzigbeegenericlights.cpp | 8 +- 5 files changed, 253 insertions(+), 121 deletions(-) diff --git a/zigbeegeneric/README.md b/zigbeegeneric/README.md index 0fb2067c..171ae337 100644 --- a/zigbeegeneric/README.md +++ b/zigbeegeneric/README.md @@ -18,6 +18,9 @@ Simple on/off power sockets. > Most power sockets have a pairing button. Once the device is powered, it can be resetted / paired by clicking the button multiple times of keeping it pressed for several seconds. +### Radiator thermostats + +Radiator thermostats that follow the ZigBee specification. ## Requirements diff --git a/zigbeegeneric/integrationpluginzigbeegeneric.cpp b/zigbeegeneric/integrationpluginzigbeegeneric.cpp index 740ec69a..6c3ef5b0 100644 --- a/zigbeegeneric/integrationpluginzigbeegeneric.cpp +++ b/zigbeegeneric/integrationpluginzigbeegeneric.cpp @@ -37,6 +37,16 @@ #include +QHash batteryLevelStateTypeIds = { + {thermostatThingClassId, thermostatBatteryLevelStateTypeId}, + {doorLockThingClassId, doorLockBatteryLevelStateTypeId} +}; + +QHash batteryCriticalStateTypeIds = { + {thermostatThingClassId, thermostatBatteryCriticalStateTypeId}, + {doorLockThingClassId, doorLockBatteryCriticalStateTypeId} +}; + IntegrationPluginZigbeeGeneric::IntegrationPluginZigbeeGeneric() { m_ieeeAddressParamTypeIds[thermostatThingClassId] = thermostatThingIeeeAddressParamTypeId; @@ -86,8 +96,9 @@ bool IntegrationPluginZigbeeGeneric::handleNode(ZigbeeNode *node, const QUuid &n // Check thermostat if (endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceThermostat) { - qCDebug(dcZigbeeGeneric()) << "Handeling thermostat endpoint for" << node << endpoint; + qCDebug(dcZigbeeGeneric()) << "Handling thermostat endpoint for" << node << endpoint; createThing(thermostatThingClassId, networkUuid, node, endpoint); + initThermostat(node, endpoint); handled = true; } @@ -100,7 +111,7 @@ bool IntegrationPluginZigbeeGeneric::handleNode(ZigbeeNode *node, const QUuid &n // Simple on/off device if (endpoint->hasInputCluster(ZigbeeClusterLibrary::ClusterIdOnOff)) { // FIXME: create powersocket with metering for SmartPlug device ID - qCDebug(dcZigbeeGeneric()) << "Handeling power socket endpoint for" << node << endpoint; + qCDebug(dcZigbeeGeneric()) << "Handling power socket endpoint for" << node << endpoint; createThing(powerSocketThingClassId, networkUuid, node, endpoint); initSimplePowerSocket(node, endpoint); handled = true; @@ -113,7 +124,7 @@ bool IntegrationPluginZigbeeGeneric::handleNode(ZigbeeNode *node, const QUuid &n !endpoint->hasInputCluster(ZigbeeClusterLibrary::ClusterIdDoorLock)) { qCWarning(dcZigbeeGeneric()) << "Endpoint claims to be a door lock, but the appropriate input clusters could not be found" << node << endpoint; } else { - qCDebug(dcZigbeeGeneric()) << "Handeling door lock endpoint for" << node << endpoint; + qCDebug(dcZigbeeGeneric()) << "Handling door lock endpoint for" << node << endpoint; createThing(doorLockThingClassId, networkUuid, node, endpoint); // Initialize bindings and cluster attributes initializeDoorLock(node, endpoint); @@ -185,49 +196,62 @@ void IntegrationPluginZigbeeGeneric::setupThing(ThingSetupInfo *info) // Set the version thing->setStateValue(m_versionStateTypeIds.value(thing->thingClassId()), endpoint->softwareBuildId()); + if (batteryLevelStateTypeIds.contains(thing->thingClassId())) { + connectToPowerConfigurationCluster(thing, endpoint); + } + // Type specific setup if (thing->thingClassId() == thermostatThingClassId) { ZigbeeClusterThermostat *thermostatCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdThermostat); if (!thermostatCluster) { qCWarning(dcZigbeeGeneric()) << "Failed to read thermostat cluster"; + info->finish(Thing::ThingErrorHardwareFailure); return; } - // thermostatCluster->attribute(ZigbeeClusterLibrary::ClusterIdThermostat); - // We need to read them from the lamp - ZigbeeClusterReply *reply = thermostatCluster->readAttributes({ZigbeeClusterThermostat::AttributeLocalTemperature, ZigbeeClusterThermostat::AttributeOccupiedHeatingSetpoint}); + // Read initial attribute values + ZigbeeClusterReply *reply = thermostatCluster->readAttributes({ZigbeeClusterThermostat::AttributeLocalTemperature, + ZigbeeClusterThermostat::AttributeOccupiedHeatingSetpoint, + ZigbeeClusterThermostat::AttributePIHeatingDemand, + ZigbeeClusterThermostat::AttributePICoolingDemand}); connect(reply, &ZigbeeClusterReply::finished, thing, [=](){ if (reply->error() != ZigbeeClusterReply::ErrorNoError) { - qCWarning(dcZigbeeGeneric()) << "Reading loacal temperature attribute finished with error" << reply->error(); + qCWarning(dcZigbeeGeneric()) << "Reading thermostat attributes finished with error" << reply->error(); return; } QList attributeStatusRecords = ZigbeeClusterLibrary::parseAttributeStatusRecords(reply->responseFrame().payload); - foreach (const ZigbeeClusterLibrary::ReadAttributeStatusRecord &record, attributeStatusRecords) { if (record.attributeId == ZigbeeClusterThermostat::AttributeLocalTemperature) { - bool valueOk = false; - quint16 localTemperature = record.dataType.toUInt16(&valueOk); - if (!valueOk) { - qCWarning(dcZigbeeGeneric()) << "Failed to read local temperature" << attributeStatusRecords; - return; - } - thing->setStateValue(thermostatTemperatureStateTypeId, localTemperature * 0.01); + thing->setStateValue(thermostatTemperatureStateTypeId, record.dataType.toInt16() * 0.01); } - if (record.attributeId == ZigbeeClusterThermostat::AttributeOccupiedHeatingSetpoint) { - bool valueOk = false; - quint16 targetTemperature = record.dataType.toUInt16(&valueOk); - if (!valueOk) { - qCWarning(dcZigbeeGeneric()) << "Failed to read local temperature" << attributeStatusRecords; - return; - } - thing->setStateValue(thermostatTargetTemperatureStateTypeId, targetTemperature * 0.01); + thing->setStateValue(thermostatTargetTemperatureStateTypeId, record.dataType.toInt16() * 0.01); + } + if (record.attributeId == ZigbeeClusterThermostat::AttributePIHeatingDemand) { + thing->setStateValue(thermostatHeatingOnStateTypeId, record.dataType.toUInt8() > 0); + } + if (record.attributeId == ZigbeeClusterThermostat::AttributePICoolingDemand) { + thing->setStateValue(thermostatCoolingOnStateTypeId, record.dataType.toUInt8() > 0); } } + }); - - + // Connect to attribute changes + connect(thermostatCluster, &ZigbeeClusterThermostat::attributeChanged, thing, [thing](const ZigbeeClusterAttribute &attribute){ + qCDebug(dcZigbeeGeneric()) << "Thermostat attribute changed" << thing->name() << attribute.id() << attribute.dataType(); + if (attribute.id() == ZigbeeClusterThermostat::AttributeOccupiedHeatingSetpoint) { + thing->setStateValue(thermostatTargetTemperatureStateTypeId, attribute.dataType().toUInt16() * 0.01); + } + if (attribute.id() == ZigbeeClusterThermostat::AttributeLocalTemperature) { + thing->setStateValue(thermostatTemperatureStateTypeId, attribute.dataType().toUInt16() * 0.01); + } + if (attribute.id() == ZigbeeClusterThermostat::AttributePIHeatingDemand) { + thing->setStateValue(thermostatHeatingOnStateTypeId, attribute.dataType().toUInt8() > 0); + } + if (attribute.id() == ZigbeeClusterThermostat::AttributePICoolingDemand) { + thing->setStateValue(thermostatCoolingOnStateTypeId, attribute.dataType().toUInt8() > 0); + } }); } @@ -237,7 +261,6 @@ void IntegrationPluginZigbeeGeneric::setupThing(ThingSetupInfo *info) if (onOffCluster->hasAttribute(ZigbeeClusterOnOff::AttributeOnOff)) { thing->setStateValue(powerSocketPowerStateTypeId, onOffCluster->power()); } - connect(onOffCluster, &ZigbeeClusterOnOff::powerChanged, thing, [thing](bool power){ qCDebug(dcZigbeeGeneric()) << thing << "power changed" << power; thing->setStateValue(powerSocketPowerStateTypeId, power); @@ -261,24 +284,6 @@ void IntegrationPluginZigbeeGeneric::setupThing(ThingSetupInfo *info) if (thing->thingClassId() == doorLockThingClassId) { - // Get battery level changes - ZigbeeClusterPowerConfiguration *powerCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdPowerConfiguration); - if (!powerCluster) { - qCWarning(dcZigbeeGeneric()) << "Could not find power configuration cluster on" << thing << endpoint; - } else { - // Only set the initial state if the attribute already exists - if (powerCluster->hasAttribute(ZigbeeClusterPowerConfiguration::AttributeBatteryPercentageRemaining)) { - thing->setStateValue(doorLockBatteryLevelStateTypeId, powerCluster->batteryPercentage()); - thing->setStateValue(doorLockBatteryCriticalStateTypeId, (powerCluster->batteryPercentage() < 10.0)); - } - - connect(powerCluster, &ZigbeeClusterPowerConfiguration::batteryPercentageChanged, thing, [=](double percentage){ - qCDebug(dcZigbeeGeneric()) << "Battery percentage changed" << percentage << "%" << thing; - thing->setStateValue(doorLockBatteryLevelStateTypeId, percentage); - thing->setStateValue(doorLockBatteryCriticalStateTypeId, (percentage < 10.0)); - }); - } - // Get door state changes ZigbeeClusterDoorLock *doorLockCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdDoorLock); if (!doorLockCluster) { @@ -322,6 +327,39 @@ void IntegrationPluginZigbeeGeneric::executeAction(ThingActionInfo *info) return; } + if (thing->thingClassId() == thermostatThingClassId) { + if (info->action().actionTypeId() == thermostatTargetTemperatureActionTypeId) { + ZigbeeClusterThermostat *thermostatCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdThermostat); + if (!thermostatCluster) { + qCWarning(dcZigbeeGeneric()) << "Thermostat cluster not found on thing" << thing->name(); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + qint16 targetTemp = qRound(info->action().paramValue(thermostatTargetTemperatureStateTypeId).toDouble() * 10) * 10; + // TODO: The following should probably move into libnymea-zibee prividing a + // thermostatCluster->writeOccupiedHeatingSetpoint(targetTemp); + ZigbeeDataType dataType(targetTemp); + QList attributes; + ZigbeeClusterLibrary::WriteAttributeRecord occupiedHeatingSetpointAttribute; + occupiedHeatingSetpointAttribute.attributeId = ZigbeeClusterThermostat::AttributeOccupiedHeatingSetpoint; + occupiedHeatingSetpointAttribute.dataType = dataType.dataType(); + occupiedHeatingSetpointAttribute.data = dataType.data(); + attributes.append(occupiedHeatingSetpointAttribute); + qCDebug(dcZigbeeGeneric()) << "Writing target temp" << targetTemp << occupiedHeatingSetpointAttribute.data; + ZigbeeClusterReply *reply = thermostatCluster->writeAttributes(attributes); + connect(reply, &ZigbeeClusterReply::finished, info, [info, reply](){ + qCDebug(dcZigbeeGeneric()) << "Writing attributes finished:" << reply->error(); + if (reply->error() != ZigbeeClusterReply::ErrorNoError) { + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + info->thing()->setStateValue(thermostatTargetTemperatureStateTypeId, info->action().paramValue(thermostatTargetTemperatureActionTargetTemperatureParamTypeId)); + info->finish(Thing::ThingErrorNoError); + }); + return; + } + } + if (thing->thingClassId() == powerSocketThingClassId) { if (info->action().actionTypeId() == powerSocketAlertActionTypeId) { ZigbeeClusterIdentify *identifyCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdIdentify); @@ -474,82 +512,132 @@ void IntegrationPluginZigbeeGeneric::initSimplePowerSocket(ZigbeeNode *node, Zig void IntegrationPluginZigbeeGeneric::initializeDoorLock(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint) { - qCDebug(dcZigbeeGeneric()) << "Read power configuration cluster attributes" << node; - ZigbeeClusterReply *readAttributeReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdPowerConfiguration)->readAttributes({ZigbeeClusterPowerConfiguration::AttributeBatteryPercentageRemaining}); - connect(readAttributeReply, &ZigbeeClusterReply::finished, node, [=](){ - if (readAttributeReply->error() != ZigbeeClusterReply::ErrorNoError) { - qCWarning(dcZigbeeGeneric()) << "Failed to read power configuration cluster attributes" << readAttributeReply->error(); + bindPowerConfigurationCluster(node, endpoint); + + qCDebug(dcZigbeeGeneric()) << "Binding door lock cluster "; + ZigbeeDeviceObjectReply * zdoReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdDoorLock, hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01); + connect(zdoReply, &ZigbeeDeviceObjectReply::finished, node, [=](){ + if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) { + qCWarning(dcZigbeeGeneric()) << "Failed to door lock cluster to coordinator" << zdoReply->error(); } else { - qCDebug(dcZigbeeGeneric()) << "Read power configuration cluster attributes finished successfully"; + qCDebug(dcZigbeeGeneric()) << "Bind door lock cluster to coordinator finished successfully"; } - // Bind the cluster to the coordinator - qCDebug(dcZigbeeGeneric()) << "Bind power configuration cluster to coordinator IEEE address"; - ZigbeeDeviceObjectReply * zdoReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdPowerConfiguration, hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01); - connect(zdoReply, &ZigbeeDeviceObjectReply::finished, node, [=](){ - if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) { - qCWarning(dcZigbeeGeneric()) << "Failed to bind power cluster to coordinator" << zdoReply->error(); + // Configure attribute reporting for lock state + ZigbeeClusterLibrary::AttributeReportingConfiguration reportingConfig; + reportingConfig.attributeId = ZigbeeClusterDoorLock::AttributeLockState; + reportingConfig.dataType = Zigbee::Enum8; + reportingConfig.minReportingInterval = 60; + reportingConfig.maxReportingInterval = 120; + reportingConfig.reportableChange = ZigbeeDataType(static_cast(1)).data(); + + qCDebug(dcZigbeeGeneric()) << "Configure attribute reporting for door lock cluster to coordinator"; + ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdDoorLock)->configureReporting({reportingConfig}); + connect(reportingReply, &ZigbeeClusterReply::finished, this, [=](){ + if (reportingReply->error() != ZigbeeClusterReply::ErrorNoError) { + qCWarning(dcZigbeeGeneric()) << "Failed to door lock cluster attribute reporting" << reportingReply->error(); } else { - qCDebug(dcZigbeeGeneric()) << "Bind power configuration cluster to coordinator finished successfully"; + qCDebug(dcZigbeeGeneric()) << "Attribute reporting configuration finished for door lock cluster" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(reportingReply->responseFrame().payload); } - - // Configure attribute rporting for battery remaining (0.5 % changes = 1) - ZigbeeClusterLibrary::AttributeReportingConfiguration reportingConfig; - reportingConfig.attributeId = ZigbeeClusterPowerConfiguration::AttributeBatteryPercentageRemaining; - reportingConfig.dataType = Zigbee::Uint8; - reportingConfig.minReportingInterval = 60; // for production use 300; - reportingConfig.maxReportingInterval = 120; // for production use 2700; - reportingConfig.reportableChange = ZigbeeDataType(static_cast(1)).data(); - - qCDebug(dcZigbeeGeneric()) << "Configure attribute reporting for power configuration cluster to coordinator"; - ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdPowerConfiguration)->configureReporting({reportingConfig}); - connect(reportingReply, &ZigbeeClusterReply::finished, this, [=](){ - if (reportingReply->error() != ZigbeeClusterReply::ErrorNoError) { - qCWarning(dcZigbeeGeneric()) << "Failed to configure power cluster attribute reporting" << reportingReply->error(); - } else { - qCDebug(dcZigbeeGeneric()) << "Attribute reporting configuration finished for power cluster" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(reportingReply->responseFrame().payload); - } - - // Configure door lock attribute reporting and read initial values - qCDebug(dcZigbeeGeneric()) << "Read door lock cluster attributes" << node; - ZigbeeClusterReply *readAttributeReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdDoorLock)->readAttributes({ZigbeeClusterDoorLock::AttributeDoorState, ZigbeeClusterDoorLock::AttributeLockType}); - connect(readAttributeReply, &ZigbeeClusterReply::finished, node, [=](){ - if (readAttributeReply->error() != ZigbeeClusterReply::ErrorNoError) { - qCWarning(dcZigbeeGeneric()) << "Failed to read door lock attributes" << readAttributeReply->error(); - } else { - qCDebug(dcZigbeeGeneric()) << "Read door lock cluster attributes finished successfully"; - } - - // Bind the cluster to the coordinator - qCDebug(dcZigbeeGeneric()) << "Bind door lock cluster to coordinator IEEE address"; - ZigbeeDeviceObjectReply * zdoReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdDoorLock, hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01); - connect(zdoReply, &ZigbeeDeviceObjectReply::finished, node, [=](){ - if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) { - qCWarning(dcZigbeeGeneric()) << "Failed to door lock cluster to coordinator" << zdoReply->error(); - } else { - qCDebug(dcZigbeeGeneric()) << "Bind door lock cluster to coordinator finished successfully"; - } - - // Configure attribute reporting for lock state - ZigbeeClusterLibrary::AttributeReportingConfiguration reportingConfig; - reportingConfig.attributeId = ZigbeeClusterDoorLock::AttributeLockState; - reportingConfig.dataType = Zigbee::Enum8; - reportingConfig.minReportingInterval = 60; - reportingConfig.maxReportingInterval = 120; - reportingConfig.reportableChange = ZigbeeDataType(static_cast(1)).data(); - - qCDebug(dcZigbeeGeneric()) << "Configure attribute reporting for door lock cluster to coordinator"; - ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdDoorLock)->configureReporting({reportingConfig}); - connect(reportingReply, &ZigbeeClusterReply::finished, this, [=](){ - if (reportingReply->error() != ZigbeeClusterReply::ErrorNoError) { - qCWarning(dcZigbeeGeneric()) << "Failed to door lock cluster attribute reporting" << reportingReply->error(); - } else { - qCDebug(dcZigbeeGeneric()) << "Attribute reporting configuration finished for door lock cluster" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(reportingReply->responseFrame().payload); - } - }); - }); - }); - }); }); }); } + +void IntegrationPluginZigbeeGeneric::initThermostat(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint) +{ + bindPowerConfigurationCluster(node, endpoint); + + qCDebug(dcZigbeeGeneric()) << "Binding thermostat custer"; + ZigbeeDeviceObjectReply *bindThermostatReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdThermostat, + hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01); + connect(bindThermostatReply, &ZigbeeDeviceObjectReply::finished, node, [=](){ + if (bindThermostatReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) { + qCWarning(dcZigbeeGeneric()) << "Failed to bind thermostat cluster" << bindThermostatReply->error(); + } else { + qCDebug(dcZigbeeGeneric()) << "Binding thermostat cluster finished successfully"; + } + + ZigbeeClusterLibrary::AttributeReportingConfiguration reportingOccupiedHeatingSetpointConfig; + reportingOccupiedHeatingSetpointConfig.attributeId = ZigbeeClusterThermostat::AttributeOccupiedHeatingSetpoint; + reportingOccupiedHeatingSetpointConfig.dataType = Zigbee::Int16; + reportingOccupiedHeatingSetpointConfig.minReportingInterval = 300; + reportingOccupiedHeatingSetpointConfig.maxReportingInterval = 2700; + reportingOccupiedHeatingSetpointConfig.reportableChange = ZigbeeDataType(static_cast(1)).data(); + + ZigbeeClusterLibrary::AttributeReportingConfiguration reportingBatteryPercentageConfig; + reportingBatteryPercentageConfig.attributeId = ZigbeeClusterPowerConfiguration::AttributeBatteryPercentageRemaining; + reportingBatteryPercentageConfig.dataType = Zigbee::Uint8; + reportingBatteryPercentageConfig.minReportingInterval = 300; + reportingBatteryPercentageConfig.maxReportingInterval = 2700; + reportingBatteryPercentageConfig.reportableChange = ZigbeeDataType(static_cast(1)).data(); + + qCDebug(dcZigbeeGeneric()) << "Configuring attribute reporting for thermostat cluster"; + ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdPowerConfiguration)->configureReporting({reportingOccupiedHeatingSetpointConfig, reportingBatteryPercentageConfig}); + connect(reportingReply, &ZigbeeClusterReply::finished, this, [=](){ + if (reportingReply->error() != ZigbeeClusterReply::ErrorNoError) { + qCWarning(dcZigbeeGeneric()) << "Failed to configure thermostat cluster attribute reporting" << reportingReply->error(); + } else { + qCDebug(dcZigbeeGeneric()) << "Attribute reporting configuration finished for thermostat cluster" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(reportingReply->responseFrame().payload); + } + }); + }); +} + +void IntegrationPluginZigbeeGeneric::bindPowerConfigurationCluster(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint) +{ + ZigbeeDeviceObjectReply *bindPowerReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdPowerConfiguration, + hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01); + connect(bindPowerReply, &ZigbeeDeviceObjectReply::finished, node, [=](){ + if (bindPowerReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) { + qCWarning(dcZigbeeGeneric()) << "Failed to bind power configuration cluster" << bindPowerReply->error(); + } else { + qCDebug(dcZigbeeGeneric()) << "Binding power configuration cluster finished successfully"; + } + + ZigbeeClusterLibrary::AttributeReportingConfiguration batteryPercentageConfig; + batteryPercentageConfig.attributeId = ZigbeeClusterPowerConfiguration::AttributeBatteryPercentageRemaining; + batteryPercentageConfig.dataType = Zigbee::Uint8; + batteryPercentageConfig.minReportingInterval = 60; // for production use 300; + batteryPercentageConfig.maxReportingInterval = 120; // for production use 2700; + batteryPercentageConfig.reportableChange = ZigbeeDataType(static_cast(1)).data(); + + qCDebug(dcZigbeeGeneric()) << "Configuring attribute reporting for power configuration cluster"; + ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdPowerConfiguration)->configureReporting({batteryPercentageConfig}); + connect(reportingReply, &ZigbeeClusterReply::finished, this, [=](){ + if (reportingReply->error() != ZigbeeClusterReply::ErrorNoError) { + qCWarning(dcZigbeeGeneric()) << "Failed to configure power configuration cluster attribute reporting" << reportingReply->error(); + } else { + qCDebug(dcZigbeeGeneric()) << "Attribute reporting configuration finished for power configuration cluster" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(reportingReply->responseFrame().payload); + } + }); + }); +} + +void IntegrationPluginZigbeeGeneric::connectToPowerConfigurationCluster(Thing *thing, ZigbeeNodeEndpoint *endpoint) +{ + // Get battery level changes + ZigbeeClusterPowerConfiguration *powerCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdPowerConfiguration); + if (powerCluster) { + // If the power cluster attributes are already available, read values now + if (powerCluster->hasAttribute(ZigbeeClusterPowerConfiguration::AttributeBatteryPercentageRemaining)) { + thing->setStateValue(batteryLevelStateTypeIds.value(thing->thingClassId()), powerCluster->batteryPercentage()); + thing->setStateValue(batteryCriticalStateTypeIds.value(thing->thingClassId()), (powerCluster->batteryPercentage() < 10.0)); + } + // Refresh power cluster attributes in any case + ZigbeeClusterReply *reply = powerCluster->readAttributes({ZigbeeClusterPowerConfiguration::AttributeBatteryPercentageRemaining}); + connect(reply, &ZigbeeClusterReply::finished, thing, [=](){ + if (reply->error() != ZigbeeClusterReply::ErrorNoError) { + qCWarning(dcZigbeeGeneric()) << "Reading power configuration cluster attributes finished with error" << reply->error(); + return; + } + thing->setStateValue(batteryLevelStateTypeIds.value(thing->thingClassId()), powerCluster->batteryPercentage()); + thing->setStateValue(batteryCriticalStateTypeIds.value(thing->thingClassId()), (powerCluster->batteryPercentage() < 10.0)); + }); + + // Connect to battery level changes + connect(powerCluster, &ZigbeeClusterPowerConfiguration::batteryPercentageChanged, thing, [=](double percentage){ + thing->setStateValue(batteryLevelStateTypeIds.value(thing->thingClassId()), percentage); + thing->setStateValue(batteryCriticalStateTypeIds.value(thing->thingClassId()), (percentage < 10.0)); + }); + } +} diff --git a/zigbeegeneric/integrationpluginzigbeegeneric.h b/zigbeegeneric/integrationpluginzigbeegeneric.h index cf90c3f6..51ccbc03 100644 --- a/zigbeegeneric/integrationpluginzigbeegeneric.h +++ b/zigbeegeneric/integrationpluginzigbeegeneric.h @@ -75,7 +75,11 @@ private: void initSimplePowerSocket(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint); void initializeDoorLock(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint); + void initThermostat(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint); + void bindPowerConfigurationCluster(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint); + + void connectToPowerConfigurationCluster(Thing *thing, ZigbeeNodeEndpoint *endpoint); }; #endif // INTEGRATIONPLUGINZIGBEEGENERIC_H diff --git a/zigbeegeneric/integrationpluginzigbeegeneric.json b/zigbeegeneric/integrationpluginzigbeegeneric.json index 5c9a13b3..f5767c2c 100644 --- a/zigbeegeneric/integrationpluginzigbeegeneric.json +++ b/zigbeegeneric/integrationpluginzigbeegeneric.json @@ -13,7 +13,7 @@ "name": "thermostat", "displayName": "Zigbee Thermostat", "createMethods": ["auto"], - "interfaces": ["thermostat", "temperaturesensor", "wirelessconnectable"], + "interfaces": ["thermostat", "temperaturesensor", "batterylevel", "wirelessconnectable"], "paramTypes": [ { "id": "f38746d8-0084-43a3-b645-3ec743ea5fbc", @@ -57,11 +57,13 @@ "name": "targetTemperature", "displayName": "Target temperature", "displayNameEvent": "Target temperature changed", + "displayNameAction": "Set target temperature", "type": "double", "unit": "DegreeCelsius", "minValue": 0, "maxValue": 50, - "defaultValue": 0 + "defaultValue": 0, + "writable": true }, { "id": "497af03a-a893-438c-aba2-1bf3ecfc66c5", @@ -72,6 +74,22 @@ "unit": "DegreeCelsius", "defaultValue": 0 }, + { + "id": "88d5dda1-b8f6-49f1-a55a-20415f9157b3", + "name": "heatingOn", + "displayName": "Heating on", + "displayNameEvent": "Heating turned on", + "type": "bool", + "defaultValue": false + }, + { + "id": "c77a3d3f-46c7-4026-b9ab-02ab88077cc4", + "name": "coolingOn", + "displayName": "Cooling on", + "displayNameEvent": "Cooling turned on", + "type": "bool", + "defaultValue": false + }, { "id": "e9fb2b10-96da-4b70-afda-46e948399af8", "name": "connected", @@ -100,6 +118,25 @@ "type": "QString", "cached": true, "defaultValue": "" + }, + { + "id": "3a733e99-850b-4c56-b058-d39850ef2fee", + "name": "batteryLevel", + "displayName": "Battery", + "displayNameEvent": "Battery changed", + "type": "int", + "unit": "Percentage", + "defaultValue": 0, + "minValue": 0, + "maxValue": 100 + }, + { + "id": "5cec4399-ba7c-4c78-8c30-c91040ad99cf", + "name": "batteryCritical", + "displayName": "Battery critical", + "displayNameEvent": "Battery critical changed", + "type": "bool", + "defaultValue": false } ] }, diff --git a/zigbeegenericlights/integrationpluginzigbeegenericlights.cpp b/zigbeegenericlights/integrationpluginzigbeegenericlights.cpp index e87c2a4d..d7ded0d8 100644 --- a/zigbeegenericlights/integrationpluginzigbeegenericlights.cpp +++ b/zigbeegenericlights/integrationpluginzigbeegenericlights.cpp @@ -97,7 +97,7 @@ bool IntegrationPluginZigbeeGenericLights::handleNode(ZigbeeNode *node, const QU (endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceOnOffLight)) { - qCDebug(dcZigbeeGenericLights()) << "Handeling on/off light for" << node << endpoint; + qCDebug(dcZigbeeGenericLights()) << "Handling on/off light for" << node << endpoint; createLightThing(onOffLightThingClassId, networkUuid, node, endpoint); handled = true; } @@ -107,7 +107,7 @@ bool IntegrationPluginZigbeeGenericLights::handleNode(ZigbeeNode *node, const QU (endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceDimmableLight)) { - qCDebug(dcZigbeeGenericLights()) << "Handeling dimmable light for" << node << endpoint; + qCDebug(dcZigbeeGenericLights()) << "Handling dimmable light for" << node << endpoint; createLightThing(dimmableLightThingClassId, networkUuid, node, endpoint); handled = true; } @@ -118,7 +118,7 @@ bool IntegrationPluginZigbeeGenericLights::handleNode(ZigbeeNode *node, const QU (endpoint->profile() == Zigbee::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceColourTemperatureLight)) { - qCDebug(dcZigbeeGenericLights()) << "Handeling color temperature light for" << node << endpoint; + qCDebug(dcZigbeeGenericLights()) << "Handling color temperature light for" << node << endpoint; createLightThing(colorTemperatureLightThingClassId, networkUuid, node, endpoint); handled = true; } @@ -127,7 +127,7 @@ bool IntegrationPluginZigbeeGenericLights::handleNode(ZigbeeNode *node, const QU (endpoint->profile() == Zigbee::ZigbeeProfileLightLink && endpoint->deviceId() == Zigbee::LightLinkDeviceExtendedColourLight) || (endpoint->profile() == Zigbee::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceExtendedColourLight)) { - qCDebug(dcZigbeeGenericLights()) << "Handeling color light for" << node << endpoint; + qCDebug(dcZigbeeGenericLights()) << "Handling color light for" << node << endpoint; createLightThing(colorLightThingClassId, networkUuid, node, endpoint); handled = true; } From 300793a97f05e43a9bbb88822b782ead46098bf7 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Sun, 14 Feb 2021 15:01:48 +0100 Subject: [PATCH 2/2] Update translations --- ...343be-9fd6-4015-9ff5-38542651c534-en_US.ts | 201 +++++++++++------- 1 file changed, 129 insertions(+), 72 deletions(-) diff --git a/zigbeegeneric/translations/6a4343be-9fd6-4015-9ff5-38542651c534-en_US.ts b/zigbeegeneric/translations/6a4343be-9fd6-4015-9ff5-38542651c534-en_US.ts index a3062199..6e494165 100644 --- a/zigbeegeneric/translations/6a4343be-9fd6-4015-9ff5-38542651c534-en_US.ts +++ b/zigbeegeneric/translations/6a4343be-9fd6-4015-9ff5-38542651c534-en_US.ts @@ -4,42 +4,60 @@ ZigbeeGeneric - - + + + + Battery The name of the ParamType (ThingClass: doorLock, EventType: batteryLevel, ID: {568e5bdd-47f3-4ccb-a1d8-ff3a5ea87ad8}) ---------- -The name of the StateType ({568e5bdd-47f3-4ccb-a1d8-ff3a5ea87ad8}) of ThingClass doorLock +The name of the StateType ({568e5bdd-47f3-4ccb-a1d8-ff3a5ea87ad8}) of ThingClass doorLock +---------- +The name of the ParamType (ThingClass: thermostat, EventType: batteryLevel, ID: {3a733e99-850b-4c56-b058-d39850ef2fee}) +---------- +The name of the StateType ({3a733e99-850b-4c56-b058-d39850ef2fee}) of ThingClass thermostat - + + Battery changed - The name of the EventType ({568e5bdd-47f3-4ccb-a1d8-ff3a5ea87ad8}) of ThingClass doorLock + The name of the EventType ({568e5bdd-47f3-4ccb-a1d8-ff3a5ea87ad8}) of ThingClass doorLock +---------- +The name of the EventType ({3a733e99-850b-4c56-b058-d39850ef2fee}) of ThingClass thermostat - - + + + + Battery critical The name of the ParamType (ThingClass: doorLock, EventType: batteryCritical, ID: {89abea26-b772-4258-9b56-e026b80c2028}) ---------- -The name of the StateType ({89abea26-b772-4258-9b56-e026b80c2028}) of ThingClass doorLock +The name of the StateType ({89abea26-b772-4258-9b56-e026b80c2028}) of ThingClass doorLock +---------- +The name of the ParamType (ThingClass: thermostat, EventType: batteryCritical, ID: {5cec4399-ba7c-4c78-8c30-c91040ad99cf}) +---------- +The name of the StateType ({5cec4399-ba7c-4c78-8c30-c91040ad99cf}) of ThingClass thermostat - + + Battery critical changed - The name of the EventType ({89abea26-b772-4258-9b56-e026b80c2028}) of ThingClass doorLock + The name of the EventType ({89abea26-b772-4258-9b56-e026b80c2028}) of ThingClass doorLock +---------- +The name of the EventType ({5cec4399-ba7c-4c78-8c30-c91040ad99cf}) of ThingClass thermostat - - - - - - + + + + + + Connected The name of the ParamType (ThingClass: doorLock, EventType: connected, ID: {206b0508-b477-4aa2-b420-aba2259fb6e6}) ---------- @@ -55,8 +73,8 @@ The name of the StateType ({e9fb2b10-96da-4b70-afda-46e948399af8}) of ThingClass - - + + Connected changed The name of the EventType ({206b0508-b477-4aa2-b420-aba2259fb6e6}) of ThingClass doorLock ---------- @@ -64,14 +82,29 @@ The name of the EventType ({b5abd47e-95f1-4e35-94fa-be87c396073a}) of ThingClass - + Connected/disconnected The name of the EventType ({e9fb2b10-96da-4b70-afda-46e948399af8}) of ThingClass thermostat - - + + + Cooling on + The name of the ParamType (ThingClass: thermostat, EventType: coolingOn, ID: {c77a3d3f-46c7-4026-b9ab-02ab88077cc4}) +---------- +The name of the StateType ({c77a3d3f-46c7-4026-b9ab-02ab88077cc4}) of ThingClass thermostat + + + + + Cooling turned on + The name of the EventType ({c77a3d3f-46c7-4026-b9ab-02ab88077cc4}) of ThingClass thermostat + + + + + Current temperature The name of the ParamType (ThingClass: thermostat, EventType: temperature, ID: {497af03a-a893-438c-aba2-1bf3ecfc66c5}) ---------- @@ -79,21 +112,21 @@ The name of the StateType ({497af03a-a893-438c-aba2-1bf3ecfc66c5}) of ThingClass - + Current temperature changed The name of the EventType ({497af03a-a893-438c-aba2-1bf3ecfc66c5}) of ThingClass thermostat - + Door lock The name of the ThingClass ({34cb3d09-dd9f-4b95-95d0-30a1cd94adac}) - - - + + + Endpoint ID The name of the ParamType (ThingClass: doorLock, Type: thing, ID: {ebf17460-4a37-461f-aa24-e0a9a4238c63}) ---------- @@ -103,9 +136,24 @@ The name of the ParamType (ThingClass: thermostat, Type: thing, ID: {138a529d-1d - - - + + + Heating on + The name of the ParamType (ThingClass: thermostat, EventType: heatingOn, ID: {88d5dda1-b8f6-49f1-a55a-20415f9157b3}) +---------- +The name of the StateType ({88d5dda1-b8f6-49f1-a55a-20415f9157b3}) of ThingClass thermostat + + + + + Heating turned on + The name of the EventType ({88d5dda1-b8f6-49f1-a55a-20415f9157b3}) of ThingClass thermostat + + + + + + IEEE adress The name of the ParamType (ThingClass: doorLock, Type: thing, ID: {484bf80f-5d20-4029-b05e-6b4c9fbefd69}) ---------- @@ -115,21 +163,21 @@ The name of the ParamType (ThingClass: thermostat, Type: thing, ID: {f38746d8-00 - + Identify The name of the ActionType ({4e3b1430-d98e-4f05-bae6-301dac34bd4b}) of ThingClass powerSocket - + Lock door The name of the ActionType ({c26e1908-25d0-4475-8f82-5aaf034640f1}) of ThingClass doorLock - - - + + + Manufacturer The name of the ParamType (ThingClass: doorLock, Type: thing, ID: {1b177eb8-a13f-4975-92a7-a6bf54670c8f}) ---------- @@ -139,9 +187,9 @@ The name of the ParamType (ThingClass: thermostat, Type: thing, ID: {ae76acb0-40 - - - + + + Model The name of the ParamType (ThingClass: doorLock, Type: thing, ID: {b13f86a7-dc65-4ed6-860c-cdb099f90916}) ---------- @@ -151,9 +199,9 @@ The name of the ParamType (ThingClass: thermostat, Type: thing, ID: {4262cfc6-4b - - - + + + Power The name of the ParamType (ThingClass: powerSocket, ActionType: power, ID: {2c8268b9-d76b-415a-b2e9-6bfeabd98a76}) ---------- @@ -163,30 +211,36 @@ The name of the StateType ({2c8268b9-d76b-415a-b2e9-6bfeabd98a76}) of ThingClass - + Power changed The name of the EventType ({2c8268b9-d76b-415a-b2e9-6bfeabd98a76}) of ThingClass powerSocket - + Power socket The name of the ThingClass ({800a8df8-06cb-4d93-8334-944fcce9651a}) - + Set power The name of the ActionType ({2c8268b9-d76b-415a-b2e9-6bfeabd98a76}) of ThingClass powerSocket - - - - - - + + Set target temperature + The name of the ActionType ({88ad3957-2912-4de1-b53d-b360565dd361}) of ThingClass thermostat + + + + + + + + + Signal strength The name of the ParamType (ThingClass: doorLock, EventType: signalStrength, ID: {6c8f8db5-464c-408a-9c65-4e8096663019}) ---------- @@ -202,9 +256,9 @@ The name of the StateType ({8f0512ab-ced2-4dcb-90fe-aaa532efd0dd}) of ThingClass - - - + + + Signal strength changed The name of the EventType ({6c8f8db5-464c-408a-9c65-4e8096663019}) of ThingClass doorLock ---------- @@ -214,33 +268,36 @@ The name of the EventType ({8f0512ab-ced2-4dcb-90fe-aaa532efd0dd}) of ThingClass - - + + + Target temperature - The name of the ParamType (ThingClass: thermostat, EventType: targetTemperature, ID: {88ad3957-2912-4de1-b53d-b360565dd361}) + The name of the ParamType (ThingClass: thermostat, ActionType: targetTemperature, ID: {88ad3957-2912-4de1-b53d-b360565dd361}) +---------- +The name of the ParamType (ThingClass: thermostat, EventType: targetTemperature, ID: {88ad3957-2912-4de1-b53d-b360565dd361}) ---------- The name of the StateType ({88ad3957-2912-4de1-b53d-b360565dd361}) of ThingClass thermostat - + Target temperature changed The name of the EventType ({88ad3957-2912-4de1-b53d-b360565dd361}) of ThingClass thermostat - + Unlock door The name of the ActionType ({6e112e3b-080f-47e4-8e2b-453029f0eacb}) of ThingClass doorLock - - - - - - + + + + + + Version The name of the ParamType (ThingClass: doorLock, EventType: version, ID: {9e27850b-99d8-40df-9bc7-4b3c7c01faf8}) ---------- @@ -256,9 +313,9 @@ The name of the StateType ({c8bd12d2-b425-4422-9e0e-a65542a47b70}) of ThingClass - - - + + + Version changed The name of the EventType ({9e27850b-99d8-40df-9bc7-4b3c7c01faf8}) of ThingClass doorLock ---------- @@ -268,21 +325,21 @@ The name of the EventType ({c8bd12d2-b425-4422-9e0e-a65542a47b70}) of ThingClass - + Zigbee Generic The name of the plugin ZigbeeGeneric ({6a4343be-9fd6-4015-9ff5-38542651c534}) - + Zigbee Thermostat The name of the ThingClass ({ca9af6cf-2d15-4d54-ba07-3d2ce03445b8}) - - - + + + Zigbee network UUID The name of the ParamType (ThingClass: doorLock, Type: thing, ID: {4d50cbd3-f297-421c-9938-65ec0bb4bd34}) ---------- @@ -292,7 +349,7 @@ The name of the ParamType (ThingClass: thermostat, Type: thing, ID: {4a92b536-de - + nymea The name of the vendor ({2062d64d-3232-433c-88bc-0d33c0ba2ba6})