src.nth.io/

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLuke Hoersten <[email protected]>2026-06-21 10:55:42 -0500
committerLuke Hoersten <[email protected]>2026-06-21 10:55:42 -0500
commit2ca1307b34b82a4b5e3e83b50c02e3a93b2c7227 (patch)
treee6e3d9cbad5f98801387a4dd68cab9846f502f21
parent0bf731cfc966a7cfad0763f22f6196ebac3d16a0 (diff)
scrypted: subscribe to camera child devices (fix Unifi doorbell wake)
Unifi doorbell cameras expose the bell button as a child device of the camera (separate nativeId, its own BinarySensor interface), not as a property of the camera itself. The previous code did cam.listen() on the parent camera only, so motion + person events arrived (those fire on the camera itself) but bell-press events never reached handleCameraEvent. HomeKit kept working because Scrypted's HomeKit bridge auto-syncs all child devices; our plugin was silently the only consumer missing the event. Confirmed by the trace log in 3b0ab73 staying silent across a bell press while motion events still printed. Fix: in attachListener, walk systemManager.getDeviceIds() and pick any device with providerId === cam.id as an additional listen target. Subscribe on all of them with the same iface list — listen() no-ops on unsupported ifaces, so we don't have to introspect each child. Tracking changed from Map<nativeId, EventListenerRegister> to Map<nativeId, EventListenerRegister[]> so detach/script-reload cleanup removes every listener, not just the camera's.
-rw-r--r--scrypted/scrypted-viewport.ts56
1 files changed, 39 insertions, 17 deletions
diff --git a/scrypted/scrypted-viewport.ts b/scrypted/scrypted-viewport.ts
index 25f784f..250cf9c 100644
--- a/scrypted/scrypted-viewport.ts
+++ b/scrypted/scrypted-viewport.ts
@@ -313,7 +313,7 @@ class ScryptedViewportProvider extends ScryptedDeviceBase
implements DeviceProvider, DeviceCreator, HttpRequestHandler, Settings {
private viewports = new Map<string, Viewport>(); // nativeId -> child instance
- private listeners = new Map<string, EventListenerRegister>(); // nativeId -> camera event listener
+ private listeners = new Map<string, EventListenerRegister[]>(); // nativeId -> all event listeners for this viewport (camera + child devices)
streams = new Map<string, { // viewport name -> stream control (accessed by Viewport.putSetting for manual wake/sleep)
timeout: NodeJS.Timeout;
abort: AbortController; // also tears down the ffmpeg child via its listener
@@ -567,27 +567,49 @@ class ScryptedViewportProvider extends ScryptedDeviceBase
ScryptedInterface.MotionSensor, // motion
ScryptedInterface.ObjectDetector, // person / etc
];
- const reg = cam.listen(ifaces, (source, details, data) => {
- this.handleCameraEvent(v, details, data);
- });
- this.listeners.set(v.nativeId!, reg);
- // Track on globalThis so script reload can remove this
- // listener from the camera plugin. Without that, every
- // re-paste leaves a dead callback subscribed to the camera
- // and every motion/doorbell event triggers handleCameraEvent
- // N times for N stacked reloads — observable as duplicate
- // "stream start" log lines + two simultaneous pushSnapshots
- // racing for the firmware decoder lock.
+ // 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?.() ?? [];
+ for (const id of ids) {
+ if (id === cam.id) continue;
+ const d: any = systemManager.getDeviceById(id);
+ if (d?.providerId === cam.id) targets.push(d);
+ }
+
const G = globalThis as any;
if (!G.__viewportListenerCleaners) G.__viewportListenerCleaners = [];
- G.__viewportListenerCleaners.push(() => { try { reg.removeListener(); } catch {} });
- this.console.log(`viewport "${tag}": subscribed to "${cam.name}"`);
+ const regs: EventListenerRegister[] = [];
+ const targetNames: string[] = [];
+ for (const t of targets) {
+ const reg = t.listen(ifaces, (source: any, details: any, data: any) => {
+ this.handleCameraEvent(v, details, data);
+ });
+ regs.push(reg);
+ targetNames.push(t.name || t.id);
+ // Track on globalThis so script reload can remove every
+ // listener from the camera/child plugins. Without that,
+ // every re-paste leaves dead callbacks subscribed and each
+ // event triggers handleCameraEvent N times for N reloads.
+ G.__viewportListenerCleaners.push(() => { try { reg.removeListener(); } catch {} });
+ }
+ this.listeners.set(v.nativeId!, regs);
+ this.console.log(`viewport "${tag}": subscribed to [${targetNames.join(", ")}]`);
}
private detachListener(nativeId: string) {
- const reg = this.listeners.get(nativeId);
- if (reg) {
- try { reg.removeListener(); } catch {}
+ const regs = this.listeners.get(nativeId);
+ if (regs) {
+ for (const r of regs) { try { r.removeListener(); } catch {} }
this.listeners.delete(nativeId);
}
}