src.nth.io/

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-06-21 13:17:31 -0500
committerLuke Hoersten <[email protected]>2026-06-21 13:17:31 -0500
commit3a7ddbdf418af1b83369e93b814cff6d91db6412 (patch)
treedd858c10fef82fd96ebff2505bc2b860bbb0d510
parent7d2b647e9204993256e1c345674b4b8c0b524a4a (diff)
scrypted: fix listen() — one call per interface (was passing array, filter broken)
ScryptedDevice.listen() takes a single ScryptedInterface | string | EventListenerOptions, not an array. We were passing [BinarySensor, MotionSensor, ObjectDetector] which stringifies to a comma-joined garbage filter, leaking unrelated events (Settings, ...) through and apparently dropping BinarySensor entirely — bell rings never surfaced. Also drops the child-device traversal: confirmed via unifi-protect/src/main.ts that the bell BinarySensor lives on the camera device itself for doorbells.
-rw-r--r--scrypted/scrypted-viewport.ts68
1 files changed, 17 insertions, 51 deletions
diff --git a/scrypted/scrypted-viewport.ts b/scrypted/scrypted-viewport.ts
index a5126de..47ad076 100644
--- a/scrypted/scrypted-viewport.ts
+++ b/scrypted/scrypted-viewport.ts
@@ -699,69 +699,35 @@ class ScryptedViewportProvider extends ScryptedDeviceBase
this.console.warn(`viewport "${tag}": camera ${v.cameraId} not found`);
return;
}
+ // Scrypted's ScryptedDevice.listen(event, cb) takes a SINGLE
+ // interface (or EventListenerOptions {event}), never an array.
+ // Confirmed in sdk/types/src/types.input.ts:21. Passing an array
+ // stringifies to "BinarySensor,MotionSensor,..." which matches
+ // nothing — events leak through with broken filtering. One
+ // listen() per interface is the right shape.
+ //
+ // For Unifi doorbells the bell-press lives on the camera device
+ // itself (unifi-protect/src/main.ts pushes BinarySensor onto the
+ // camera's interfaces when isDoorbell). So listening on `cam`
+ // for BinarySensor is all that's needed — no child traversal.
const ifaces = [
- ScryptedInterface.BinarySensor, // doorbell
+ ScryptedInterface.BinarySensor, // doorbell ring
ScryptedInterface.MotionSensor, // motion
ScryptedInterface.ObjectDetector, // person / etc
];
- // Unifi doorbell cameras expose the bell-press as a *child
- // device* of the camera (the bell button has its own nativeId
- // and BinarySensor interface), not as a property of the camera
- // itself. cam.listen() on the parent only sees motion + object
- // events; without subscribing on the children we miss the bell
- // press entirely. Symptom: HomeKit gets the doorbell event
- // (Scrypted auto-syncs child devices to the bridge) but our
- // handleCameraEvent never fires. Walk providerId to find
- // children of this camera and listen on each too. Safe to send
- // the same iface list to every target — listen() no-ops on
- // ifaces the device doesn't expose.
- const targets: any[] = [cam];
- const ids = (systemManager as any).getDeviceIds?.() ?? [];
- const camAny = cam as any;
- for (const id of ids) {
- if (id === cam.id) continue;
- const d: any = systemManager.getDeviceById(id);
- if (!d) continue;
- // True child of the camera.
- const isChild = d.providerId === cam.id;
- // Sibling under the same provider that exposes BinarySensor
- // (Unifi exposes the doorbell button this way on some models).
- const isSiblingBell =
- camAny.providerId &&
- d.providerId === camAny.providerId &&
- Array.isArray(d.interfaces) &&
- d.interfaces.includes(ScryptedInterface.BinarySensor);
- if (isChild || isSiblingBell) targets.push(d);
- }
- // Diagnostic: list every BinarySensor device in the system so we
- // can see which one is the doorbell button and what its
- // provider/relationship to the camera looks like.
- const camAny2 = cam as any;
- const bells: string[] = [];
- for (const id of ids) {
- const d: any = systemManager.getDeviceById(id);
- if (!d) continue;
- if (Array.isArray(d.interfaces) && d.interfaces.includes(ScryptedInterface.BinarySensor)) {
- bells.push(`${d.name} [id=${d.id} provider=${d.providerId} type=${d.type}]`);
- }
- }
- this.console.log(`viewport "${tag}": camera id=${cam.id} provider=${camAny2.providerId} type=${camAny2.type}`);
- this.console.log(`viewport "${tag}": BinarySensor devices in system: ${bells.length ? bells.join(" | ") : "(none)"}`);
const regs: EventListenerRegister[] = [];
- const targetNames: string[] = [];
- for (const t of targets) {
- const reg = t.listen(ifaces, (source: any, details: any, data: any) => {
+ for (const iface of ifaces) {
+ const reg = (cam as any).listen(iface, (source: any, details: any, data: any) => {
this.handleCameraEvent(v, details, data);
});
regs.push(reg);
- targetNames.push(t.name || t.id);
// Register for cross-reload cleanup so a re-paste removes
- // every camera/child listener. Without this each re-paste
- // stacks an extra callback per event source and each event
- // triggers handleCameraEvent N times for N reloads.
+ // every listener. Without this each re-paste stacks an extra
+ // callback per event source.
pushShutdownCleaner(() => { try { reg.removeListener(); } catch {} });
}
+ const targetNames = [cam.name || cam.id];
this.listeners.set(v.nativeId!, regs);
this.console.log(`viewport "${tag}": subscribed to [${targetNames.join(", ")}]`);
}