/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
*
*
* Copyright 2013 - 2020, nymea GmbH
* Contact: contact@nymea.io
* This file is part of nymea.
* This project including source code and documentation is protected by
* copyright law, and remains the property of nymea GmbH. All rights, including
* reproduction, publication, editing and translation, are reserved. The use of
* this project is subject to the terms of a license agreement to be concluded
* with nymea GmbH in accordance with the terms of use of nymea GmbH, available
* under https://nymea.io/license
*
* GNU Lesser General Public License Usage
* Alternatively, this project may be redistributed and/or modified under the
* terms of the GNU Lesser General Public License as published by the Free
* Software Foundation; version 3. This project is distributed in the hope that
* it will be useful, but WITHOUT ANY WARRANTY; without even the implied
* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this project. If not, see .
*
* For any further details and any questions please contact us under
* contact@nymea.io or see our FAQ/Licensing Information on
* https://nymea.io/license/faq
*
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
#include "integrationpluginzigbeegeneric.h"
#include "plugininfo.h"
#include "hardware/zigbee/zigbeehardwareresource.h"
#include "zcl/hvac/zigbeeclusterthermostat.h"
#include
static QHash batteryLevelStateTypeIds = {
{thermostatThingClassId, thermostatBatteryLevelStateTypeId},
{doorLockThingClassId, doorLockBatteryLevelStateTypeId},
{doorSensorThingClassId, doorSensorBatteryLevelStateTypeId},
{motionSensorThingClassId, motionSensorBatteryLevelStateTypeId}
};
static QHash batteryCriticalStateTypeIds = {
{thermostatThingClassId, thermostatBatteryCriticalStateTypeId},
{doorLockThingClassId, doorLockBatteryCriticalStateTypeId},
{doorSensorThingClassId, doorSensorBatteryCriticalStateTypeId},
{motionSensorThingClassId, motionSensorBatteryCriticalStateTypeId}
};
static QHash ieeeAddressParamTypeIds = {
{thermostatThingClassId, thermostatThingIeeeAddressParamTypeId},
{powerSocketThingClassId, powerSocketThingIeeeAddressParamTypeId},
{doorLockThingClassId, doorLockThingIeeeAddressParamTypeId},
{doorSensorThingClassId, doorSensorThingIeeeAddressParamTypeId},
{motionSensorThingClassId, motionSensorThingIeeeAddressParamTypeId}
};
static QHash networkUuidParamTypeIds = {
{thermostatThingClassId, thermostatThingNetworkUuidParamTypeId},
{powerSocketThingClassId, powerSocketThingNetworkUuidParamTypeId},
{doorLockThingClassId, doorLockThingNetworkUuidParamTypeId},
{doorSensorThingClassId, doorSensorThingNetworkUuidParamTypeId},
{motionSensorThingClassId, motionSensorThingNetworkUuidParamTypeId}
};
static QHash endpointIdParamTypeIds = {
{thermostatThingClassId, thermostatThingEndpointIdParamTypeId},
{powerSocketThingClassId, powerSocketThingEndpointIdParamTypeId},
{doorLockThingClassId, doorLockThingEndpointIdParamTypeId},
{doorSensorThingClassId, doorSensorThingEndpointIdParamTypeId},
{motionSensorThingClassId, motionSensorThingEndpointIdParamTypeId}
};
static QHash modelIdParamTypeIds = {
{thermostatThingClassId, thermostatThingManufacturerParamTypeId},
{powerSocketThingClassId, powerSocketThingManufacturerParamTypeId},
{doorLockThingClassId, doorLockThingManufacturerParamTypeId},
{doorSensorThingClassId, doorSensorThingManufacturerParamTypeId},
{motionSensorThingClassId, motionSensorThingManufacturerParamTypeId}
};
static QHash manufacturerIdParamTypeIds = {
{thermostatThingClassId, thermostatThingModelParamTypeId},
{powerSocketThingClassId, powerSocketThingModelParamTypeId},
{doorLockThingClassId, doorLockThingModelParamTypeId},
{doorSensorThingClassId, doorSensorThingModelParamTypeId},
{motionSensorThingClassId, motionSensorThingModelParamTypeId}
};
static QHash connectedStateTypeIds = {
{thermostatThingClassId, thermostatConnectedStateTypeId},
{powerSocketThingClassId, powerSocketConnectedStateTypeId},
{doorLockThingClassId, doorLockConnectedStateTypeId},
{doorSensorThingClassId, doorSensorConnectedStateTypeId},
{motionSensorThingClassId, motionSensorConnectedStateTypeId}
};
static QHash signalStrengthStateTypeIds = {
{thermostatThingClassId, thermostatSignalStrengthStateTypeId},
{powerSocketThingClassId, powerSocketSignalStrengthStateTypeId},
{doorLockThingClassId, doorLockSignalStrengthStateTypeId},
{doorSensorThingClassId, doorSensorSignalStrengthStateTypeId},
{motionSensorThingClassId, motionSensorSignalStrengthStateTypeId}
};
static QHash versionStateTypeIds = {
{thermostatThingClassId, thermostatVersionStateTypeId},
{powerSocketThingClassId, powerSocketVersionStateTypeId},
{doorLockThingClassId, doorLockVersionStateTypeId}
};
IntegrationPluginZigbeeGeneric::IntegrationPluginZigbeeGeneric()
{
}
QString IntegrationPluginZigbeeGeneric::name() const
{
return "Generic";
}
bool IntegrationPluginZigbeeGeneric::handleNode(ZigbeeNode *node, const QUuid &networkUuid)
{
bool handled = false;
foreach (ZigbeeNodeEndpoint *endpoint, node->endpoints()) {
qCDebug(dcZigbeeGeneric()) << "Checking node endpoint:" << endpoint->endpointId() << endpoint->deviceId();
// Check thermostat
if (endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation &&
endpoint->deviceId() == Zigbee::HomeAutomationDeviceThermostat) {
qCDebug(dcZigbeeGeneric()) << "Handling thermostat endpoint for" << node << endpoint;
createThing(thermostatThingClassId, networkUuid, node, endpoint);
initThermostat(node, endpoint);
handled = true;
}
// Check on/off thing
if ((endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileLightLink && endpoint->deviceId() == Zigbee::LightLinkDevice::LightLinkDeviceOnOffPlugin) ||
(endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceOnOffPlugin) ||
(endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceMainPowerOutlet) ||
(endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceSmartPlug)) {
// Simple on/off device
if (endpoint->hasInputCluster(ZigbeeClusterLibrary::ClusterIdOnOff)) {
// FIXME: create powersocket with metering for SmartPlug device ID
qCDebug(dcZigbeeGeneric()) << "Handling power socket endpoint for" << node << endpoint;
createThing(powerSocketThingClassId, networkUuid, node, endpoint);
initSimplePowerSocket(node, endpoint);
handled = true;
}
}
// Check door lock
if (endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceDoorLock) {
if (!endpoint->hasInputCluster(ZigbeeClusterLibrary::ClusterIdPowerConfiguration) ||
!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()) << "Handling door lock endpoint for" << node << endpoint;
createThing(doorLockThingClassId, networkUuid, node, endpoint);
// Initialize bindings and cluster attributes
initDoorLock(node, endpoint);
handled = true;
}
}
// Security sensors
if (endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceIasZone) {
qCInfo(dcZigbeeGeneric()) << "IAS Zone device found!";
// We need to read the Type cluster to determine what this actually is...
ZigbeeClusterIasZone *iasZoneCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdIasZone);
ZigbeeClusterReply *reply = iasZoneCluster->readAttributes({ZigbeeClusterIasZone::AttributeZoneType});
connect(reply, &ZigbeeClusterReply::finished, this, [=](){
if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
qCWarning(dcZigbeeGeneric()) << "Reading IAS Zone type attribute finished with error" << reply->error();
return;
}
QList attributeStatusRecords = ZigbeeClusterLibrary::parseAttributeStatusRecords(reply->responseFrame().payload);
if (attributeStatusRecords.count() != 1 || attributeStatusRecords.first().attributeId != ZigbeeClusterIasZone::AttributeZoneType) {
qCWarning(dcZigbeeGeneric()) << "Unexpected reply in reading IAS Zone device type:" << attributeStatusRecords;
return;
}
initIASSensor(node, endpoint);
ZigbeeClusterLibrary::ReadAttributeStatusRecord iasZoneTypeRecord = attributeStatusRecords.first();
qCDebug(dcZigbeeGeneric()) << "IAS Zone device type:" << iasZoneTypeRecord.dataType.toUInt16();
switch (iasZoneTypeRecord.dataType.toUInt16()) {
case ZigbeeClusterIasZone::ZoneTypeContactSwitch:
qCInfo(dcZigbeeGeneric()) << "Creating contact switch thing";
createThing(doorSensorThingClassId, networkUuid, node, endpoint);
break;
case ZigbeeClusterIasZone::ZoneTypeMotionSensor:
qCInfo(dcZigbeeGeneric()) << "Creating motion sensor thing";
createThing(motionSensorThingClassId, networkUuid, node, endpoint);
break;
default:
qCWarning(dcZigbeeGeneric()) << "Unhandled IAS Zone device type:" << "0x" + QString::number(iasZoneTypeRecord.dataType.toUInt16(), 16);
}
});
handled = true;
}
}
return handled;
}
void IntegrationPluginZigbeeGeneric::handleRemoveNode(ZigbeeNode *node, const QUuid &networkUuid)
{
Q_UNUSED(networkUuid)
Thing *thing = m_thingNodes.key(node);
if (thing) {
qCDebug(dcZigbeeGeneric()) << node << "for" << thing << "has left the network.";
emit autoThingDisappeared(thing->id());
// Removing it from our map to prevent a loop that would ask the zigbee network to remove this node (see thingRemoved())
m_thingNodes.remove(thing);
}
}
void IntegrationPluginZigbeeGeneric::init()
{
hardwareManager()->zigbeeResource()->registerHandler(this, ZigbeeHardwareResource::HandlerTypeCatchAll);
}
void IntegrationPluginZigbeeGeneric::setupThing(ThingSetupInfo *info)
{
Thing *thing = info->thing();
QUuid networkUuid = thing->paramValue(networkUuidParamTypeIds.value(thing->thingClassId())).toUuid();
qCDebug(dcZigbeeGeneric()) << "Setting up generic zigbee thing";
ZigbeeAddress zigbeeAddress = ZigbeeAddress(thing->paramValue(ieeeAddressParamTypeIds.value(thing->thingClassId())).toString());
ZigbeeNode *node = m_thingNodes.value(thing);
if (!node) {
node = hardwareManager()->zigbeeResource()->claimNode(this, networkUuid, zigbeeAddress);
}
if (!node) {
qCWarning(dcZigbeeGeneric()) << "Zigbee node for" << info->thing()->name() << "not found.´";
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
m_thingNodes.insert(thing, node);
ZigbeeNodeEndpoint *endpoint = findEndpoint(thing);
if (!endpoint) {
qCWarning(dcZigbeeGeneric()) << "Could not find endpoint for" << thing;
info->finish(Thing::ThingErrorSetupFailed);
return;
}
// Update connected state
thing->setStateValue(connectedStateTypeIds.value(thing->thingClassId()), node->reachable());
connect(node, &ZigbeeNode::reachableChanged, thing, [thing, this](bool reachable){
thing->setStateValue(connectedStateTypeIds.value(thing->thingClassId()), reachable);
});
// Update signal strength
thing->setStateValue(signalStrengthStateTypeIds.value(thing->thingClassId()), qRound(node->lqi() * 100.0 / 255.0));
connect(node, &ZigbeeNode::lqiChanged, thing, [this, thing](quint8 lqi){
uint signalStrength = qRound(lqi * 100.0 / 255.0);
qCDebug(dcZigbeeGeneric()) << thing << "signal strength changed" << signalStrength << "%";
thing->setStateValue(signalStrengthStateTypeIds.value(thing->thingClassId()), signalStrength);
});
// Set the version
thing->setStateValue(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;
}
// 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 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) {
thing->setStateValue(thermostatTemperatureStateTypeId, record.dataType.toInt16() * 0.01);
}
if (record.attributeId == ZigbeeClusterThermostat::AttributeOccupiedHeatingSetpoint) {
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);
}
});
}
if (thing->thingClassId() == powerSocketThingClassId) {
ZigbeeClusterOnOff *onOffCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdOnOff);
if (onOffCluster) {
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);
});
connect(node, &ZigbeeNode::reachableChanged, thing, [=](bool reachable){
if (reachable) {
ZigbeeClusterReply *reply = onOffCluster->readAttributes({ZigbeeClusterOnOff::AttributeOnOff});
connect(reply, &ZigbeeClusterReply::finished, thing, [=](){
if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
qCWarning(dcZigbeeGeneric()) << "Reading attribute from" << thing << "finished with error" << reply->error();
}
// Note: the state will be updated using the power changed signal from the cluster
});
}
});
} else {
qCWarning(dcZigbeeGeneric()) << "Could not find the OnOff input cluster on" << thing << endpoint;
}
}
if (thing->thingClassId() == doorLockThingClassId) {
// Get door state changes
ZigbeeClusterDoorLock *doorLockCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdDoorLock);
if (!doorLockCluster) {
qCWarning(dcZigbeeGeneric()) << "Could not find door lock cluster on" << thing << endpoint;
} else {
// Only set the initial state if the attribute already exists
if (doorLockCluster->hasAttribute(ZigbeeClusterDoorLock::AttributeDoorState)) {
qCDebug(dcZigbeeGeneric()) << thing << doorLockCluster->doorState();
// TODO: check if we can use smart lock and set appropriate state
}
connect(doorLockCluster, &ZigbeeClusterDoorLock::lockStateChanged, thing, [=](ZigbeeClusterDoorLock::LockState lockState){
qCDebug(dcZigbeeGeneric()) << thing << "lock state changed" << lockState;
// TODO: check if we can use smart lock and set appropriate state
});
}
}
if (thing->thingClassId() == doorSensorThingClassId) {
ZigbeeClusterIasZone *iasZoneCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdIasZone);
if (!iasZoneCluster) {
qCWarning(dcZigbeeGeneric()) << "Could not find IAS zone cluster on" << thing << endpoint;
} else {
if (iasZoneCluster->hasAttribute(ZigbeeClusterIasZone::AttributeZoneStatus)) {
qCDebug(dcZigbeeGeneric()) << thing << iasZoneCluster->zoneStatus();
ZigbeeClusterIasZone::ZoneStatusFlags zoneStatus = iasZoneCluster->zoneStatus();
thing->setStateValue(doorSensorClosedStateTypeId, !zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm1) && !zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm2));
}
connect(iasZoneCluster, &ZigbeeClusterIasZone::zoneStatusChanged, thing, [=](ZigbeeClusterIasZone::ZoneStatusFlags zoneStatus, quint8 extendedStatus, quint8 zoneId, quint16 delays) {
qCDebug(dcZigbeeGeneric()) << "Zone status changed to:" << zoneStatus << extendedStatus << zoneId << delays;
thing->setStateValue(doorSensorClosedStateTypeId, !zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm1) && !zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm2));
});
}
}
if (thing->thingClassId() == motionSensorThingClassId) {
qCDebug(dcZigbeeGeneric()) << "Setting up motion sensor" << endpoint->endpointId();;
ZigbeeClusterIasZone *iasZoneCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdIasZone);
if (!iasZoneCluster) {
qCWarning(dcZigbeeGeneric()) << "Could not find IAS zone cluster on" << thing << endpoint;
} else {
qCDebug(dcZigbeeGeneric()) << "Cluster attributes:" << iasZoneCluster->attributes();
qCDebug(dcZigbeeGeneric()) << "Zone state:" << thing->name() << iasZoneCluster->zoneState();
qCDebug(dcZigbeeGeneric()) << "Zone type:" << thing->name() << iasZoneCluster->zoneType();
qCDebug(dcZigbeeGeneric()) << "Zone status:" << thing->name() << iasZoneCluster->zoneStatus();
if (iasZoneCluster->hasAttribute(ZigbeeClusterIasZone::AttributeZoneStatus)) {
ZigbeeClusterIasZone::ZoneStatusFlags zoneStatus = iasZoneCluster->zoneStatus();
thing->setStateValue(motionSensorIsPresentStateTypeId, zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm1) || zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm2));
thing->setStateValue(motionSensorTamperedStateTypeId, zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusTamper));
}
connect(iasZoneCluster, &ZigbeeClusterIasZone::zoneStatusChanged, thing, [=](ZigbeeClusterIasZone::ZoneStatusFlags zoneStatus, quint8 extendedStatus, quint8 zoneId, quint16 delays) {
qCDebug(dcZigbeeGeneric()) << "Zone status changed to:" << zoneStatus << extendedStatus << zoneId << delays;
thing->setStateValue(motionSensorIsPresentStateTypeId, zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm1) || zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusAlarm2));
thing->setStateValue(motionSensorTamperedStateTypeId, zoneStatus.testFlag(ZigbeeClusterIasZone::ZoneStatusTamper));
});
}
}
info->finish(Thing::ThingErrorNoError);
}
void IntegrationPluginZigbeeGeneric::executeAction(ThingActionInfo *info)
{
if (!hardwareManager()->zigbeeResource()->available()) {
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
// Get the node
Thing *thing = info->thing();
ZigbeeNode *node = m_thingNodes.value(thing);
if (!node->reachable()) {
info->finish(Thing::ThingErrorHardwareNotAvailable);
return;
}
// Get the endpoint
ZigbeeNodeEndpoint *endpoint = findEndpoint(thing);
if (!endpoint) {
info->finish(Thing::ThingErrorHardwareNotAvailable);
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);
if (!identifyCluster) {
qCWarning(dcZigbeeGeneric()) << "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);
}
});
return;
}
if (info->action().actionTypeId() == powerSocketPowerActionTypeId) {
ZigbeeClusterOnOff *onOffCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdOnOff);
if (!onOffCluster) {
qCWarning(dcZigbeeGeneric()) << "Could not find on/off cluster for" << thing << "in" << endpoint;
info->finish(Thing::ThingErrorHardwareFailure);
return;
}
// Send the command trough the network
bool power = info->action().param(powerSocketPowerActionPowerParamTypeId).value().toBool();
qCDebug(dcZigbeeGeneric()) << "Set power for" << 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(dcZigbeeGeneric()) << "Failed to set power on" << thing << reply->error();
info->finish(Thing::ThingErrorHardwareFailure);
} else {
info->finish(Thing::ThingErrorNoError);
qCDebug(dcZigbeeGeneric()) << "Set power finished successfully for" << thing;
thing->setStateValue(powerSocketPowerStateTypeId, power);
}
});
return;
}
}
if (thing->thingClassId() == doorLockThingClassId) {
if (info->action().actionTypeId() == doorLockOpenActionTypeId) {
ZigbeeClusterDoorLock *doorLockCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdDoorLock);
if (!doorLockCluster) {
qCWarning(dcZigbeeGeneric()) << "Could not find door lock cluster for" << thing << "in" << m_thingNodes.value(thing);
info->finish(Thing::ThingErrorHardwareFailure);
return;
}
// Send the command trough the network
ZigbeeClusterReply *reply = doorLockCluster->unlockDoor();
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);
}
});
return;
}
if (info->action().actionTypeId() == doorLockCloseActionTypeId) {
ZigbeeClusterDoorLock *doorLockCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdDoorLock);
if (!doorLockCluster) {
qCWarning(dcZigbeeGeneric()) << "Could not find door lock cluster for" << thing << "in" << m_thingNodes.value(thing);
info->finish(Thing::ThingErrorHardwareFailure);
return;
}
// Send the command trough the network
ZigbeeClusterReply *reply = doorLockCluster->lockDoor();
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);
}
});
return;
}
}
info->finish(Thing::ThingErrorUnsupportedFeature);
}
void IntegrationPluginZigbeeGeneric::thingRemoved(Thing *thing)
{
ZigbeeNode *node = m_thingNodes.take(thing);
if (node) {
QUuid networkUuid = thing->paramValue(networkUuidParamTypeIds.value(thing->thingClassId())).toUuid();
hardwareManager()->zigbeeResource()->removeNodeFromNetwork(networkUuid, node);
}
}
ZigbeeNodeEndpoint *IntegrationPluginZigbeeGeneric::findEndpoint(Thing *thing)
{
ZigbeeNode *node = m_thingNodes.value(thing);
if (!node) {
qCWarning(dcZigbeeGeneric()) << "Could not find the node for" << thing;
return nullptr;
}
quint8 endpointId = thing->paramValue(endpointIdParamTypeIds.value(thing->thingClassId())).toUInt();
return node->getEndpoint(endpointId);
}
void IntegrationPluginZigbeeGeneric::createThing(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(networkUuidParamTypeIds[thingClassId], networkUuid.toString()));
params.append(Param(ieeeAddressParamTypeIds[thingClassId], node->extendedAddress().toString()));
params.append(Param(endpointIdParamTypeIds[thingClassId], endpoint->endpointId()));
params.append(Param(modelIdParamTypeIds[thingClassId], endpoint->modelIdentifier()));
params.append(Param(manufacturerIdParamTypeIds[thingClassId], endpoint->manufacturerName()));
descriptor.setParams(params);
emit autoThingsAppeared({descriptor});
}
void IntegrationPluginZigbeeGeneric::initSimplePowerSocket(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
{
// Get the on/off server cluster from the endpoint
ZigbeeClusterOnOff *onOffCluster = endpoint->inputCluster(ZigbeeClusterLibrary::ClusterIdOnOff);
if (!onOffCluster)
return;
qCDebug(dcZigbeeGeneric()) << "Reading on/off power value for" << node << endpoint;
ZigbeeClusterReply *reply = onOffCluster->readAttributes({ZigbeeClusterOnOff::AttributeOnOff});
connect(reply, &ZigbeeClusterReply::finished, node, [=](){
if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
qCWarning(dcZigbeeGeneric()) << "Failed to read on/off cluster attribute from" << node << endpoint << reply->error();
return;
}
});
}
void IntegrationPluginZigbeeGeneric::initDoorLock(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
{
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()) << "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();
qCDebug(dcZigbeeGeneric()) << "Configuring attribute reporting for thermostat cluster";
ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdThermostat)->configureReporting({reportingOccupiedHeatingSetpointConfig});
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::initIASSensor(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
{
bindPowerConfigurationCluster(node, endpoint);
// First, bind the IAS cluster in a regular manner, for devices that don't fully implement the enrollment process:
qCDebug(dcZigbeeGeneric()) << "Binding IAS Zone cluster";
ZigbeeDeviceObjectReply *bindIasClusterReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdIasZone,
hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01);
connect(bindIasClusterReply, &ZigbeeDeviceObjectReply::finished, node, [=](){
if (bindIasClusterReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
qCWarning(dcZigbeeGeneric()) << "Failed to bind IAS zone cluster" << bindIasClusterReply->error();
} else {
qCDebug(dcZigbeeGeneric()) << "Binding IAS zone cluster finished successfully";
}
ZigbeeClusterLibrary::AttributeReportingConfiguration reportingStatusConfig;
reportingStatusConfig.attributeId = ZigbeeClusterIasZone::AttributeZoneStatus;
reportingStatusConfig.dataType = Zigbee::BitMap16;
reportingStatusConfig.minReportingInterval = 300;
reportingStatusConfig.maxReportingInterval = 2700;
reportingStatusConfig.reportableChange = ZigbeeDataType(static_cast(1)).data();
qCDebug(dcZigbeeGeneric()) << "Configuring attribute reporting for IAS Zone cluster";
ZigbeeClusterReply *reportingReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdIasZone)->configureReporting({reportingStatusConfig});
connect(reportingReply, &ZigbeeClusterReply::finished, this, [=](){
if (reportingReply->error() != ZigbeeClusterReply::ErrorNoError) {
qCWarning(dcZigbeeGeneric()) << "Failed to configure IAS Zone cluster status attribute reporting" << reportingReply->error();
} else {
qCDebug(dcZigbeeGeneric()) << "Attribute reporting configuration finished for IAS Zone cluster" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(reportingReply->responseFrame().payload);
}
// OK, now we've bound regularly, devices that require zone enrollment may still not send us anything, so let's try to enroll a zone
// For that we need to write our own IEEE address as the CIE (security zone master)
ZigbeeDataType dataType(hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()).toUInt64());
ZigbeeClusterLibrary::WriteAttributeRecord record;
record.attributeId = ZigbeeClusterIasZone::AttributeCieAddress;
record.dataType = Zigbee::IeeeAddress;
record.data = dataType.data();
qCDebug(dcZigbeeGeneric()) << "Setting CIE address" << hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()) << record.data;
ZigbeeClusterIasZone *iasZoneCluster = dynamic_cast(endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdIasZone));
ZigbeeClusterReply *writeCIEreply = iasZoneCluster->writeAttributes({record});
connect(writeCIEreply, &ZigbeeClusterReply::finished, this, [=](){
if (writeCIEreply->error() != ZigbeeClusterReply::ErrorNoError) {
qCWarning(dcZigbeeGeneric()) << "Failed to write CIE address to IAS server:" << writeCIEreply->error();
return;
}
qCDebug(dcZigbeeGeneric()) << "Wrote CIE address to IAS server:" << ZigbeeClusterLibrary::parseAttributeReportingStatusRecords(writeCIEreply->responseFrame().payload);
// Auto-Enroll-Response mechanism: We'll be sending an enroll response right away (without request) to try and enroll a zone
qCDebug(dcZigbeeGeneric()) << "Enrolling zone 0x42 to IAS server.";
ZigbeeClusterReply *enrollReply = iasZoneCluster->sendZoneEnrollResponse(0x42);
connect(enrollReply, &ZigbeeClusterReply::finished, this, [=](){
// Interestingly some devices stop regular conversation as soon as a zone is enrolled, so we might never get this reply...
qCDebug(dcZigbeeGeneric()) << "Zone enrollment reply:" << enrollReply->error() << enrollReply->responseData() << enrollReply->responseFrame();
});
// According to the spec, if Auto-Enroll-Response is implemented, also Trip-to-Pair is to be handled
connect(iasZoneCluster, &ZigbeeClusterIasZone::zoneEnrollRequest, this, [=](ZigbeeClusterIasZone::ZoneType zoneType, quint16 manufacturerCode){
// Accepting any zoneZype/manufacturercode
Q_UNUSED(zoneType)
Q_UNUSED(manufacturerCode)
iasZoneCluster->sendZoneEnrollResponse(0x42);
});
});
});
});
}
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));
});
}
}