From cbf9fa3420b0f24a53a7a002de126e97486a46df Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 14 Oct 2021 21:13:53 +0200 Subject: [PATCH] Zigbee generic: Add support for IAS based motion sensors --- .../integrationpluginzigbeegeneric.cpp | 109 +++++++++++++++--- .../integrationpluginzigbeegeneric.h | 4 +- .../integrationpluginzigbeegeneric.json | 101 ++++++++++++++++ 3 files changed, 196 insertions(+), 18 deletions(-) diff --git a/zigbeegeneric/integrationpluginzigbeegeneric.cpp b/zigbeegeneric/integrationpluginzigbeegeneric.cpp index 0947d148f..5aa6c83c4 100644 --- a/zigbeegeneric/integrationpluginzigbeegeneric.cpp +++ b/zigbeegeneric/integrationpluginzigbeegeneric.cpp @@ -40,62 +40,71 @@ static QHash batteryLevelStateTypeIds = { {thermostatThingClassId, thermostatBatteryLevelStateTypeId}, {doorLockThingClassId, doorLockBatteryLevelStateTypeId}, - {doorSensorThingClassId, doorSensorBatteryLevelStateTypeId} + {doorSensorThingClassId, doorSensorBatteryLevelStateTypeId}, + {motionSensorThingClassId, motionSensorBatteryLevelStateTypeId} }; static QHash batteryCriticalStateTypeIds = { {thermostatThingClassId, thermostatBatteryCriticalStateTypeId}, {doorLockThingClassId, doorLockBatteryCriticalStateTypeId}, - {doorSensorThingClassId, doorSensorBatteryCriticalStateTypeId} + {doorSensorThingClassId, doorSensorBatteryCriticalStateTypeId}, + {motionSensorThingClassId, motionSensorBatteryCriticalStateTypeId} }; static QHash ieeeAddressParamTypeIds = { {thermostatThingClassId, thermostatThingIeeeAddressParamTypeId}, {powerSocketThingClassId, powerSocketThingIeeeAddressParamTypeId}, {doorLockThingClassId, doorLockThingIeeeAddressParamTypeId}, - {doorSensorThingClassId, doorSensorThingIeeeAddressParamTypeId} + {doorSensorThingClassId, doorSensorThingIeeeAddressParamTypeId}, + {motionSensorThingClassId, motionSensorThingIeeeAddressParamTypeId} }; static QHash networkUuidParamTypeIds = { {thermostatThingClassId, thermostatThingNetworkUuidParamTypeId}, {powerSocketThingClassId, powerSocketThingNetworkUuidParamTypeId}, {doorLockThingClassId, doorLockThingNetworkUuidParamTypeId}, - {doorSensorThingClassId, doorSensorThingNetworkUuidParamTypeId} + {doorSensorThingClassId, doorSensorThingNetworkUuidParamTypeId}, + {motionSensorThingClassId, motionSensorThingNetworkUuidParamTypeId} }; static QHash endpointIdParamTypeIds = { {thermostatThingClassId, thermostatThingEndpointIdParamTypeId}, {powerSocketThingClassId, powerSocketThingEndpointIdParamTypeId}, {doorLockThingClassId, doorLockThingEndpointIdParamTypeId}, - {doorSensorThingClassId, doorSensorThingEndpointIdParamTypeId} + {doorSensorThingClassId, doorSensorThingEndpointIdParamTypeId}, + {motionSensorThingClassId, motionSensorThingEndpointIdParamTypeId} }; static QHash modelIdParamTypeIds = { {thermostatThingClassId, thermostatThingManufacturerParamTypeId}, {powerSocketThingClassId, powerSocketThingManufacturerParamTypeId}, {doorLockThingClassId, doorLockThingManufacturerParamTypeId}, - {doorSensorThingClassId, doorSensorThingManufacturerParamTypeId} + {doorSensorThingClassId, doorSensorThingManufacturerParamTypeId}, + {motionSensorThingClassId, motionSensorThingManufacturerParamTypeId} }; static QHash manufacturerIdParamTypeIds = { {thermostatThingClassId, thermostatThingModelParamTypeId}, {powerSocketThingClassId, powerSocketThingModelParamTypeId}, {doorLockThingClassId, doorLockThingModelParamTypeId}, - {doorSensorThingClassId, doorSensorThingModelParamTypeId} + {doorSensorThingClassId, doorSensorThingModelParamTypeId}, + {motionSensorThingClassId, motionSensorThingModelParamTypeId} }; static QHash connectedStateTypeIds = { {thermostatThingClassId, thermostatConnectedStateTypeId}, {powerSocketThingClassId, powerSocketConnectedStateTypeId}, {doorLockThingClassId, doorLockConnectedStateTypeId}, - {doorSensorThingClassId, doorSensorConnectedStateTypeId} + {doorSensorThingClassId, doorSensorConnectedStateTypeId}, + {motionSensorThingClassId, motionSensorConnectedStateTypeId} }; static QHash signalStrengthStateTypeIds = { {thermostatThingClassId, thermostatSignalStrengthStateTypeId}, {powerSocketThingClassId, powerSocketSignalStrengthStateTypeId}, {doorLockThingClassId, doorLockSignalStrengthStateTypeId}, - {doorSensorThingClassId, doorSensorSignalStrengthStateTypeId} + {doorSensorThingClassId, doorSensorSignalStrengthStateTypeId}, + {motionSensorThingClassId, motionSensorSignalStrengthStateTypeId} }; static QHash versionStateTypeIds = { @@ -160,8 +169,8 @@ bool IntegrationPluginZigbeeGeneric::handleNode(ZigbeeNode *node, const QUuid &n } // Security sensors - if (endpoint->profile() == Zigbee::ZigbeeProfile::ZigbeeProfileHomeAutomation && endpoint->deviceId() == Zigbee::HomeAutomationDeviceIsaZone) { - qCInfo(dcZigbeeGeneric()) << "ISA Zone device found!"; + 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}); @@ -176,13 +185,19 @@ bool IntegrationPluginZigbeeGeneric::handleNode(ZigbeeNode *node, const QUuid &n 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); - initDoorSensor(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); @@ -220,7 +235,7 @@ void IntegrationPluginZigbeeGeneric::setupThing(ThingSetupInfo *info) { Thing *thing = info->thing(); QUuid networkUuid = thing->paramValue(networkUuidParamTypeIds.value(thing->thingClassId())).toUuid(); - qCDebug(dcZigbeeGeneric()) << "Nework uuid:" << networkUuid; + 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) { @@ -381,6 +396,29 @@ void IntegrationPluginZigbeeGeneric::setupThing(ThingSetupInfo *info) } } + 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); } @@ -655,11 +693,12 @@ void IntegrationPluginZigbeeGeneric::initThermostat(ZigbeeNode *node, ZigbeeNode }); } -void IntegrationPluginZigbeeGeneric::initDoorSensor(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint) +void IntegrationPluginZigbeeGeneric::initIASSensor(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint) { bindPowerConfigurationCluster(node, endpoint); - qCDebug(dcZigbeeGeneric()) << "Binding IAS custer"; + // 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, [=](){ @@ -671,12 +710,12 @@ void IntegrationPluginZigbeeGeneric::initDoorSensor(ZigbeeNode *node, ZigbeeNode ZigbeeClusterLibrary::AttributeReportingConfiguration reportingStatusConfig; reportingStatusConfig.attributeId = ZigbeeClusterIasZone::AttributeZoneStatus; - reportingStatusConfig.dataType = Zigbee::Int16; + reportingStatusConfig.dataType = Zigbee::BitMap16; reportingStatusConfig.minReportingInterval = 300; reportingStatusConfig.maxReportingInterval = 2700; reportingStatusConfig.reportableChange = ZigbeeDataType(static_cast(1)).data(); - qCDebug(dcZigbeeGeneric()) << "Configuring attribute reporting for thermostat cluster"; + 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) { @@ -684,6 +723,42 @@ void IntegrationPluginZigbeeGeneric::initDoorSensor(ZigbeeNode *node, ZigbeeNode } 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); + }); + }); }); }); } diff --git a/zigbeegeneric/integrationpluginzigbeegeneric.h b/zigbeegeneric/integrationpluginzigbeegeneric.h index 05f179a54..a894be0fe 100644 --- a/zigbeegeneric/integrationpluginzigbeegeneric.h +++ b/zigbeegeneric/integrationpluginzigbeegeneric.h @@ -37,6 +37,8 @@ #include +#include "extern-plugininfo.h" + class IntegrationPluginZigbeeGeneric: public IntegrationPlugin, public ZigbeeHandler { Q_OBJECT @@ -66,7 +68,7 @@ class IntegrationPluginZigbeeGeneric: public IntegrationPlugin, public ZigbeeHan void initSimplePowerSocket(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint); void initDoorLock(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint); void initThermostat(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint); - void initDoorSensor(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint); + void initIASSensor(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint); void bindPowerConfigurationCluster(ZigbeeNode *node, ZigbeeNodeEndpoint *endpoint); diff --git a/zigbeegeneric/integrationpluginzigbeegeneric.json b/zigbeegeneric/integrationpluginzigbeegeneric.json index c69c137ac..6be7305b7 100644 --- a/zigbeegeneric/integrationpluginzigbeegeneric.json +++ b/zigbeegeneric/integrationpluginzigbeegeneric.json @@ -436,6 +436,107 @@ "defaultValue": 0 } ] + }, + { + "id": "500a8b65-ad34-4bf0-a35d-c167510999f2", + "name": "motionSensor", + "displayName": "Motion sensor", + "interfaces": ["presencesensor"], + "paramTypes": [ + { + "id": "e1048378-8d3d-40ae-a2d4-070e291b9db8", + "name": "ieeeAddress", + "displayName": "IEEE adress", + "type": "QString", + "defaultValue": "00:00:00:00:00:00:00:00" + }, + { + "id": "85e46994-e9d6-4c20-99e4-feb609ef25b1", + "name": "networkUuid", + "displayName": "Zigbee network UUID", + "type": "QString", + "defaultValue": "" + }, + { + "id": "22a362b6-3cbe-421a-a20d-0f491a4150cf", + "name": "endpointId", + "displayName": "Endpoint ID", + "type": "uint", + "defaultValue": 1 + }, + { + "id": "27a158b8-5a29-4d80-8661-4027652c55c7", + "name": "manufacturer", + "displayName": "Manufacturer", + "type": "QString", + "defaultValue": "" + }, + { + "id": "63910764-55d4-4b67-a1d7-a568c269abd9", + "name": "model", + "displayName": "Model", + "type": "QString", + "defaultValue": "" + } + ], + "stateTypes": [ + { + "id": "73523dee-93c1-4143-abff-7685b2d1bb1c", + "name": "isPresent", + "displayName": "Presence detected", + "displayNameEvent": "Presence detected changed", + "type": "bool", + "defaultValue": "false", + "cached": false + }, + { + "id": "c9c0343a-e396-4a21-ab7f-5f3d88cc55c5", + "name": "tampered", + "displayName": "Tampered", + "displayNameEvent": "Tampered changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "4ed91f61-298f-411a-a1b8-403d77eecf34", + "name": "batteryLevel", + "displayName": "Battery level", + "displayNameEvent": "Battery level changed", + "type": "int", + "minValue": 0, + "maxValue": 100, + "unit": "Percentage", + "defaultValue": 0 + }, + { + "id": "ca81d872-dcf0-430d-8b00-98e6e4e4ccf4", + "name": "batteryCritical", + "displayName": "Battery critical", + "displayNameEvent": "Battery critical changed", + "type": "bool", + "defaultValue": false + }, + { + "id": "5d12e9da-2f5c-4d40-a20a-b5d378fd0387", + "name": "connected", + "displayName": "Connected", + "displayNameEvent": "Connected or disconnected", + "type": "bool", + "defaultValue": false, + "cached": false + }, + { + "id": "5062aac1-aa2d-4a35-9435-482ff45a4d02", + "name": "signalStrength", + "displayName": "Signal strength", + "displayNameEvent": "Signal strength changed", + "type": "uint", + "minValue": 0, + "maxValue": 100, + "unit": "Percentage", + "defaultValue": 0 + } + ] } ] }