Zigbee generic: Add support for IAS based motion sensors
This commit is contained in:
parent
469fa10a19
commit
a9afd4a501
@ -26,6 +26,10 @@ Radiator thermostats that follow the ZigBee specification.
|
||||
|
||||
Door/window that follow the ZigBee IAS Zone specification.
|
||||
|
||||
### Motion sensors
|
||||
|
||||
Door/window that follow the ZigBee IAS Zone specification.
|
||||
|
||||
## Requirements
|
||||
|
||||
* A compatible ZigBee controller and a running ZigBee network in nymea. You can find more information about supported controllers and ZigBee network configurations [here](https://nymea.io/documentation/users/usage/configuration#zigbee).
|
||||
|
||||
@ -40,62 +40,71 @@
|
||||
static QHash<ThingClassId, StateTypeId> batteryLevelStateTypeIds = {
|
||||
{thermostatThingClassId, thermostatBatteryLevelStateTypeId},
|
||||
{doorLockThingClassId, doorLockBatteryLevelStateTypeId},
|
||||
{doorSensorThingClassId, doorSensorBatteryLevelStateTypeId}
|
||||
{doorSensorThingClassId, doorSensorBatteryLevelStateTypeId},
|
||||
{motionSensorThingClassId, motionSensorBatteryLevelStateTypeId}
|
||||
};
|
||||
|
||||
static QHash<ThingClassId, StateTypeId> batteryCriticalStateTypeIds = {
|
||||
{thermostatThingClassId, thermostatBatteryCriticalStateTypeId},
|
||||
{doorLockThingClassId, doorLockBatteryCriticalStateTypeId},
|
||||
{doorSensorThingClassId, doorSensorBatteryCriticalStateTypeId}
|
||||
{doorSensorThingClassId, doorSensorBatteryCriticalStateTypeId},
|
||||
{motionSensorThingClassId, motionSensorBatteryCriticalStateTypeId}
|
||||
};
|
||||
|
||||
static QHash<ThingClassId, ParamTypeId> ieeeAddressParamTypeIds = {
|
||||
{thermostatThingClassId, thermostatThingIeeeAddressParamTypeId},
|
||||
{powerSocketThingClassId, powerSocketThingIeeeAddressParamTypeId},
|
||||
{doorLockThingClassId, doorLockThingIeeeAddressParamTypeId},
|
||||
{doorSensorThingClassId, doorSensorThingIeeeAddressParamTypeId}
|
||||
{doorSensorThingClassId, doorSensorThingIeeeAddressParamTypeId},
|
||||
{motionSensorThingClassId, motionSensorThingIeeeAddressParamTypeId}
|
||||
};
|
||||
|
||||
static QHash<ThingClassId, ParamTypeId> networkUuidParamTypeIds = {
|
||||
{thermostatThingClassId, thermostatThingNetworkUuidParamTypeId},
|
||||
{powerSocketThingClassId, powerSocketThingNetworkUuidParamTypeId},
|
||||
{doorLockThingClassId, doorLockThingNetworkUuidParamTypeId},
|
||||
{doorSensorThingClassId, doorSensorThingNetworkUuidParamTypeId}
|
||||
{doorSensorThingClassId, doorSensorThingNetworkUuidParamTypeId},
|
||||
{motionSensorThingClassId, motionSensorThingNetworkUuidParamTypeId}
|
||||
};
|
||||
|
||||
static QHash<ThingClassId, ParamTypeId> endpointIdParamTypeIds = {
|
||||
{thermostatThingClassId, thermostatThingEndpointIdParamTypeId},
|
||||
{powerSocketThingClassId, powerSocketThingEndpointIdParamTypeId},
|
||||
{doorLockThingClassId, doorLockThingEndpointIdParamTypeId},
|
||||
{doorSensorThingClassId, doorSensorThingEndpointIdParamTypeId}
|
||||
{doorSensorThingClassId, doorSensorThingEndpointIdParamTypeId},
|
||||
{motionSensorThingClassId, motionSensorThingEndpointIdParamTypeId}
|
||||
};
|
||||
|
||||
static QHash<ThingClassId, ParamTypeId> modelIdParamTypeIds = {
|
||||
{thermostatThingClassId, thermostatThingManufacturerParamTypeId},
|
||||
{powerSocketThingClassId, powerSocketThingManufacturerParamTypeId},
|
||||
{doorLockThingClassId, doorLockThingManufacturerParamTypeId},
|
||||
{doorSensorThingClassId, doorSensorThingManufacturerParamTypeId}
|
||||
{doorSensorThingClassId, doorSensorThingManufacturerParamTypeId},
|
||||
{motionSensorThingClassId, motionSensorThingManufacturerParamTypeId}
|
||||
};
|
||||
|
||||
static QHash<ThingClassId, ParamTypeId> manufacturerIdParamTypeIds = {
|
||||
{thermostatThingClassId, thermostatThingModelParamTypeId},
|
||||
{powerSocketThingClassId, powerSocketThingModelParamTypeId},
|
||||
{doorLockThingClassId, doorLockThingModelParamTypeId},
|
||||
{doorSensorThingClassId, doorSensorThingModelParamTypeId}
|
||||
{doorSensorThingClassId, doorSensorThingModelParamTypeId},
|
||||
{motionSensorThingClassId, motionSensorThingModelParamTypeId}
|
||||
};
|
||||
|
||||
static QHash<ThingClassId, StateTypeId> connectedStateTypeIds = {
|
||||
{thermostatThingClassId, thermostatConnectedStateTypeId},
|
||||
{powerSocketThingClassId, powerSocketConnectedStateTypeId},
|
||||
{doorLockThingClassId, doorLockConnectedStateTypeId},
|
||||
{doorSensorThingClassId, doorSensorConnectedStateTypeId}
|
||||
{doorSensorThingClassId, doorSensorConnectedStateTypeId},
|
||||
{motionSensorThingClassId, motionSensorConnectedStateTypeId}
|
||||
};
|
||||
|
||||
static QHash<ThingClassId, StateTypeId> signalStrengthStateTypeIds = {
|
||||
{thermostatThingClassId, thermostatSignalStrengthStateTypeId},
|
||||
{powerSocketThingClassId, powerSocketSignalStrengthStateTypeId},
|
||||
{doorLockThingClassId, doorLockSignalStrengthStateTypeId},
|
||||
{doorSensorThingClassId, doorSensorSignalStrengthStateTypeId}
|
||||
{doorSensorThingClassId, doorSensorSignalStrengthStateTypeId},
|
||||
{motionSensorThingClassId, motionSensorSignalStrengthStateTypeId}
|
||||
};
|
||||
|
||||
static QHash<ThingClassId, StateTypeId> 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<ZigbeeClusterIasZone>(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<ZigbeeClusterIasZone>(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<quint8>(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<ZigbeeClusterIasZone*>(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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -37,6 +37,8 @@
|
||||
|
||||
#include <QTimer>
|
||||
|
||||
#include "extern-plugininfo.h"
|
||||
|
||||
class IntegrationPluginZigbeeGeneric: public IntegrationPlugin, public ZigbeeHandler
|
||||
{
|
||||
Q_OBJECT
|
||||
@ -66,7 +68,7 @@ private:
|
||||
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);
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user