diff --git a/src/http/device.ts b/src/http/device.ts index 9d66929c..f5efd405 100644 --- a/src/http/device.ts +++ b/src/http/device.ts @@ -2020,7 +2020,12 @@ export class Device extends TypedEmitter { } static isSensor(type: number): boolean { - if (type == DeviceType.SENSOR || type == DeviceType.MOTION_SENSOR || type == DeviceType.ENTRY_SENSOR_E20) + if ( + type == DeviceType.SENSOR || + type == DeviceType.MOTION_SENSOR || + type == DeviceType.ENTRY_SENSOR_E20 || + type == DeviceType.PIR_SENSOR_E20 + ) return true; return false; } @@ -2508,7 +2513,11 @@ export class Device extends TypedEmitter { } static isMotionSensor(type: number): boolean { - return DeviceType.MOTION_SENSOR == type; + return DeviceType.MOTION_SENSOR == type || this.isPirSensorE20(type); + } + + static isPirSensorE20(type: number): boolean { + return DeviceType.PIR_SENSOR_E20 == type; } static isSmartDrop(type: number): boolean { @@ -4609,6 +4618,10 @@ export class EntrySensor extends Sensor { export class MotionSensor extends Sensor { public static readonly MOTION_COOLDOWN_MS = 120000; + // PIR_SENSOR_E20 reports motion as a detection timestamp without an explicit + // "motion ended" signal. The official eufy app keeps motion active for 1 + // minute after the last detection, so mirror that timeout here. + public static readonly PIR_SENSOR_E20_MOTION_TIMEOUT_MS = 60000; //TODO: CMD_MOTION_SENSOR_ENABLE_LED = 1607 //TODO: CMD_MOTION_SENSOR_ENTER_USER_TEST_MODE = 1613 @@ -4664,6 +4677,40 @@ export class MotionSensor extends Sensor { super.handlePropertyChange(metadata, oldValue, newValue); if (metadata.name === PropertyName.DeviceMotionDetected) { this.emit("motion detected", this, newValue as boolean); + } else if ( + metadata.name === PropertyName.DeviceMotionSensorPIREvent && + metadata.key === CommandType.CMD_MOTION_SENSOR_PIR_EVT && + oldValue !== undefined && + newValue !== undefined && + Number(newValue) > Number(oldValue) && + Date.now() - Number(newValue) < MotionSensor.PIR_SENSOR_E20_MOTION_TIMEOUT_MS + ) { + // PIR_SENSOR_E20 (e.g. paired to a HomeBase 3) does not send a + // CusPushEvent.MOTION_SENSOR_PIR push. Instead it reports motion as a + // CMD_MOTION_SENSOR_PIR_EVT (param 1605) update carrying the Unix-ms + // timestamp of the last PIR detection. A timestamp newer than the + // previously known one (and recent enough) indicates a fresh live + // detection, so raise the DeviceMotionDetected event and auto-reset it + // after the same 1 minute timeout the official eufy app uses. + try { + this.updateProperty(PropertyName.DeviceMotionDetected, true); + this.clearEventTimeout(DeviceEvent.MotionDetected); + this.eventTimeouts.set( + DeviceEvent.MotionDetected, + setTimeout(async () => { + this.updateProperty(PropertyName.DeviceMotionDetected, false); + this.eventTimeouts.delete(DeviceEvent.MotionDetected); + }, MotionSensor.PIR_SENSOR_E20_MOTION_TIMEOUT_MS) + ); + } catch (err) { + const error = ensureError(err); + rootHTTPLogger.debug(`MotionSensor handle PIR event property change - Error`, { + error: getError(error), + deviceSN: this.getSerial(), + oldValue: oldValue, + newValue: newValue, + }); + } } } diff --git a/src/http/types.ts b/src/http/types.ts index cfb9a8d2..9dfd785f 100644 --- a/src/http/types.ts +++ b/src/http/types.ts @@ -8733,6 +8733,7 @@ export const DeviceProperties: Properties = { [DeviceType.PIR_SENSOR_E20]: { ...GenericDeviceProperties, [PropertyName.DeviceBattery]: DeviceBatteryProperty, + [PropertyName.DeviceMotionDetected]: DeviceMotionDetectedProperty, [PropertyName.DeviceMotionSensorPIREvent]: DeviceMotionSensorPIREventProperty, [PropertyName.DeviceWifiRSSI]: DeviceWifiRSSILockProperty, [PropertyName.DeviceBatteryLow]: DeviceBatteryLowMotionSensorProperty, diff --git a/src/p2p/session.ts b/src/p2p/session.ts index 564840f9..93c06d47 100644 --- a/src/p2p/session.ts +++ b/src/p2p/session.ts @@ -3644,16 +3644,43 @@ export class P2PClientProtocol extends TypedEmitter { } } } else if (json.cmd === CommandType.CMD_HUB_NOTIFY_UPDATE) { - rootP2PLogger.debug( - `Handle DATA ${P2PDataType[message.dataType]} - CMD_NOTIFY_PAYLOAD - Homebase notify update`, - { - stationSN: this.rawStation.station_sn, - commandIdName: CommandType[json.cmd], - commandId: json.cmd, - message: data.toString(), + if ( + json.payload !== undefined && + Array.isArray((json.payload as unknown as { params?: unknown }).params) + ) { + // HomeBase 3 delivers live property updates for Sub-1G sensors + // (e.g. PIR_SENSOR_E20 motion via param_type 1605) as a generic + // params batch: + // {"cmd":1829,"payload":{"params":[{"dev_type":17,"param_type":1605,"param_value":"..."}]}} + // Route each param through the normal "parameter" pipeline so the owning + // device receives a live raw-property update instead of the event being + // silently dropped as "Not implemented". + const params = (json.payload as unknown as { params: Array<{ param_type?: number; param_value?: string }> }).params; + rootP2PLogger.debug( + `Handle DATA ${P2PDataType[message.dataType]} - CMD_NOTIFY_PAYLOAD device params update`, + { + stationSN: this.rawStation.station_sn, + channel: message.channel, + params: params, + } + ); + for (const param of params) { + if (param !== undefined && param.param_type !== undefined && param.param_value !== undefined) { + this.emit("parameter", message.channel, param.param_type, param.param_value); + } } - ); - this.emit("hub notify update"); + } else { + rootP2PLogger.debug( + `Handle DATA ${P2PDataType[message.dataType]} - CMD_NOTIFY_PAYLOAD - Homebase notify update`, + { + stationSN: this.rawStation.station_sn, + commandIdName: CommandType[json.cmd], + commandId: json.cmd, + message: data.toString(), + } + ); + this.emit("hub notify update"); + } } else { rootP2PLogger.debug( `Handle DATA ${P2PDataType[message.dataType]} - CMD_NOTIFY_PAYLOAD - Not implemented 2`,