Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions src/http/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2020,7 +2020,12 @@ export class Device extends TypedEmitter<DeviceEvents> {
}

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;
}
Expand Down Expand Up @@ -2508,7 +2513,11 @@ export class Device extends TypedEmitter<DeviceEvents> {
}

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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
});
}
}
}

Expand Down
1 change: 1 addition & 0 deletions src/http/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
45 changes: 36 additions & 9 deletions src/p2p/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3644,16 +3644,43 @@ export class P2PClientProtocol extends TypedEmitter<P2PClientProtocolEvents> {
}
}
} 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`,
Expand Down