Zigbee: Complete support for ZigBee Thermostats (generic)
This commit is contained in:
parent
b97b7c3ee3
commit
56ed62b730
@ -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.
|
> 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
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
@ -37,6 +37,16 @@
|
|||||||
|
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
|
|
||||||
|
QHash<ThingClassId, StateTypeId> batteryLevelStateTypeIds = {
|
||||||
|
{thermostatThingClassId, thermostatBatteryLevelStateTypeId},
|
||||||
|
{doorLockThingClassId, doorLockBatteryLevelStateTypeId}
|
||||||
|
};
|
||||||
|
|
||||||
|
QHash<ThingClassId, StateTypeId> batteryCriticalStateTypeIds = {
|
||||||
|
{thermostatThingClassId, thermostatBatteryCriticalStateTypeId},
|
||||||
|
{doorLockThingClassId, doorLockBatteryCriticalStateTypeId}
|
||||||
|
};
|
||||||
|
|
||||||
IntegrationPluginZigbeeGeneric::IntegrationPluginZigbeeGeneric()
|
IntegrationPluginZigbeeGeneric::IntegrationPluginZigbeeGeneric()
|
||||||
{
|
{
|
||||||
m_ieeeAddressParamTypeIds[thermostatThingClassId] = thermostatThingIeeeAddressParamTypeId;
|
m_ieeeAddressParamTypeIds[thermostatThingClassId] = thermostatThingIeeeAddressParamTypeId;
|
||||||
@ -86,8 +96,9 @@ bool IntegrationPluginZigbeeGeneric::handleNode(ZigbeeNode *node, const QUuid &n
|
|||||||
// Check thermostat
|
// Check thermostat
|
||||||
if (endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation &&
|
if (endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation &&
|
||||||
endpoint->deviceId() == Zigbee::HomeAutomationDeviceThermostat) {
|
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);
|
createThing(thermostatThingClassId, networkUuid, node, endpoint);
|
||||||
|
initThermostat(node, endpoint);
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +111,7 @@ bool IntegrationPluginZigbeeGeneric::handleNode(ZigbeeNode *node, const QUuid &n
|
|||||||
// Simple on/off device
|
// Simple on/off device
|
||||||
if (endpoint->hasInputCluster(ZigbeeClusterLibrary::ClusterIdOnOff)) {
|
if (endpoint->hasInputCluster(ZigbeeClusterLibrary::ClusterIdOnOff)) {
|
||||||
// FIXME: create powersocket with metering for SmartPlug device ID
|
// 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);
|
createThing(powerSocketThingClassId, networkUuid, node, endpoint);
|
||||||
initSimplePowerSocket(node, endpoint);
|
initSimplePowerSocket(node, endpoint);
|
||||||
handled = true;
|
handled = true;
|
||||||
@ -113,7 +124,7 @@ bool IntegrationPluginZigbeeGeneric::handleNode(ZigbeeNode *node, const QUuid &n
|
|||||||
!endpoint->hasInputCluster(ZigbeeClusterLibrary::ClusterIdDoorLock)) {
|
!endpoint->hasInputCluster(ZigbeeClusterLibrary::ClusterIdDoorLock)) {
|
||||||
qCWarning(dcZigbeeGeneric()) << "Endpoint claims to be a door lock, but the appropriate input clusters could not be found" << node << endpoint;
|
qCWarning(dcZigbeeGeneric()) << "Endpoint claims to be a door lock, but the appropriate input clusters could not be found" << node << endpoint;
|
||||||
} else {
|
} else {
|
||||||
qCDebug(dcZigbeeGeneric()) << "Handeling door lock endpoint for" << node << endpoint;
|
qCDebug(dcZigbeeGeneric()) << "Handling door lock endpoint for" << node << endpoint;
|
||||||
createThing(doorLockThingClassId, networkUuid, node, endpoint);
|
createThing(doorLockThingClassId, networkUuid, node, endpoint);
|
||||||
// Initialize bindings and cluster attributes
|
// Initialize bindings and cluster attributes
|
||||||
initializeDoorLock(node, endpoint);
|
initializeDoorLock(node, endpoint);
|
||||||
@ -185,49 +196,62 @@ void IntegrationPluginZigbeeGeneric::setupThing(ThingSetupInfo *info)
|
|||||||
// Set the version
|
// Set the version
|
||||||
thing->setStateValue(m_versionStateTypeIds.value(thing->thingClassId()), endpoint->softwareBuildId());
|
thing->setStateValue(m_versionStateTypeIds.value(thing->thingClassId()), endpoint->softwareBuildId());
|
||||||
|
|
||||||
|
if (batteryLevelStateTypeIds.contains(thing->thingClassId())) {
|
||||||
|
connectToPowerConfigurationCluster(thing, endpoint);
|
||||||
|
}
|
||||||
|
|
||||||
// Type specific setup
|
// Type specific setup
|
||||||
if (thing->thingClassId() == thermostatThingClassId) {
|
if (thing->thingClassId() == thermostatThingClassId) {
|
||||||
ZigbeeClusterThermostat *thermostatCluster = endpoint->inputCluster<ZigbeeClusterThermostat>(ZigbeeClusterLibrary::ClusterIdThermostat);
|
ZigbeeClusterThermostat *thermostatCluster = endpoint->inputCluster<ZigbeeClusterThermostat>(ZigbeeClusterLibrary::ClusterIdThermostat);
|
||||||
if (!thermostatCluster) {
|
if (!thermostatCluster) {
|
||||||
qCWarning(dcZigbeeGeneric()) << "Failed to read thermostat cluster";
|
qCWarning(dcZigbeeGeneric()) << "Failed to read thermostat cluster";
|
||||||
|
info->finish(Thing::ThingErrorHardwareFailure);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// thermostatCluster->attribute(ZigbeeClusterLibrary::ClusterIdThermostat);
|
|
||||||
|
|
||||||
// We need to read them from the lamp
|
// Read initial attribute values
|
||||||
ZigbeeClusterReply *reply = thermostatCluster->readAttributes({ZigbeeClusterThermostat::AttributeLocalTemperature, ZigbeeClusterThermostat::AttributeOccupiedHeatingSetpoint});
|
ZigbeeClusterReply *reply = thermostatCluster->readAttributes({ZigbeeClusterThermostat::AttributeLocalTemperature,
|
||||||
|
ZigbeeClusterThermostat::AttributeOccupiedHeatingSetpoint,
|
||||||
|
ZigbeeClusterThermostat::AttributePIHeatingDemand,
|
||||||
|
ZigbeeClusterThermostat::AttributePICoolingDemand});
|
||||||
connect(reply, &ZigbeeClusterReply::finished, thing, [=](){
|
connect(reply, &ZigbeeClusterReply::finished, thing, [=](){
|
||||||
if (reply->error() != ZigbeeClusterReply::ErrorNoError) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<ZigbeeClusterLibrary::ReadAttributeStatusRecord> attributeStatusRecords = ZigbeeClusterLibrary::parseAttributeStatusRecords(reply->responseFrame().payload);
|
QList<ZigbeeClusterLibrary::ReadAttributeStatusRecord> attributeStatusRecords = ZigbeeClusterLibrary::parseAttributeStatusRecords(reply->responseFrame().payload);
|
||||||
|
|
||||||
foreach (const ZigbeeClusterLibrary::ReadAttributeStatusRecord &record, attributeStatusRecords) {
|
foreach (const ZigbeeClusterLibrary::ReadAttributeStatusRecord &record, attributeStatusRecords) {
|
||||||
if (record.attributeId == ZigbeeClusterThermostat::AttributeLocalTemperature) {
|
if (record.attributeId == ZigbeeClusterThermostat::AttributeLocalTemperature) {
|
||||||
bool valueOk = false;
|
thing->setStateValue(thermostatTemperatureStateTypeId, record.dataType.toInt16() * 0.01);
|
||||||
quint16 localTemperature = record.dataType.toUInt16(&valueOk);
|
|
||||||
if (!valueOk) {
|
|
||||||
qCWarning(dcZigbeeGeneric()) << "Failed to read local temperature" << attributeStatusRecords;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
thing->setStateValue(thermostatTemperatureStateTypeId, localTemperature * 0.01);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (record.attributeId == ZigbeeClusterThermostat::AttributeOccupiedHeatingSetpoint) {
|
if (record.attributeId == ZigbeeClusterThermostat::AttributeOccupiedHeatingSetpoint) {
|
||||||
bool valueOk = false;
|
thing->setStateValue(thermostatTargetTemperatureStateTypeId, record.dataType.toInt16() * 0.01);
|
||||||
quint16 targetTemperature = record.dataType.toUInt16(&valueOk);
|
}
|
||||||
if (!valueOk) {
|
if (record.attributeId == ZigbeeClusterThermostat::AttributePIHeatingDemand) {
|
||||||
qCWarning(dcZigbeeGeneric()) << "Failed to read local temperature" << attributeStatusRecords;
|
thing->setStateValue(thermostatHeatingOnStateTypeId, record.dataType.toUInt8() > 0);
|
||||||
return;
|
}
|
||||||
}
|
if (record.attributeId == ZigbeeClusterThermostat::AttributePICoolingDemand) {
|
||||||
thing->setStateValue(thermostatTargetTemperatureStateTypeId, targetTemperature * 0.01);
|
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)) {
|
if (onOffCluster->hasAttribute(ZigbeeClusterOnOff::AttributeOnOff)) {
|
||||||
thing->setStateValue(powerSocketPowerStateTypeId, onOffCluster->power());
|
thing->setStateValue(powerSocketPowerStateTypeId, onOffCluster->power());
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(onOffCluster, &ZigbeeClusterOnOff::powerChanged, thing, [thing](bool power){
|
connect(onOffCluster, &ZigbeeClusterOnOff::powerChanged, thing, [thing](bool power){
|
||||||
qCDebug(dcZigbeeGeneric()) << thing << "power changed" << power;
|
qCDebug(dcZigbeeGeneric()) << thing << "power changed" << power;
|
||||||
thing->setStateValue(powerSocketPowerStateTypeId, power);
|
thing->setStateValue(powerSocketPowerStateTypeId, power);
|
||||||
@ -261,24 +284,6 @@ void IntegrationPluginZigbeeGeneric::setupThing(ThingSetupInfo *info)
|
|||||||
|
|
||||||
if (thing->thingClassId() == doorLockThingClassId) {
|
if (thing->thingClassId() == doorLockThingClassId) {
|
||||||
|
|
||||||
// Get battery level changes
|
|
||||||
ZigbeeClusterPowerConfiguration *powerCluster = endpoint->inputCluster<ZigbeeClusterPowerConfiguration>(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
|
// Get door state changes
|
||||||
ZigbeeClusterDoorLock *doorLockCluster = endpoint->inputCluster<ZigbeeClusterDoorLock>(ZigbeeClusterLibrary::ClusterIdDoorLock);
|
ZigbeeClusterDoorLock *doorLockCluster = endpoint->inputCluster<ZigbeeClusterDoorLock>(ZigbeeClusterLibrary::ClusterIdDoorLock);
|
||||||
if (!doorLockCluster) {
|
if (!doorLockCluster) {
|
||||||
@ -322,6 +327,39 @@ void IntegrationPluginZigbeeGeneric::executeAction(ThingActionInfo *info)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (thing->thingClassId() == thermostatThingClassId) {
|
||||||
|
if (info->action().actionTypeId() == thermostatTargetTemperatureActionTypeId) {
|
||||||
|
ZigbeeClusterThermostat *thermostatCluster = endpoint->inputCluster<ZigbeeClusterThermostat>(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<ZigbeeClusterLibrary::WriteAttributeRecord> 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 (thing->thingClassId() == powerSocketThingClassId) {
|
||||||
if (info->action().actionTypeId() == powerSocketAlertActionTypeId) {
|
if (info->action().actionTypeId() == powerSocketAlertActionTypeId) {
|
||||||
ZigbeeClusterIdentify *identifyCluster = endpoint->inputCluster<ZigbeeClusterIdentify>(ZigbeeClusterLibrary::ClusterIdIdentify);
|
ZigbeeClusterIdentify *identifyCluster = endpoint->inputCluster<ZigbeeClusterIdentify>(ZigbeeClusterLibrary::ClusterIdIdentify);
|
||||||
@ -474,82 +512,132 @@ void IntegrationPluginZigbeeGeneric::initSimplePowerSocket(ZigbeeNode *node, Zig
|
|||||||
|
|
||||||
void IntegrationPluginZigbeeGeneric::initializeDoorLock(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
|
void IntegrationPluginZigbeeGeneric::initializeDoorLock(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint)
|
||||||
{
|
{
|
||||||
qCDebug(dcZigbeeGeneric()) << "Read power configuration cluster attributes" << node;
|
bindPowerConfigurationCluster(node, endpoint);
|
||||||
ZigbeeClusterReply *readAttributeReply = endpoint->getInputCluster(ZigbeeClusterLibrary::ClusterIdPowerConfiguration)->readAttributes({ZigbeeClusterPowerConfiguration::AttributeBatteryPercentageRemaining});
|
|
||||||
connect(readAttributeReply, &ZigbeeClusterReply::finished, node, [=](){
|
qCDebug(dcZigbeeGeneric()) << "Binding door lock cluster ";
|
||||||
if (readAttributeReply->error() != ZigbeeClusterReply::ErrorNoError) {
|
ZigbeeDeviceObjectReply * zdoReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdDoorLock, hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01);
|
||||||
qCWarning(dcZigbeeGeneric()) << "Failed to read power configuration cluster attributes" << readAttributeReply->error();
|
connect(zdoReply, &ZigbeeDeviceObjectReply::finished, node, [=](){
|
||||||
|
if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
|
||||||
|
qCWarning(dcZigbeeGeneric()) << "Failed to door lock cluster to coordinator" << zdoReply->error();
|
||||||
} else {
|
} 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
|
// Configure attribute reporting for lock state
|
||||||
qCDebug(dcZigbeeGeneric()) << "Bind power configuration cluster to coordinator IEEE address";
|
ZigbeeClusterLibrary::AttributeReportingConfiguration reportingConfig;
|
||||||
ZigbeeDeviceObjectReply * zdoReply = node->deviceObject()->requestBindIeeeAddress(endpoint->endpointId(), ZigbeeClusterLibrary::ClusterIdPowerConfiguration, hardwareManager()->zigbeeResource()->coordinatorAddress(node->networkUuid()), 0x01);
|
reportingConfig.attributeId = ZigbeeClusterDoorLock::AttributeLockState;
|
||||||
connect(zdoReply, &ZigbeeDeviceObjectReply::finished, node, [=](){
|
reportingConfig.dataType = Zigbee::Enum8;
|
||||||
if (zdoReply->error() != ZigbeeDeviceObjectReply::ErrorNoError) {
|
reportingConfig.minReportingInterval = 60;
|
||||||
qCWarning(dcZigbeeGeneric()) << "Failed to bind power cluster to coordinator" << zdoReply->error();
|
reportingConfig.maxReportingInterval = 120;
|
||||||
|
reportingConfig.reportableChange = ZigbeeDataType(static_cast<quint8>(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 {
|
} 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<quint8>(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<quint8>(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<quint8>(1)).data();
|
||||||
|
|
||||||
|
ZigbeeClusterLibrary::AttributeReportingConfiguration reportingBatteryPercentageConfig;
|
||||||
|
reportingBatteryPercentageConfig.attributeId = ZigbeeClusterPowerConfiguration::AttributeBatteryPercentageRemaining;
|
||||||
|
reportingBatteryPercentageConfig.dataType = Zigbee::Uint8;
|
||||||
|
reportingBatteryPercentageConfig.minReportingInterval = 300;
|
||||||
|
reportingBatteryPercentageConfig.maxReportingInterval = 2700;
|
||||||
|
reportingBatteryPercentageConfig.reportableChange = ZigbeeDataType(static_cast<quint8>(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<quint8>(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<ZigbeeClusterPowerConfiguration>(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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -75,7 +75,11 @@ private:
|
|||||||
|
|
||||||
void initSimplePowerSocket(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint);
|
void initSimplePowerSocket(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint);
|
||||||
void initializeDoorLock(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
|
#endif // INTEGRATIONPLUGINZIGBEEGENERIC_H
|
||||||
|
|||||||
@ -13,7 +13,7 @@
|
|||||||
"name": "thermostat",
|
"name": "thermostat",
|
||||||
"displayName": "Zigbee Thermostat",
|
"displayName": "Zigbee Thermostat",
|
||||||
"createMethods": ["auto"],
|
"createMethods": ["auto"],
|
||||||
"interfaces": ["thermostat", "temperaturesensor", "wirelessconnectable"],
|
"interfaces": ["thermostat", "temperaturesensor", "batterylevel", "wirelessconnectable"],
|
||||||
"paramTypes": [
|
"paramTypes": [
|
||||||
{
|
{
|
||||||
"id": "f38746d8-0084-43a3-b645-3ec743ea5fbc",
|
"id": "f38746d8-0084-43a3-b645-3ec743ea5fbc",
|
||||||
@ -57,11 +57,13 @@
|
|||||||
"name": "targetTemperature",
|
"name": "targetTemperature",
|
||||||
"displayName": "Target temperature",
|
"displayName": "Target temperature",
|
||||||
"displayNameEvent": "Target temperature changed",
|
"displayNameEvent": "Target temperature changed",
|
||||||
|
"displayNameAction": "Set target temperature",
|
||||||
"type": "double",
|
"type": "double",
|
||||||
"unit": "DegreeCelsius",
|
"unit": "DegreeCelsius",
|
||||||
"minValue": 0,
|
"minValue": 0,
|
||||||
"maxValue": 50,
|
"maxValue": 50,
|
||||||
"defaultValue": 0
|
"defaultValue": 0,
|
||||||
|
"writable": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "497af03a-a893-438c-aba2-1bf3ecfc66c5",
|
"id": "497af03a-a893-438c-aba2-1bf3ecfc66c5",
|
||||||
@ -72,6 +74,22 @@
|
|||||||
"unit": "DegreeCelsius",
|
"unit": "DegreeCelsius",
|
||||||
"defaultValue": 0
|
"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",
|
"id": "e9fb2b10-96da-4b70-afda-46e948399af8",
|
||||||
"name": "connected",
|
"name": "connected",
|
||||||
@ -100,6 +118,25 @@
|
|||||||
"type": "QString",
|
"type": "QString",
|
||||||
"cached": true,
|
"cached": true,
|
||||||
"defaultValue": ""
|
"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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@ -97,7 +97,7 @@ bool IntegrationPluginZigbeeGenericLights::handleNode(ZigbeeNode *node, const QU
|
|||||||
(endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation &&
|
(endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation &&
|
||||||
endpoint->deviceId() == Zigbee::HomeAutomationDeviceOnOffLight)) {
|
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);
|
createLightThing(onOffLightThingClassId, networkUuid, node, endpoint);
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
@ -107,7 +107,7 @@ bool IntegrationPluginZigbeeGenericLights::handleNode(ZigbeeNode *node, const QU
|
|||||||
(endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation &&
|
(endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation &&
|
||||||
endpoint->deviceId() == Zigbee::HomeAutomationDeviceDimmableLight)) {
|
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);
|
createLightThing(dimmableLightThingClassId, networkUuid, node, endpoint);
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
@ -118,7 +118,7 @@ bool IntegrationPluginZigbeeGenericLights::handleNode(ZigbeeNode *node, const QU
|
|||||||
(endpoint->profile() == Zigbee::ZigbeeProfileHomeAutomation &&
|
(endpoint->profile() == Zigbee::ZigbeeProfileHomeAutomation &&
|
||||||
endpoint->deviceId() == Zigbee::HomeAutomationDeviceColourTemperatureLight)) {
|
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);
|
createLightThing(colorTemperatureLightThingClassId, networkUuid, node, endpoint);
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
@ -127,7 +127,7 @@ bool IntegrationPluginZigbeeGenericLights::handleNode(ZigbeeNode *node, const QU
|
|||||||
(endpoint->profile() == Zigbee::ZigbeeProfileLightLink && endpoint->deviceId() == Zigbee::LightLinkDeviceExtendedColourLight) ||
|
(endpoint->profile() == Zigbee::ZigbeeProfileLightLink && endpoint->deviceId() == Zigbee::LightLinkDeviceExtendedColourLight) ||
|
||||||
(endpoint->profile() == Zigbee::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceExtendedColourLight)) {
|
(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);
|
createLightThing(colorLightThingClassId, networkUuid, node, endpoint);
|
||||||
handled = true;
|
handled = true;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user