From 771929363b6e8c8293eeea9c411ae5054e08339c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Fri, 13 Nov 2020 10:36:09 +0100 Subject: [PATCH] Add dimmable and color temperature light to generic lights plugin --- ...ntegrationpluginzigbee-generic-lights.json | 237 ++++++- .../integrationpluginzigbeegenericlights.cpp | 588 ++++++++++++++++-- .../integrationpluginzigbeegenericlights.h | 35 ++ 3 files changed, 802 insertions(+), 58 deletions(-) diff --git a/zigbee-generic-lights/integrationpluginzigbee-generic-lights.json b/zigbee-generic-lights/integrationpluginzigbee-generic-lights.json index b1185b55..b7266a54 100644 --- a/zigbee-generic-lights/integrationpluginzigbee-generic-lights.json +++ b/zigbee-generic-lights/integrationpluginzigbee-generic-lights.json @@ -56,8 +56,8 @@ { "id": "7548ac08-4ea1-4dcd-98b9-9d58bbc06ab7", "name": "connected", - "displayName": "Available", - "displayNameEvent": "Available changed", + "displayName": "Connected", + "displayNameEvent": "Connected changed", "type": "bool", "cached": false, "defaultValue": false @@ -102,6 +102,239 @@ ], "eventTypes": [ + ] + }, + { + "name": "dimmableLight", + "displayName": "Dimmable Light", + "id": "b2eeb554-ca5c-46b5-a897-bc443fb186ea", + "setupMethod": "JustAdd", + "createMethods": [ "Auto" ], + "interfaces": [ "dimmablelight", "alert", "wirelessconnectable" ], + "paramTypes": [ + { + "id": "beab891f-c1b0-4b2c-88c4-216c8d19f9e2", + "name": "ieeeAddress", + "displayName": "IEEE address", + "type": "QString", + "defaultValue": "00:00:00:00:00:00:00:00" + }, + { + "id": "81a82ed2-c465-4e53-a208-157d8473b16c", + "name": "networkUuid", + "displayName": "Zigbee network UUID", + "type": "QString", + "defaultValue": "" + }, + { + "id": "9472299b-b91b-456a-95fe-bf80d1a266a1", + "name": "endpointId", + "displayName": "Endpoint ID", + "type": "uint", + "defaultValue": 1 + }, + { + "id": "d9044272-2d7d-46d2-b7bf-37239014f2df", + "name": "manufacturer", + "displayName": "Manufacturer", + "type": "QString", + "defaultValue": "" + }, + { + "id": "3e971afc-a6f1-429c-a902-69cba865c222", + "name": "model", + "displayName": "Model", + "type": "QString", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "2dc93a53-c506-41a5-9242-b5d045f51c40", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "cached": false, + "defaultValue": false + }, + { + "id": "ab75980d-12f5-4ddc-be2d-dc54514a3b75", + "name": "signalStrength", + "displayName": "Signal strength", + "displayNameEvent": "Signal strength changed", + "defaultValue": 0, + "maxValue": 100, + "minValue": 0, + "type": "uint", + "unit": "Percentage" + }, + { + "id": "9390d2a6-7a17-4b42-8b35-cdb8b3d4d723", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "QString", + "cached": true, + "defaultValue": "" + }, + { + "id": "19d82a60-0009-4cff-966d-3b1c4fe358e1", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power changed", + "displayNameAction": "Set power", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "6376376f-3221-4e6b-bce3-41872a6fc98a", + "name": "brightness", + "displayName": "Brightness", + "displayNameEvent": "Brightness changed", + "displayNameAction": "Set brightness", + "maxValue": 100, + "minValue": 0, + "type": "int", + "defaultValue": 100, + "writable": true + } + ], + "actionTypes": [ + { + "id": "a04d9e2b-e854-4361-bb34-2da74f8f028f", + "name": "alert", + "displayName": "Identify" + } + ], + "eventTypes": [ + + ] + }, + { + "name": "colorTemperatureLight", + "displayName": "Color Temperature Light", + "id": "498815c1-a6a3-48e9-9c9b-bff974694b26", + "setupMethod": "JustAdd", + "createMethods": [ "Auto" ], + "interfaces": [ "colortemperaturelight", "alert", "wirelessconnectable" ], + "paramTypes": [ + { + "id": "60abc695-c945-4e08-8ea3-21c4f05ff2f4", + "name": "ieeeAddress", + "displayName": "IEEE address", + "type": "QString", + "defaultValue": "00:00:00:00:00:00:00:00" + }, + { + "id": "2f1799f0-76da-4147-89a0-1db93bb74872", + "name": "networkUuid", + "displayName": "Zigbee network UUID", + "type": "QString", + "defaultValue": "" + }, + { + "id": "8511c361-79c2-420c-93c2-89e97da1baff", + "name": "endpointId", + "displayName": "Endpoint ID", + "type": "uint", + "defaultValue": 1 + }, + { + "id": "3b688ee1-dd08-4863-a221-70c4ccc062e9", + "name": "manufacturer", + "displayName": "Manufacturer", + "type": "QString", + "defaultValue": "" + }, + { + "id": "c58bef19-87e3-484c-9a5c-c0daa23092a3", + "name": "model", + "displayName": "Model", + "type": "QString", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "dd4eb1fe-4ddd-42fa-97bf-df690728c866", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected changed", + "type": "bool", + "cached": false, + "defaultValue": false + }, + { + "id": "45e5b606-1e4d-42b8-9ee1-0442751247ef", + "name": "signalStrength", + "displayName": "Signal strength", + "displayNameEvent": "Signal strength changed", + "defaultValue": 0, + "maxValue": 100, + "minValue": 0, + "type": "uint", + "unit": "Percentage" + }, + { + "id": "477a1531-5f87-4f21-9b32-01161be93cd2", + "name": "version", + "displayName": "Version", + "displayNameEvent": "Version changed", + "type": "QString", + "cached": true, + "defaultValue": "" + }, + { + "id": "a8c56366-5b2d-4f4b-b910-38a87851c484", + "name": "power", + "displayName": "Power", + "displayNameEvent": "Power changed", + "displayNameAction": "Set power", + "type": "bool", + "defaultValue": false, + "writable": true + }, + { + "id": "b72177f9-b673-49b5-864a-345936e5c821", + "name": "brightness", + "displayName": "Brightness", + "displayNameEvent": "Brightness changed", + "displayNameAction": "Set brightness", + "maxValue": 100, + "minValue": 0, + "type": "int", + "defaultValue": 100, + "writable": true + }, + { + "id": "69e94f88-a664-4bb9-ba04-2c3ea6cd7569", + "name": "colorTemperature", + "displayName": "Color temperature scaled", + "displayNameEvent": "Color temperature scaled changed", + "displayNameAction": "Set color temperature scaled", + "defaultValue": 100, + "minValue": 0, + "maxValue": 200, + "type": "int", + "writable": true + } + ], + "actionTypes": [ + { + "id": "4f8a38f4-652f-40e1-86dd-028dcd64349c", + "name": "alert", + "displayName": "Identify" + }, + { + "id": "a3e8d1c7-81c7-4255-b410-3bc4d5c7b4e9", + "name": "removeFromNetwork", + "displayName": "Remove from network" + } + ], + "eventTypes": [ + ] } ] diff --git a/zigbee-generic-lights/integrationpluginzigbeegenericlights.cpp b/zigbee-generic-lights/integrationpluginzigbeegenericlights.cpp index 047e6de4..d3f5441f 100644 --- a/zigbee-generic-lights/integrationpluginzigbeegenericlights.cpp +++ b/zigbee-generic-lights/integrationpluginzigbeegenericlights.cpp @@ -37,17 +37,40 @@ IntegrationPluginZigbeeGenericLights::IntegrationPluginZigbeeGenericLights() { + // Common thing params map m_ieeeAddressParamTypeIds[onOffLightThingClassId] = onOffLightThingIeeeAddressParamTypeId; + m_ieeeAddressParamTypeIds[dimmableLightThingClassId] = dimmableLightThingIeeeAddressParamTypeId; + m_ieeeAddressParamTypeIds[colorTemperatureLightThingClassId] = colorTemperatureLightThingIeeeAddressParamTypeId; m_networkUuidParamTypeIds[onOffLightThingClassId] = onOffLightThingNetworkUuidParamTypeId; + m_networkUuidParamTypeIds[dimmableLightThingClassId] = dimmableLightThingNetworkUuidParamTypeId; + m_networkUuidParamTypeIds[colorTemperatureLightThingClassId] = colorTemperatureLightThingNetworkUuidParamTypeId; m_endpointIdParamTypeIds[onOffLightThingClassId] = onOffLightThingEndpointIdParamTypeId; + m_endpointIdParamTypeIds[dimmableLightThingClassId] = dimmableLightThingEndpointIdParamTypeId; + m_endpointIdParamTypeIds[colorTemperatureLightThingClassId] = colorTemperatureLightThingEndpointIdParamTypeId; + m_manufacturerIdParamTypeIds[onOffLightThingClassId] = onOffLightThingManufacturerParamTypeId; + m_manufacturerIdParamTypeIds[dimmableLightThingClassId] = dimmableLightThingManufacturerParamTypeId; + m_manufacturerIdParamTypeIds[colorTemperatureLightThingClassId] = colorTemperatureLightThingManufacturerParamTypeId; + + m_modelIdParamTypeIds[onOffLightThingClassId] = onOffLightThingModelParamTypeId; + m_modelIdParamTypeIds[dimmableLightThingClassId] = dimmableLightThingModelParamTypeId; + m_modelIdParamTypeIds[colorTemperatureLightThingClassId] = colorTemperatureLightThingModelParamTypeId; + + + // Common sates map m_connectedStateTypeIds[onOffLightThingClassId] = onOffLightConnectedStateTypeId; + m_connectedStateTypeIds[dimmableLightThingClassId] = dimmableLightConnectedStateTypeId; + m_connectedStateTypeIds[colorTemperatureLightThingClassId] = colorTemperatureLightConnectedStateTypeId; m_signalStrengthStateTypeIds[onOffLightThingClassId] = onOffLightSignalStrengthStateTypeId; + m_signalStrengthStateTypeIds[dimmableLightThingClassId] = dimmableLightSignalStrengthStateTypeId; + m_signalStrengthStateTypeIds[colorTemperatureLightThingClassId] = colorTemperatureLightSignalStrengthStateTypeId; m_versionStateTypeIds[onOffLightThingClassId] = onOffLightVersionStateTypeId; + m_versionStateTypeIds[dimmableLightThingClassId] = dimmableLightVersionStateTypeId; + m_versionStateTypeIds[colorTemperatureLightThingClassId] = colorTemperatureLightVersionStateTypeId; } QString IntegrationPluginZigbeeGenericLights::name() const @@ -63,20 +86,29 @@ bool IntegrationPluginZigbeeGenericLights::handleNode(ZigbeeNode *node, const QU (endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceOnOffLight)) { - ThingDescriptor descriptor(onOffLightThingClassId); - QString deviceClassName = supportedThings().findById(onOffLightThingClassId).displayName(); - descriptor.setTitle(QString("%1 (%2 - %3)").arg(deviceClassName).arg(endpoint->manufacturerName()).arg(endpoint->modelIdentifier())); - qCDebug(dcZigbeeGenericLights()) << "Handeling generic on/off light" << descriptor.title(); + qCDebug(dcZigbeeGenericLights()) << "Handeling on/off light for" << node << endpoint; + createLightThing(onOffLightThingClassId, networkUuid, node, endpoint); + return true; + } - ParamList params; - params.append(Param(onOffLightThingNetworkUuidParamTypeId, networkUuid.toString())); - params.append(Param(onOffLightThingIeeeAddressParamTypeId, node->extendedAddress().toString())); - params.append(Param(onOffLightThingEndpointIdParamTypeId, endpoint->endpointId())); - params.append(Param(onOffLightThingModelParamTypeId, endpoint->modelIdentifier())); - params.append(Param(onOffLightThingManufacturerParamTypeId, endpoint->manufacturerName())); - descriptor.setParams(params); - emit autoThingsAppeared({descriptor}); + if ((endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileLightLink && + endpoint->deviceId() == Zigbee::LightLinkDevice::LightLinkDeviceDimmableLight) || + (endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation && + endpoint->deviceId() == Zigbee::HomeAutomationDeviceDimmableLight)) { + qCDebug(dcZigbeeGenericLights()) << "Handeling dimmable light for" << node << endpoint; + createLightThing(dimmableLightThingClassId, networkUuid, node, endpoint); + return true; + } + + + if ((endpoint->profile() == Zigbee::ZigbeeProfileLightLink && + endpoint->deviceId() == Zigbee::LightLinkDeviceColourTemperatureLight) || + (endpoint->profile() == Zigbee::ZigbeeProfileHomeAutomation && + endpoint->deviceId() == Zigbee::HomeAutomationDeviceColourTemperatureLight)) { + + qCDebug(dcZigbeeGenericLights()) << "Handeling color temperature light for" << node << endpoint; + createLightThing(colorTemperatureLightThingClassId, networkUuid, node, endpoint); return true; } } @@ -93,6 +125,10 @@ void IntegrationPluginZigbeeGenericLights::handleRemoveNode(ZigbeeNode *node, co qCDebug(dcZigbeeGenericLights()) << node << "for" << thing << "has left the network."; m_thingNodes.remove(thing); emit autoThingDisappeared(thing->id()); + + if (m_colorTemperatureRanges.contains(thing)) { + m_colorTemperatureRanges.remove(thing); + } } } @@ -146,6 +182,8 @@ void IntegrationPluginZigbeeGenericLights::setupThing(ThingSetupInfo *info) thing->setStateValue(m_versionStateTypeIds.value(thing->thingClassId()), endpoint->softwareBuildId()); // Thing specific setup + + // On/Off light if (thing->thingClassId() == onOffLightThingClassId) { // Get the on/off cluster @@ -156,6 +194,10 @@ void IntegrationPluginZigbeeGenericLights::setupThing(ThingSetupInfo *info) return; } + if (onOffCluster->hasAttribute(ZigbeeClusterOnOff::AttributeOnOff)) { + thing->setStateValue(onOffLightPowerStateTypeId, onOffCluster->power()); + } + // Update the power state if the node power value changes connect(onOffCluster, &ZigbeeClusterOnOff::powerChanged, thing, [thing](bool power){ qCDebug(dcZigbeeGenericLights()) << thing << "power state changed" << power; @@ -170,6 +212,134 @@ void IntegrationPluginZigbeeGenericLights::setupThing(ThingSetupInfo *info) }); } + // Dimmable light + if (thing->thingClassId() == dimmableLightThingClassId) { + + // Get the on/off cluster + ZigbeeClusterOnOff *onOffCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdOnOff); + if (!onOffCluster) { + qCWarning(dcZigbeeGenericLights()) << "Could not find on/off cluster for" << thing << "in" << node; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + // Only set the state if the cluster actually has the attribute + if (onOffCluster->hasAttribute(ZigbeeClusterOnOff::AttributeOnOff)) { + thing->setStateValue(dimmableLightPowerStateTypeId, onOffCluster->power()); + } + + // Update the power state if the node power value changes + connect(onOffCluster, &ZigbeeClusterOnOff::powerChanged, thing, [thing](bool power){ + qCDebug(dcZigbeeGenericLights()) << thing << "power state changed" << power; + thing->setStateValue(dimmableLightPowerStateTypeId, power); + }); + + + // Get the level cluster + ZigbeeClusterLevelControl *levelCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdLevelControl); + if (!levelCluster) { + qCWarning(dcZigbeeGenericLights()) << "Could not find level cluster for" << thing << "in" << node; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + // Only set the state if the cluster actually has the attribute + if (levelCluster->hasAttribute(ZigbeeClusterLevelControl::AttributeCurrentLevel)) { + int percentage = qRound(levelCluster->currentLevel() * 100.0 / 255.0); + thing->setStateValue(dimmableLightBrightnessStateTypeId, percentage); + } + + connect(levelCluster, &ZigbeeClusterLevelControl::currentLevelChanged, thing, [thing](quint8 level){ + int percentage = qRound(level * 100.0 / 255.0); + qCDebug(dcZigbeeGenericLights()) << thing << "level state changed" << level << percentage << "%"; + thing->setStateValue(dimmableLightBrightnessStateTypeId, percentage); + }); + + // Read the states once the node gets reachable + connect(node, &ZigbeeNode::reachableChanged, thing, [thing, this](bool reachable){ + if (reachable) { + readLightPowerState(thing); + readLightLevelState(thing); + } + }); + } + + // Color temperature light + if (thing->thingClassId() == colorTemperatureLightThingClassId) { + + // Get the on/off cluster + ZigbeeClusterOnOff *onOffCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdOnOff); + if (!onOffCluster) { + qCWarning(dcZigbeeGenericLights()) << "Could not find on/off cluster for" << thing << "in" << node; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + // Only set the state if the cluster actually has the attribute + if (onOffCluster->hasAttribute(ZigbeeClusterOnOff::AttributeOnOff)) { + thing->setStateValue(colorTemperatureLightPowerStateTypeId, onOffCluster->power()); + } + + // Update the power state if the node power value changes + connect(onOffCluster, &ZigbeeClusterOnOff::powerChanged, thing, [thing](bool power){ + qCDebug(dcZigbeeGenericLights()) << thing << "power state changed" << power; + thing->setStateValue(colorTemperatureLightPowerStateTypeId, power); + }); + + + // Get the level cluster + ZigbeeClusterLevelControl *levelCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdLevelControl); + if (!levelCluster) { + qCWarning(dcZigbeeGenericLights()) << "Could not find level cluster for" << thing << "in" << node; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + // Only set the state if the cluster actually has the attribute + if (levelCluster->hasAttribute(ZigbeeClusterLevelControl::AttributeCurrentLevel)) { + int percentage = qRound(levelCluster->currentLevel() * 100.0 / 255.0); + thing->setStateValue(colorTemperatureLightBrightnessStateTypeId, percentage); + } + + connect(levelCluster, &ZigbeeClusterLevelControl::currentLevelChanged, thing, [thing](quint8 level){ + int percentage = qRound(level * 100.0 / 255.0); + qCDebug(dcZigbeeGenericLights()) << thing << "level state changed" << level << percentage << "%"; + thing->setStateValue(colorTemperatureLightBrightnessStateTypeId, percentage); + }); + + + // Get color cluster + ZigbeeClusterColorControl *colorCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdColorControl); + if (!colorCluster) { + qCWarning(dcZigbeeGenericLights()) << "Could not find color cluster for" << thing << "in" << node; + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + // Only set the state if the cluster actually has the attribute + if (colorCluster->hasAttribute(ZigbeeClusterColorControl::AttributeColorTemperatureMireds)) { + int mappedValue = mapColorTemperatureToScaledValue(thing, colorCluster->colorTemperatureMireds()); + thing->setStateValue(colorTemperatureLightColorTemperatureStateTypeId, mappedValue); + } + + connect(colorCluster, &ZigbeeClusterColorControl::colorTemperatureMiredsChanged, thing, [this, thing](quint16 colorTemperatureMired){ + qCDebug(dcZigbeeGenericLights()) << "Actual color temperature is" << colorTemperatureMired << "mireds"; + int mappedValue = mapColorTemperatureToScaledValue(thing, colorTemperatureMired); + qCDebug(dcZigbeeGenericLights()) << "Mapped color temperature is" << mappedValue; + thing->setStateValue(colorTemperatureLightColorTemperatureStateTypeId, mappedValue); + }); + + // Read the states once the node gets reachable + connect(node, &ZigbeeNode::reachableChanged, thing, [thing, this](bool reachable){ + if (reachable) { + readColorTemperatureRange(thing); + readLightPowerState(thing); + readLightLevelState(thing); + readLightColorTemperatureState(thing); + } + }); + } + info->finish(Thing::ThingErrorNoError); } @@ -183,10 +353,10 @@ void IntegrationPluginZigbeeGenericLights::executeAction(ThingActionInfo *info) // Get the node Thing *thing = info->thing(); ZigbeeNode *node = m_thingNodes.value(thing); -// if (!node->reachable()) { -// info->finish(Thing::ThingErrorHardwareNotAvailable); -// return; -// } + if (!node->reachable()) { + info->finish(Thing::ThingErrorHardwareNotAvailable); + return; + } // Get the endpoint ZigbeeNodeEndpoint *endpoint = findEndpoint(thing); @@ -195,58 +365,71 @@ void IntegrationPluginZigbeeGenericLights::executeAction(ThingActionInfo *info) return; } + // On/Off light if (thing->thingClassId() == onOffLightThingClassId) { - if (info->action().actionTypeId() == onOffLightAlertActionTypeId) { - // Get the identify server cluster from the endpoint - ZigbeeClusterIdentify *identifyCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdIdentify); - if (!identifyCluster) { - qCWarning(dcZigbeeGenericLights()) << "Could not find identify cluster for" << thing << "in" << node; - info->finish(Thing::ThingErrorHardwareFailure); - return; - } - - // Send the command trough the network - ZigbeeClusterReply *reply = identifyCluster->identify(2); - connect(reply, &ZigbeeClusterReply::finished, this, [reply, info](){ - // Note: reply will be deleted automatically - if (reply->error() != ZigbeeClusterReply::ErrorNoError) { - info->finish(Thing::ThingErrorHardwareFailure); - } else { - info->finish(Thing::ThingErrorNoError); - } - }); + executeAlertAction(info, endpoint); return; } if (info->action().actionTypeId() == onOffLightPowerActionTypeId) { // Get the on/off server cluster from the endpoint - ZigbeeClusterOnOff *onOffCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdOnOff); - if (!onOffCluster) { - qCWarning(dcZigbeeGenericLights()) << "Could not find on/off cluster for" << thing << "in" << node; - info->finish(Thing::ThingErrorHardwareFailure); - return; - } - - // Send the command trough the network bool power = info->action().param(onOffLightPowerActionPowerParamTypeId).value().toBool(); - qCDebug(dcZigbeeGenericLights()) << "Set power for" << thing << "to" << power; - ZigbeeClusterReply *reply = (power ? onOffCluster->commandOn() : onOffCluster->commandOff()); - connect(reply, &ZigbeeClusterReply::finished, thing, [reply, info, power, thing](){ - // Note: reply will be deleted automatically - if (reply->error() != ZigbeeClusterReply::ErrorNoError) { - qCWarning(dcZigbeeGenericLights()) << "Failed to set power on" << thing << reply->error(); - info->finish(Thing::ThingErrorHardwareFailure); - } else { - info->finish(Thing::ThingErrorNoError); - qCDebug(dcZigbeeGenericLights()) << "Set power finished successfully for" << thing; - thing->setStateValue(onOffLightPowerStateTypeId, power); - } - }); + executePowerAction(info, endpoint, onOffLightPowerStateTypeId, power); return; } } + // Dimmable light + if (thing->thingClassId() == dimmableLightThingClassId) { + if (info->action().actionTypeId() == dimmableLightAlertActionTypeId) { + executeAlertAction(info, endpoint); + return; + } + + if (info->action().actionTypeId() == dimmableLightPowerActionTypeId) { + bool power = info->action().param(dimmableLightPowerActionPowerParamTypeId).value().toBool(); + executePowerAction(info, endpoint, dimmableLightPowerStateTypeId, power); + return; + } + + if (info->action().actionTypeId() == dimmableLightBrightnessActionTypeId) { + int brightness = info->action().param(dimmableLightBrightnessActionBrightnessParamTypeId).value().toInt(); + quint8 level = static_cast(qRound(255.0 * brightness / 100.0)); + executeBrightnessAction(info, endpoint, dimmableLightPowerStateTypeId, dimmableLightBrightnessStateTypeId, brightness, level); + return; + } + } + + // Color temperature light + if (thing->thingClassId() == colorTemperatureLightThingClassId) { + if (info->action().actionTypeId() == colorTemperatureLightAlertActionTypeId) { + executeAlertAction(info, endpoint); + return; + } + + if (info->action().actionTypeId() == colorTemperatureLightPowerActionTypeId) { + bool power = info->action().param(colorTemperatureLightPowerActionPowerParamTypeId).value().toBool(); + executePowerAction(info, endpoint, colorTemperatureLightPowerStateTypeId, power); + return; + } + + if (info->action().actionTypeId() == colorTemperatureLightBrightnessActionTypeId) { + int brightness = info->action().param(colorTemperatureLightBrightnessActionBrightnessParamTypeId).value().toInt(); + quint8 level = static_cast(qRound(255.0 * brightness / 100.0)); + executeBrightnessAction(info, endpoint, colorTemperatureLightPowerStateTypeId, colorTemperatureLightBrightnessStateTypeId, brightness, level); + return; + } + + if (info->action().actionTypeId() == colorTemperatureLightColorTemperatureActionTypeId) { + int colorTemperatureScaled = info->action().param(colorTemperatureLightColorTemperatureActionColorTemperatureParamTypeId).value().toInt(); + executeColorTemperatureAction(info, endpoint, colorTemperatureLightColorTemperatureStateTypeId, colorTemperatureScaled); + return; + } + } + + + info->finish(Thing::ThingErrorUnsupportedFeature); } @@ -257,6 +440,10 @@ void IntegrationPluginZigbeeGenericLights::thingRemoved(Thing *thing) QUuid networkUuid = thing->paramValue(m_networkUuidParamTypeIds.value(thing->thingClassId())).toUuid(); hardwareManager()->zigbeeResource()->removeNodeFromNetwork(networkUuid, node); } + + if (m_colorTemperatureRanges.contains(thing)) { + m_colorTemperatureRanges.remove(thing); + } } ZigbeeNodeEndpoint *IntegrationPluginZigbeeGenericLights::findEndpoint(Thing *thing) @@ -271,6 +458,118 @@ ZigbeeNodeEndpoint *IntegrationPluginZigbeeGenericLights::findEndpoint(Thing *th return node->getEndpoint(endpointId); } +void IntegrationPluginZigbeeGenericLights::createLightThing(const ThingClassId &thingClassId, const QUuid &networkUuid, ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint) +{ + ThingDescriptor descriptor(thingClassId); + QString deviceClassName = supportedThings().findById(thingClassId).displayName(); + descriptor.setTitle(QString("%1 (%2 - %3)").arg(deviceClassName).arg(endpoint->manufacturerName()).arg(endpoint->modelIdentifier())); + + ParamList params; + params.append(Param(m_networkUuidParamTypeIds[thingClassId], networkUuid.toString())); + params.append(Param(m_ieeeAddressParamTypeIds[thingClassId], node->extendedAddress().toString())); + params.append(Param(m_endpointIdParamTypeIds[thingClassId], endpoint->endpointId())); + params.append(Param(m_modelIdParamTypeIds[thingClassId], endpoint->modelIdentifier())); + params.append(Param(m_manufacturerIdParamTypeIds[thingClassId], endpoint->manufacturerName())); + descriptor.setParams(params); + emit autoThingsAppeared({descriptor}); +} + +void IntegrationPluginZigbeeGenericLights::executeAlertAction(ThingActionInfo *info, ZigbeeNodeEndpoint *endpoint) +{ + Thing *thing = info->thing(); + ZigbeeClusterIdentify *identifyCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdIdentify); + if (!identifyCluster) { + qCWarning(dcZigbeeGenericLights()) << "Could not find identify cluster for" << thing << "in" << m_thingNodes.value(thing); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + // Send the command trough the network + ZigbeeClusterReply *reply = identifyCluster->identify(2); + connect(reply, &ZigbeeClusterReply::finished, this, [reply, info](){ + // Note: reply will be deleted automatically + if (reply->error() != ZigbeeClusterReply::ErrorNoError) { + info->finish(Thing::ThingErrorHardwareFailure); + } else { + info->finish(Thing::ThingErrorNoError); + } + }); +} + +void IntegrationPluginZigbeeGenericLights::executePowerAction(ThingActionInfo *info, ZigbeeNodeEndpoint *endpoint, const StateTypeId &powerStateTypeId, bool power) +{ + Thing *thing = info->thing(); + ZigbeeClusterOnOff *onOffCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdOnOff); + if (!onOffCluster) { + qCWarning(dcZigbeeGenericLights()) << "Could not find on/off cluster for" << thing << "in" << m_thingNodes.value(thing); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + // Send the command trough the network + qCDebug(dcZigbeeGenericLights()) << "Set power for" << info->thing() << "to" << power; + ZigbeeClusterReply *reply = (power ? onOffCluster->commandOn() : onOffCluster->commandOff()); + connect(reply, &ZigbeeClusterReply::finished, info, [=](){ + // Note: reply will be deleted automatically + if (reply->error() != ZigbeeClusterReply::ErrorNoError) { + qCWarning(dcZigbeeGenericLights()) << "Failed to set power on" << thing << reply->error(); + info->finish(Thing::ThingErrorHardwareFailure); + } else { + info->finish(Thing::ThingErrorNoError); + qCDebug(dcZigbeeGenericLights()) << "Set power finished successfully for" << thing; + thing->setStateValue(powerStateTypeId, power); + } + }); +} + +void IntegrationPluginZigbeeGenericLights::executeBrightnessAction(ThingActionInfo *info, ZigbeeNodeEndpoint *endpoint, const StateTypeId &powerStateTypeId, const StateTypeId &brightnessStateTypeId, int brightness, quint8 level) +{ + Thing *thing = info->thing(); + ZigbeeClusterLevelControl *levelCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdLevelControl); + if (!levelCluster) { + qCWarning(dcZigbeeGenericLights()) << "Could not find level control cluster for" << thing << "in" << m_thingNodes.value(thing); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + ZigbeeClusterReply *reply = levelCluster->commandMoveToLevelWithOnOff(level); + connect(reply, &ZigbeeClusterReply::finished, info, [=](){ + // Note: reply will be deleted automatically + if (reply->error() != ZigbeeClusterReply::ErrorNoError) { + info->finish(Thing::ThingErrorHardwareFailure); + } else { + info->finish(Thing::ThingErrorNoError); + thing->setStateValue(powerStateTypeId, (brightness > 0 ? true : false)); + thing->setStateValue(brightnessStateTypeId, brightness); + } + }); +} + +void IntegrationPluginZigbeeGenericLights::executeColorTemperatureAction(ThingActionInfo *info, ZigbeeNodeEndpoint *endpoint, const StateTypeId &colorTemperatureStateTypeId, int colorTemperatureScaled) +{ + Thing *thing = info->thing(); + ZigbeeClusterColorControl *colorCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdColorControl); + if (!colorCluster) { + qCWarning(dcZigbeeGenericLights()) << "Could not find color control cluster for" << thing << "in" << m_thingNodes.value(thing); + info->finish(Thing::ThingErrorHardwareFailure); + return; + } + + // Scale the value to the actual color temperature of this lamp + quint16 colorTemperature = mapScaledValueToColorTemperature(thing, colorTemperatureScaled); + // Note: time unit is 1/10 s + ZigbeeClusterReply *reply = colorCluster->commandMoveToColorTemperature(colorTemperature, 5); + connect(reply, &ZigbeeClusterReply::finished, info, [=](){ + // Note: reply will be deleted automatically + if (reply->error() != ZigbeeClusterReply::ErrorNoError) { + info->finish(Thing::ThingErrorHardwareFailure); + } else { + info->finish(Thing::ThingErrorNoError); + thing->setStateValue(colorTemperatureStateTypeId, colorTemperatureScaled); + } + }); +} + void IntegrationPluginZigbeeGenericLights::readLightPowerState(Thing *thing) { // Get the node @@ -297,3 +596,180 @@ void IntegrationPluginZigbeeGenericLights::readLightPowerState(Thing *thing) } }); } + +void IntegrationPluginZigbeeGenericLights::readLightLevelState(Thing *thing) +{ + // Get the node + ZigbeeNode *node = m_thingNodes.value(thing); + if (!node->reachable()) + return; + + // Get the endpoint + ZigbeeNodeEndpoint *endpoint = findEndpoint(thing); + if (!endpoint) + return; + + // Get the level server cluster from the endpoint + ZigbeeClusterLevelControl *levelCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdLevelControl); + if (!levelCluster) + return; + + qCDebug(dcZigbeeGenericLights()) << "Reading level value for" << thing << "on" << node; + ZigbeeClusterReply *reply = levelCluster->readAttributes({ZigbeeClusterLevelControl::AttributeCurrentLevel}); + connect(reply, &ZigbeeClusterReply::finished, thing, [thing, reply](){ + if (reply->error() != ZigbeeClusterReply::ErrorNoError) { + qCWarning(dcZigbeeGenericLights()) << "Failed to read level cluster attribute from" << thing << reply->error(); + return; + } + }); +} + +void IntegrationPluginZigbeeGenericLights::readLightColorTemperatureState(Thing *thing) +{ + ZigbeeNodeEndpoint *endpoint = findEndpoint(thing); + if (!endpoint) { + qCWarning(dcZigbeeGenericLights()) << "Failed to read color temperature for" << thing << "because the node could not be found"; + return; + } + + // Get the color server cluster from the endpoint + ZigbeeClusterColorControl *colorCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdColorControl); + if (!colorCluster) { + qCWarning(dcZigbeeGenericLights()) << "Failed to read color temperature for" << thing << "because the color cluster could not be found on" << endpoint; + return; + } + + ZigbeeClusterReply *reply = colorCluster->readAttributes({ZigbeeClusterColorControl::AttributeColorTemperatureMireds}); + connect(reply, &ZigbeeClusterReply::finished, thing, [=](){ + if (reply->error() != ZigbeeClusterReply::ErrorNoError) { + qCWarning(dcZigbeeGenericLights()) << "Failed to read ColorControl cluster attribute color temperature" << reply->error(); + return; + } + }); +} + +void IntegrationPluginZigbeeGenericLights::readColorTemperatureRange(Thing *thing) +{ + ZigbeeNodeEndpoint *endpoint = findEndpoint(thing); + if (!endpoint) { + qCWarning(dcZigbeeGenericLights()) << "Failed to read color temperature range for" << thing << "because the node could not be found"; + return; + } + + // Get the color server cluster from the endpoint + ZigbeeClusterColorControl *colorCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdColorControl); + if (!colorCluster) { + qCWarning(dcZigbeeGenericLights()) << "Failed to read color temperature range for" << thing << "because the color cluster could not be found on" << endpoint; + return; + } + + // Check if we can use the cached values from the database + if (readCachedColorTemperatureRange(thing, colorCluster)) { + qCDebug(dcZigbeeGenericLights()) << "Using cached color temperature mireds interval for mapping" << thing << "[" << m_colorTemperatureRanges[thing].minValue << "," << m_colorTemperatureRanges[thing].maxValue << "] mired"; + return; + } + + // Init with default values + m_colorTemperatureRanges[thing] = ColorTemperatureRange(); + + // We need to read them from the lamp + ZigbeeClusterReply *reply = colorCluster->readAttributes({ZigbeeClusterColorControl::AttributeColorTempPhysicalMinMireds, ZigbeeClusterColorControl::AttributeColorTempPhysicalMaxMireds}); + connect(reply, &ZigbeeClusterReply::finished, thing, [=](){ + if (reply->error() != ZigbeeClusterReply::ErrorNoError) { + qCWarning(dcZigbeeGenericLights()) << "Reading color temperature range attributes finished with error" << reply->error(); + qCWarning(dcZigbeeGenericLights()) << "Failed to read color temperature min/max interval values. Using default values for" << thing << "[" << m_colorTemperatureRanges[thing].minValue << "," << m_colorTemperatureRanges[thing].maxValue << "] mired"; + return; + } + + QList attributeStatusRecords = ZigbeeClusterLibrary::parseAttributeStatusRecords(reply->responseFrame().payload); + if (attributeStatusRecords.count() != 2) { + qCWarning(dcZigbeeGenericLights()) << "Did not receive temperature min/max interval values from" << thing; + qCWarning(dcZigbeeGenericLights()) << "Using default values for" << thing << "[" << m_colorTemperatureRanges[thing].minValue << "," << m_colorTemperatureRanges[thing].maxValue << "] mired" ; + return; + } + + // Parse the attribute status records + foreach (const ZigbeeClusterLibrary::ReadAttributeStatusRecord &attributeStatusRecord, attributeStatusRecords) { + if (attributeStatusRecord.attributeId == ZigbeeClusterColorControl::AttributeColorTempPhysicalMinMireds) { + bool valueOk = false; + quint16 minMiredsValue = attributeStatusRecord.dataType.toUInt16(&valueOk); + if (!valueOk) { + qCWarning(dcZigbeeGenericLights()) << "Failed to read color temperature min mireds attribute value and convert it" << attributeStatusRecord; + break; + } + + m_colorTemperatureRanges[thing].minValue = minMiredsValue; + } + + if (attributeStatusRecord.attributeId == ZigbeeClusterColorControl::AttributeColorTempPhysicalMaxMireds) { + bool valueOk = false; + quint16 maxMiredsValue = attributeStatusRecord.dataType.toUInt16(&valueOk); + if (!valueOk) { + qCWarning(dcZigbeeGenericLights()) << "Failed to read color temperature max mireds attribute value and convert it" << attributeStatusRecord; + break; + } + + m_colorTemperatureRanges[thing].maxValue = maxMiredsValue; + } + } + + qCDebug(dcZigbeeGenericLights()) << "Using lamp specific color temperature mireds interval for mapping" << thing << "[" << m_colorTemperatureRanges[thing].minValue << "," << m_colorTemperatureRanges[thing].maxValue << "] mired"; + }); + +} + +bool IntegrationPluginZigbeeGenericLights::readCachedColorTemperatureRange(Thing *thing, ZigbeeClusterColorControl *colorCluster) +{ + if (colorCluster->hasAttribute(ZigbeeClusterColorControl::AttributeColorTempPhysicalMinMireds) && colorCluster->hasAttribute(ZigbeeClusterColorControl::AttributeColorTempPhysicalMaxMireds)) { + ZigbeeClusterAttribute minMiredsAttribute = colorCluster->attribute(ZigbeeClusterColorControl::AttributeColorTempPhysicalMinMireds); + bool valueOk = false; + quint16 minMiredsValue = minMiredsAttribute.dataType().toUInt16(&valueOk); + if (!valueOk) { + qCWarning(dcZigbeeGenericLights()) << "Failed to read color temperature min mireds attribute value and convert it" << minMiredsAttribute; + return false; + } + + ZigbeeClusterAttribute maxMiredsAttribute = colorCluster->attribute(ZigbeeClusterColorControl::AttributeColorTempPhysicalMaxMireds); + quint16 maxMiredsValue = maxMiredsAttribute.dataType().toUInt16(&valueOk); + if (!valueOk) { + qCWarning(dcZigbeeGenericLights()) << "Failed to read color temperature max mireds attribute value and convert it" << maxMiredsAttribute; + return false; + } + + ColorTemperatureRange range; + range.minValue = minMiredsValue; + range.maxValue = maxMiredsValue; + m_colorTemperatureRanges[thing] = range; + return true; + } + + return false; +} + +quint16 IntegrationPluginZigbeeGenericLights::mapScaledValueToColorTemperature(Thing *thing, int scaledColorTemperature) +{ + // Make sure we have values to scale + if (!m_colorTemperatureRanges.contains(thing)) { + m_colorTemperatureRanges[thing] = ColorTemperatureRange(); + } + + double percentage = static_cast((scaledColorTemperature - m_minScaleValue)) / (m_maxScaleValue - m_minScaleValue); + //qCDebug(dcZigbeeGenericLights()) << "Mapping color temperature value" << scaledColorTemperature << "between" << m_minScaleValue << m_maxScaleValue << "is" << percentage * 100 << "%"; + double mappedValue = (m_colorTemperatureRanges[thing].maxValue - m_colorTemperatureRanges[thing].minValue) * percentage + m_colorTemperatureRanges[thing].minValue; + //qCDebug(dcZigbeeGenericLights()) << "Mapping color temperature value" << scaledColorTemperature << "is" << mappedValue << "mireds"; + return static_cast(qRound(mappedValue)); +} + +int IntegrationPluginZigbeeGenericLights::mapColorTemperatureToScaledValue(Thing *thing, quint16 colorTemperature) +{ + // Make sure we have values to scale + if (!m_colorTemperatureRanges.contains(thing)) { + m_colorTemperatureRanges[thing] = ColorTemperatureRange(); + } + + double percentage = static_cast((colorTemperature - m_colorTemperatureRanges[thing].minValue)) / (m_colorTemperatureRanges[thing].maxValue - m_colorTemperatureRanges[thing].minValue); + //qCDebug(dcZigbee()) << "Mapping color temperature value" << colorTemperature << "mirred" << m_minColorTemperature << m_maxColorTemperature << "is" << percentage * 100 << "%"; + double mappedValue = (m_maxScaleValue - m_minScaleValue) * percentage + m_minScaleValue; + //qCDebug(dcZigbee()) << "Mapping color temperature value" << colorTemperature << "results into the scaled value of" << mappedValue; + return static_cast(qRound(mappedValue)); +} diff --git a/zigbee-generic-lights/integrationpluginzigbeegenericlights.h b/zigbee-generic-lights/integrationpluginzigbeegenericlights.h index 9f67f6b9..b8be53fb 100644 --- a/zigbee-generic-lights/integrationpluginzigbeegenericlights.h +++ b/zigbee-generic-lights/integrationpluginzigbeegenericlights.h @@ -54,19 +54,54 @@ public: void thingRemoved(Thing *thing) override; private: + // Common thing params QHash m_ieeeAddressParamTypeIds; QHash m_networkUuidParamTypeIds; QHash m_endpointIdParamTypeIds; + QHash m_modelIdParamTypeIds; + QHash m_manufacturerIdParamTypeIds; + // Common states QHash m_connectedStateTypeIds; QHash m_signalStrengthStateTypeIds; QHash m_versionStateTypeIds; QHash m_thingNodes; + // Get the endpoint for the given thing ZigbeeNodeEndpoint *findEndpoint(Thing *thing); + void createLightThing(const ThingClassId &thingClassId, const QUuid &networkUuid, ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint); + + // Action help methods since they work all the same + void executeAlertAction(ThingActionInfo *info, ZigbeeNodeEndpoint *endpoint); + void executePowerAction(ThingActionInfo *info, ZigbeeNodeEndpoint *endpoint, const StateTypeId &powerStateTypeId, bool power); + void executeBrightnessAction(ThingActionInfo *info, ZigbeeNodeEndpoint *endpoint, const StateTypeId &powerStateTypeId, const StateTypeId &brightnessStateTypeId, int brightness, quint8 level); + void executeColorTemperatureAction(ThingActionInfo *info, ZigbeeNodeEndpoint *endpoint, const StateTypeId &colorTemperatureStateTypeId, int colorTemperatureScaled); + + // Read state values from the node void readLightPowerState(Thing *thing); + void readLightLevelState(Thing *thing); + void readLightColorTemperatureState(Thing *thing); + + // Color temperature information handling + typedef struct ColorTemperatureRange { + quint16 minValue = 250; + quint16 maxValue = 450; + } ColorTemperatureRange; + + // Color temperature scaling range defined in + // all color temperature states/actions (the slider min/max) + int m_minScaleValue = 0; + int m_maxScaleValue = 200; + + QHash m_colorTemperatureRanges; + + void readColorTemperatureRange(Thing *thing); + bool readCachedColorTemperatureRange(Thing *thing, ZigbeeClusterColorControl *colorCluster); + quint16 mapScaledValueToColorTemperature(Thing *thing, int scaledColorTemperature); + int mapColorTemperatureToScaledValue(Thing *thing, quint16 colorTemperature); + }; #endif // INTEGRATIONPLUGINZIGBEEGENERICLIGHTS_H