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; }