From 7a14d5b044b5047ae50dcd1726a40d9241c086a5 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Sun, 15 Dec 2024 20:53:37 +0100 Subject: [PATCH] fix: Add occupancy timeout for Tuya IH012-RT01/ZMS-102 (#8333) --- src/devices/tuya.ts | 10 ++++++---- src/lib/modernExtend.ts | 29 +++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/devices/tuya.ts b/src/devices/tuya.ts index e8e92948991ff..0f9d29f07a3b2 100644 --- a/src/devices/tuya.ts +++ b/src/devices/tuya.ts @@ -1851,12 +1851,14 @@ const definitions: DefinitionWithExtend[] = [ model: 'IH012-RT01', vendor: 'Tuya', description: 'Motion sensor', - fromZigbee: [fz.ias_occupancy_alarm_1, fz.ignore_basic_report, fz.ZM35HQ_attr, fz.battery], + fromZigbee: [fz.ignore_basic_report, fz.ZM35HQ_attr, fz.battery], toZigbee: [tz.ZM35HQ_attr], - extend: [quirkCheckinInterval(15000)], + extend: [ + quirkCheckinInterval(15000), + // Occupancy reporting interval is 60s, so allow for one dropped update plus a small safety margin of 5s + iasZoneAlarm({zoneType: 'occupancy', zoneAttributes: ['alarm_1', 'battery_low'], keepAliveTimeout: 125}), + ], exposes: [ - e.occupancy(), - e.battery_low(), e.battery(), e.battery_voltage(), e.enum('sensitivity', ea.ALL, ['low', 'medium', 'high']).withDescription('PIR sensor sensitivity'), diff --git a/src/lib/modernExtend.ts b/src/lib/modernExtend.ts index 4629113c58d69..ac4e4046c0df3 100644 --- a/src/lib/modernExtend.ts +++ b/src/lib/modernExtend.ts @@ -1415,6 +1415,7 @@ export interface IasArgs { zoneType: iasZoneType; zoneAttributes: iasZoneAttribute[]; alarmTimeout?: boolean; + keepAliveTimeout?: number; zoneStatusReporting?: boolean; description?: string; manufacturerZoneAttributes?: manufacturerZoneAttribute[]; @@ -1489,7 +1490,8 @@ export function iasZoneAlarm(args: IasArgs): ModernExtend { globalStore.putValue(msg.endpoint, 'timer', timer); } } - const zoneStatus = msg.type === 'commandStatusChangeNotification' ? msg.data.zonestatus : msg.data.zoneStatus; + const isChange = msg.type === 'commandStatusChangeNotification'; + const zoneStatus = isChange ? msg.data.zonestatus : msg.data.zoneStatus; if (zoneStatus !== undefined) { let payload = {}; if (args.zoneAttributes.includes('tamper')) { @@ -1525,13 +1527,28 @@ export function iasZoneAlarm(args: IasArgs): ModernExtend { alarm2Payload = !alarm2Payload; } - if (bothAlarms) { + // Can't just alarm1Payload || alarm2Payload as an unused alarm's bit might be always 1 or random in the received data + let addTimeout = false; + if (args.zoneAttributes.includes('alarm_1')) { payload = {[alarm1Name]: alarm1Payload, ...payload}; + addTimeout ||= alarm1Payload; + } + if (args.zoneAttributes.includes('alarm_2')) { payload = {[alarm2Name]: alarm2Payload, ...payload}; - } else if (args.zoneAttributes.includes('alarm_1')) { - payload = {[alarm1Name]: alarm1Payload, ...payload}; - } else if (args.zoneAttributes.includes('alarm_2')) { - payload = {[alarm2Name]: alarm2Payload, ...payload}; + addTimeout ||= alarm2Payload; + } + if (isChange && args.keepAliveTimeout > 0) { + // This sensor continuously sends occupation updates as long as motion is detected; (re)start a timeout + // each time we receive one, in case the clearance message gets lost. Normally, these kinds of sensors + // send a clearance message, so this is an additional safety measure. + clearTimeout(globalStore.getValue(msg.endpoint, 'timeout')); + if (addTimeout) { + // At least one zone active + const timer = setTimeout(() => publish({[alarm1Name]: false, [alarm2Name]: false}), args.keepAliveTimeout * 1000); + globalStore.putValue(msg.endpoint, 'timeout', timer); + } else { + globalStore.clearValue(msg.endpoint, 'timeout'); + } } if (args.manufacturerZoneAttributes)