@camstack/types 1.0.5 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/addon.d.ts +34 -0
  2. package/dist/addon.js +22 -0
  3. package/dist/addon.mjs +3 -0
  4. package/dist/capabilities/addons.cap.d.ts +3 -3
  5. package/dist/capabilities/advanced-notifier.cap.d.ts +4 -4
  6. package/dist/capabilities/alerts.cap.d.ts +5 -5
  7. package/dist/capabilities/audio-codec.cap.d.ts +2 -2
  8. package/dist/capabilities/camera-streams.cap.d.ts +10 -10
  9. package/dist/capabilities/consumables.cap.d.ts +4 -4
  10. package/dist/capabilities/cover.cap.d.ts +4 -4
  11. package/dist/capabilities/decoder.cap.d.ts +1 -1
  12. package/dist/capabilities/local-network.cap.d.ts +6 -6
  13. package/dist/capabilities/log-destination.cap.d.ts +5 -5
  14. package/dist/capabilities/media-player.cap.d.ts +4 -4
  15. package/dist/capabilities/mesh-network.cap.d.ts +3 -3
  16. package/dist/capabilities/metrics-provider.cap.d.ts +33 -3
  17. package/dist/capabilities/network-access.cap.d.ts +7 -7
  18. package/dist/capabilities/oauth-integration.cap.d.ts +2 -2
  19. package/dist/capabilities/pipeline-orchestrator.cap.d.ts +3 -3
  20. package/dist/capabilities/platform-probe.cap.d.ts +1 -1
  21. package/dist/capabilities/restreamer.cap.d.ts +2 -2
  22. package/dist/capabilities/schemas/streaming-shared.d.ts +7 -7
  23. package/dist/capabilities/sso-bridge.cap.d.ts +3 -3
  24. package/dist/capabilities/storage.cap.d.ts +1 -1
  25. package/dist/capabilities/stream-broker.cap.d.ts +27 -27
  26. package/dist/capabilities/stream-params.cap.d.ts +14 -14
  27. package/dist/capabilities/user-management.cap.d.ts +20 -20
  28. package/dist/capabilities/vacuum-control.cap.d.ts +13 -13
  29. package/dist/capabilities/valve.cap.d.ts +4 -4
  30. package/dist/capabilities/webrtc-session.cap.d.ts +12 -12
  31. package/dist/deps/binary-downloader.d.ts +1 -1
  32. package/dist/deps/ffmpeg-downloader.d.ts +1 -1
  33. package/dist/deps/python-downloader.d.ts +1 -1
  34. package/dist/device/base-device-provider.d.ts +4 -1
  35. package/dist/encode-profile.d.ts +2 -2
  36. package/dist/err-msg-COpsHMw2.js +18 -0
  37. package/dist/err-msg-IQTHeDzc.mjs +13 -0
  38. package/dist/generated/addon-api.d.ts +22 -12
  39. package/dist/generated/method-access-map.d.ts +1 -1
  40. package/dist/generated/system-proxy.d.ts +1 -1
  41. package/dist/health/wiring-health.d.ts +16 -16
  42. package/dist/index.js +1098 -4572
  43. package/dist/index.mjs +156 -3629
  44. package/dist/interfaces/metrics-provider.d.ts +3 -1
  45. package/dist/node.js +3 -3
  46. package/dist/node.mjs +1 -1
  47. package/dist/schemas/auth-records.d.ts +4 -4
  48. package/dist/sleep-D7JeS58T.mjs +3507 -0
  49. package/dist/sleep-DnS0eJh_.js +3920 -0
  50. package/dist/storage/filesystem-storage-provider.d.ts +2 -1
  51. package/package.json +6 -1
@@ -0,0 +1,3507 @@
1
+ import { z } from "zod";
2
+ //#region src/disposer-chain.ts
3
+ var DisposerChain = class {
4
+ disposers = [];
5
+ disposed = false;
6
+ onError;
7
+ constructor(opts = {}) {
8
+ this.onError = opts.onError ?? ((err, index) => {
9
+ console.error(`[DisposerChain] disposer #${index} threw`, err);
10
+ });
11
+ }
12
+ /**
13
+ * Register a teardown callback. Returns an unregister function so
14
+ * callers can drop a single disposer without disposing the whole
15
+ * chain.
16
+ *
17
+ * If the chain has already been disposed, the callback runs immediately
18
+ * (sync) — this matches the “register-after-shutdown” edge case where
19
+ * an addon's late initialization races with kernel restart.
20
+ */
21
+ add(fn) {
22
+ if (this.disposed) {
23
+ try {
24
+ const result = fn();
25
+ if (result && typeof result.then === "function") result.catch((err) => this.onError(err, -1));
26
+ } catch (err) {
27
+ this.onError(err, -1);
28
+ }
29
+ return () => void 0;
30
+ }
31
+ this.disposers.push(fn);
32
+ return () => {
33
+ const idx = this.disposers.indexOf(fn);
34
+ if (idx >= 0) this.disposers.splice(idx, 1);
35
+ };
36
+ }
37
+ /** True after `dispose()` has been called at least once. */
38
+ get isDisposed() {
39
+ return this.disposed;
40
+ }
41
+ /** Number of disposers currently registered. */
42
+ get size() {
43
+ return this.disposers.length;
44
+ }
45
+ /**
46
+ * Run every registered disposer in LIFO order. Idempotent: subsequent
47
+ * calls do nothing. Awaits async disposers so callers can sequence
48
+ * shutdown → restart correctly.
49
+ */
50
+ async dispose() {
51
+ if (this.disposed) return;
52
+ this.disposed = true;
53
+ const drain = this.disposers.slice().toReversed();
54
+ this.disposers = [];
55
+ for (let i = 0; i < drain.length; i++) {
56
+ const fn = drain[i];
57
+ try {
58
+ const result = fn();
59
+ if (result && typeof result.then === "function") await result;
60
+ } catch (err) {
61
+ this.onError(err, drain.length - 1 - i);
62
+ }
63
+ }
64
+ }
65
+ };
66
+ //#endregion
67
+ //#region src/enums/event-category.ts
68
+ var EventCategory = /* @__PURE__ */ function(EventCategory) {
69
+ EventCategory["SystemBoot"] = "system.boot";
70
+ EventCategory["SystemAddonsReady"] = "system.addons-ready";
71
+ EventCategory["SystemRestarting"] = "system.restarting";
72
+ /**
73
+ * Fired exactly once after the hub finishes booting following a
74
+ * restart that was triggered by `RestartCoordinator` (today: framework
75
+ * live-update). Payload is the marker that was on disk at boot
76
+ * (`PendingRestartMarkerPayload`); admin UI listens for it to display a
77
+ * success toast describing what changed.
78
+ *
79
+ * Spec: docs/superpowers/specs/2026-05-14-framework-live-update-design.md
80
+ */
81
+ EventCategory["SystemRestartCompleted"] = "system.restart-completed";
82
+ /**
83
+ * Readiness transition for a capability provider. Every producer emits
84
+ * this event on `onInitialize` completion, `onDestroy`, and
85
+ * `$node.reconnect`; every consumer that needs to gate on a cross-process
86
+ * cap subscribes via the kernel's readiness module (`awaitReady` /
87
+ * `onReadyState`) instead of polling. Payload is
88
+ * `SystemReadyStatePayload` — see event-bus.ts.
89
+ */
90
+ EventCategory["SystemReadyState"] = "system.ready-state";
91
+ EventCategory["AddonStarted"] = "addon.started";
92
+ EventCategory["AddonStopped"] = "addon.stopped";
93
+ EventCategory["AddonRestarted"] = "addon.restarted";
94
+ EventCategory["AddonUpdated"] = "addon.updated";
95
+ EventCategory["AddonInstalled"] = "addon.installed";
96
+ EventCategory["AddonUninstalled"] = "addon.uninstalled";
97
+ EventCategory["AddonCrashed"] = "addon.crashed";
98
+ EventCategory["AddonError"] = "addon.error";
99
+ EventCategory["AddonPageReady"] = "addon.page-ready";
100
+ EventCategory["AddonWidgetReady"] = "addon.widget-ready";
101
+ /**
102
+ * Addon failed to load (import or initialize). Emitted by the kernel's
103
+ * AddonHealthMonitor only AFTER the boot grace period ends — failures
104
+ * during the first 5 minutes are silently retried without alerting
105
+ * (slow-starting addons must have time to come up). Post-grace, this
106
+ * event is emitted exactly once per failure-streak; AlertCenter
107
+ * consumes it to create a persistent operator-visible alert.
108
+ *
109
+ * Payload: `{ packageName, addonId?, error: { message, stack }, retryCount, nextRetryAt }`.
110
+ */
111
+ EventCategory["AddonLoadFailed"] = "addon.load-failed";
112
+ /**
113
+ * Addon recovered from a previous failure. Emitted when an addon
114
+ * transitions from `failed` back to `healthy` (typically via the
115
+ * monitor's auto-retry loop, or after manual `addons.retryLoad`).
116
+ * AlertCenter dismisses the corresponding `AddonLoadFailed` alert
117
+ * on this event.
118
+ */
119
+ EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
120
+ /**
121
+ * Monitor scheduled the next retry for a failed addon. Transient —
122
+ * surfaced to the UI for live-updating the "next retry in Ns"
123
+ * countdown on the Addons page row, NOT persisted as an alert.
124
+ */
125
+ EventCategory["AddonRetryScheduled"] = "addon.retry-scheduled";
126
+ /**
127
+ * Monitor is attempting to reload a failed addon NOW. UI uses this
128
+ * to show a spinner during the retry attempt. Same transient nature
129
+ * as AddonRetryScheduled.
130
+ */
131
+ EventCategory["AddonRetryAttempting"] = "addon.retry-attempting";
132
+ EventCategory["DeviceRegistered"] = "device.registered";
133
+ EventCategory["DeviceUnregistered"] = "device.unregistered";
134
+ EventCategory["DeviceEnabled"] = "device.enabled";
135
+ EventCategory["DeviceDisabled"] = "device.disabled";
136
+ EventCategory["DeviceSettingsUpdated"] = "device.settings-updated";
137
+ /**
138
+ * Emitted when the set of native capability providers bound to a device
139
+ * changes — e.g. an addon registers a new native cap via
140
+ * `DeviceContext.registerNativeCap`, or all native bindings for a device
141
+ * are cleared on removal. Hub consumers re-resolve device-proxy routes
142
+ * when this fires.
143
+ */
144
+ EventCategory["DeviceBindingsChanged"] = "device.bindings-changed";
145
+ /**
146
+ * Emitted when the operator-organisational meta surface changes
147
+ * (`name` / `location` / `disabled`). Payload: `{deviceId, field,
148
+ * value}`. Live consumers (UI device list, alert center) react
149
+ * without polling. Distinct from `DeviceSettingsUpdated` which
150
+ * fires on hardware-config changes (host/port/credentials/etc).
151
+ */
152
+ EventCategory["DeviceMetaChanged"] = "device.meta-changed";
153
+ /**
154
+ * Emitted by `BaseDevice.updateSourceInfo()` after a successful patch
155
+ * to the device's upstream-system identity / rendering envelope. The
156
+ * full new SourceInfo travels in the payload so cross-process listeners
157
+ * (UI, export adapters) don't need to re-read the meta blob.
158
+ *
159
+ * Payload: `{deviceId, sourceInfo}`.
160
+ *
161
+ * Distinct from `DeviceMetaChanged` which covers the operator-edited
162
+ * surface (`name` / `location` / `disabled`). The two share the
163
+ * persistence layer (both ride on `device-manager.setMetadata` for
164
+ * SourceInfo, or `setName`/`setLocation`/`setDisabled` for the meta
165
+ * fields) but consumers care about different subsets.
166
+ */
167
+ EventCategory["DeviceSourceInfoChanged"] = "device.source-info-changed";
168
+ /**
169
+ * Emitted by DeviceStreamWiringService after a successful
170
+ * `stream-broker.registerDeviceStreams` call. Payload includes
171
+ * deviceId — consumers look up the full registered device info via
172
+ * `brokerManager.getRegisteredDevice(deviceId)`.
173
+ *
174
+ * Replaces the legacy `StreamRouterService.onDeviceRegistered` callback.
175
+ */
176
+ EventCategory["DeviceStreamsRegistered"] = "device.streams-registered";
177
+ /**
178
+ * Device-level "fully provisioned" signal — the EXPORT trigger. Emitted once
179
+ * a device's persisted export-relevant shape (its `DeviceFeature` set) is
180
+ * established or changes, carrying `{deviceId, fingerprint, generation}`.
181
+ * Export adapters (Alexa / HAP) react to THIS instead of the chatty per-cap
182
+ * `DeviceBindingsChanged`, turning a boot's incomplete→complete trickle into
183
+ * one clean delta. `fingerprint` is `canonicalDeviceFingerprint(shape)`.
184
+ *
185
+ * Spec: docs/superpowers/specs/2026-06-01-alexa-hap-export-reconciler-design.md
186
+ */
187
+ EventCategory["DeviceProvisioned"] = "device.provisioned";
188
+ /**
189
+ * Fires once when a device's initial feature-probe completes successfully
190
+ * (lastProbedAt 0→>0); exported shape is now stable. Telemetry (may be
191
+ * dropped) — consumers MUST also gate on the device record's `probed` flag.
192
+ */
193
+ EventCategory["DeviceReady"] = "device.ready";
194
+ EventCategory["IntegrationEnabled"] = "integration.enabled";
195
+ EventCategory["IntegrationDisabled"] = "integration.disabled";
196
+ EventCategory["IntegrationDeleted"] = "integration.deleted";
197
+ /** Emitted when a broker connection's status flips (connected /
198
+ * disconnected / error). Carries `{ brokerId, status, error? }`. */
199
+ EventCategory["BrokerStatusChanged"] = "broker.status-changed";
200
+ /** Emitted for every subscription-matched message routed by a
201
+ * broker provider. Carries `{ brokerId, subscriptionId, key, payload }`.
202
+ * Consumers filter by `brokerId` + `subscriptionId` in the handler. */
203
+ EventCategory["BrokerMessage"] = "broker.message";
204
+ EventCategory["ProviderStarted"] = "provider.started";
205
+ EventCategory["ProviderStopped"] = "provider.stopped";
206
+ EventCategory["ProcessCrashed"] = "process.crashed";
207
+ EventCategory["ProcessRestartScheduled"] = "process.restart_scheduled";
208
+ EventCategory["ProcessRestarted"] = "process.restarted";
209
+ EventCategory["RecordingStarted"] = "recording.started";
210
+ EventCategory["RecordingStopped"] = "recording.stopped";
211
+ EventCategory["RecordingError"] = "recording.error";
212
+ EventCategory["RecordingHealthDegraded"] = "recording.health.degraded";
213
+ EventCategory["RecordingStorageCritical"] = "recording.storage.critical";
214
+ EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
215
+ EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
216
+ EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
217
+ EventCategory["DetectionEvent"] = "detection.event";
218
+ EventCategory["SessionTrackNew"] = "session.track.new";
219
+ EventCategory["SessionTrackExpired"] = "session.track.expired";
220
+ EventCategory["BenchmarkProgress"] = "benchmark.progress";
221
+ EventCategory["PlatformProbePhase"] = "platform-probe.phase";
222
+ EventCategory["PipelineProgress"] = "pipeline.progress";
223
+ /** Per-frame execution trace emitted by the pipeline executor for live observability. */
224
+ EventCategory["PipelineTrace"] = "pipeline.trace";
225
+ /**
226
+ * Raw inference output emitted by `addon-pipeline-runner` after running the
227
+ * detection pipeline on a frame. Carries the `FrameResult` (with
228
+ * `detections[]`, `width`/`height`, timing debug) — never the frame
229
+ * buffer itself. Hub-side consumers (analysis pipeline, class filters,
230
+ * notifications) subscribe to this and re-emit `detection.result` after
231
+ * post-processing.
232
+ */
233
+ EventCategory["PipelineInferenceResult"] = "pipeline.inference-result";
234
+ /**
235
+ * Camera lifecycle event emitted by `addon-pipeline-orchestrator` when it
236
+ * assigns or unassigns a camera to/from an agent. Carries no frame data;
237
+ * pure observability for UI dashboards and metrics consumers.
238
+ */
239
+ EventCategory["PipelineCameraAssigned"] = "pipeline.camera-assigned";
240
+ EventCategory["PipelineCameraUnassigned"] = "pipeline.camera-unassigned";
241
+ /**
242
+ * Per-camera pipeline config was mutated by the orchestrator
243
+ * (3-level settings change via `setAgentAddonDefaults` /
244
+ * `setCameraStepToggle` / `setCameraPipelineForAgent` or a
245
+ * pipeline-scoped `applyDeviceSettingsPatch`). Orchestrator
246
+ * subscribes to its own emission to hot-reload the assigned runner
247
+ * via `attachCamera` so the next frame executes against the new
248
+ * engine/steps without waiting for a rebalance or the next
249
+ * `DeviceStreamsRegistered` cycle.
250
+ */
251
+ EventCategory["PipelineCameraUpdated"] = "pipeline.camera-updated";
252
+ /**
253
+ * Periodic snapshot of per-node pipeline-runner load
254
+ * (`RunnerLocalLoad`). Emitted ~1Hz by every runner so UI dashboards
255
+ * subscribe instead of polling `pipelineRunner.getLocalLoad`.
256
+ * `nodeId` carried in the payload + on `event.source.nodeId`.
257
+ */
258
+ EventCategory["PipelineRunnerLoadSnapshot"] = "pipeline.runner-load-snapshot";
259
+ /**
260
+ * Periodic snapshot of per-camera pipeline metrics (`CameraMetrics`
261
+ * + `deviceId` + `nodeId`). Emitted ~1Hz by every runner for each
262
+ * attached camera. UI subscribes to drive overlay phase / fps /
263
+ * inference time without polling `getCameraMetrics`.
264
+ */
265
+ EventCategory["PipelineCameraMetricsSnapshot"] = "pipeline.camera-metrics-snapshot";
266
+ /**
267
+ * Periodic snapshot of stream-broker per-broker statistics (input
268
+ * fps, decoded fps, bitrate, codec). Emitted ~1Hz by every
269
+ * stream-broker process for each active broker so the UI can drive
270
+ * the Stream / Cluster dashboards without polling
271
+ * `streamBroker.listAllProfileSlots` and friends.
272
+ */
273
+ EventCategory["StreamBrokerMetricsSnapshot"] = "stream-broker.metrics-snapshot";
274
+ /**
275
+ * Cap event fired by `stream-broker` when a profile slot enters
276
+ * "demanded" state — a cam stream has been assigned and at least one
277
+ * consumer (RTSP restream, decoded subscriber, WebRTC session, …) is
278
+ * present. Camera-provider addons (Reolink Baichuan push, …)
279
+ * subscribe to this category to start their underlying transport
280
+ * lazily. Payload: `{ deviceId, camStreamId, profile }`.
281
+ */
282
+ EventCategory["StreamBrokerOnCamStreamDemand"] = "stream-broker.onCamStreamDemand";
283
+ /**
284
+ * Cap event fired by `stream-broker` when the last consumer leaves a
285
+ * previously-demanded cam stream. Providers tear down their
286
+ * underlying transport on receipt. Payload: `{ deviceId, camStreamId }`.
287
+ */
288
+ EventCategory["StreamBrokerOnCamStreamIdle"] = "stream-broker.onCamStreamIdle";
289
+ /**
290
+ * Cap event fired by `stream-broker` when a broker fails to dial a
291
+ * managed-loopback source (today: `pull-rfc4571`) and the publisher
292
+ * needs to refresh the cached URL. The lib's TCP server idle-tears-down
293
+ * on its own schedule, so a re-publish with a fresh `host:port` is the
294
+ * only way to keep the broker dialable. Camera providers (Reolink
295
+ * Baichuan native, …) subscribe and respond by re-running their publish
296
+ * pipeline.
297
+ * Payload: `{ deviceId, camStreamId, brokerId }`.
298
+ */
299
+ EventCategory["StreamBrokerOnRequestStreamSourceRefresh"] = "stream-broker.onRequestStreamSourceRefresh";
300
+ /**
301
+ * A camera provider changed a device's stream parameters (codec /
302
+ * resolution / bitrate / a stream added or removed). A LOW-LATENCY NUDGE
303
+ * for the stream-broker's catalog reconcile: it carries NO authoritative
304
+ * data — the broker re-PULLS that device's `stream-catalog` cap on receipt
305
+ * (the 30s reconcile poll is the backstop if this nudge is dropped). Emitted
306
+ * by providers from `stream-params.setProfile`. Payload: `{ deviceId }`.
307
+ */
308
+ EventCategory["StreamParamsChanged"] = "stream-params.changed";
309
+ /**
310
+ * Generic per-device runtime-state change. Fired by `device-manager`
311
+ * whenever a persisted slice in any cap's `runtimeState` shape
312
+ * mutates. Payload: `{deviceId, capName, slice}`. Subscribers are
313
+ * the `deviceState` cap router (cross-process listeners) and the
314
+ * deviceProxy reactive bindings (`device.state.<capName>.value`).
315
+ * Cap-specific events (`battery.onStatusChanged`, …) still fire
316
+ * — they're authoritative for callers that want a typed payload
317
+ * without filtering on `capName`.
318
+ */
319
+ EventCategory["DeviceStateChanged"] = "device.state-changed";
320
+ /**
321
+ * Cap event fired by every device that registers the `battery`
322
+ * capability. Mirrors the cap definition's `onStatusChanged`. Carries
323
+ * `{ deviceId, status: BatteryStatus }`. Subscribers (alert center,
324
+ * snapshot wrapper, UI) react to charge/sleep transitions without
325
+ * polling `batteryCapability.getStatus`.
326
+ */
327
+ EventCategory["BatteryOnStatusChanged"] = "battery.onStatusChanged";
328
+ /**
329
+ * Emitted by the battery cap provider WHEN `wakeForStream` enters the
330
+ * "wake in progress" window — between the Baichuan wake-up issue and
331
+ * the camera's first dialed-back RTP packet. The stream-broker
332
+ * manager subscribes to flip the per-broker placeholder reason to
333
+ * `'waking'` so viewers see a labelled "WAKING UP" tile instead of
334
+ * the generic `'reconnecting'` frame. Carries `{ deviceId }`. The
335
+ * complementary "wake complete" signal is the existing
336
+ * `BatteryOnStatusChanged { sleeping: false }` event.
337
+ */
338
+ EventCategory["BatteryOnWakeStarted"] = "battery.onWakeStarted";
339
+ /**
340
+ * Cap event fired by every device that registers the `doorbell`
341
+ * capability. Mirrors `doorbellCapability.events.onPressed`. Carries
342
+ * `{ deviceId, timestamp }`. Operators consuming the UI subscribe
343
+ * here to render transient ring toasts and a "Recent presses" row
344
+ * on the device detail page.
345
+ */
346
+ EventCategory["DoorbellOnPressed"] = "doorbell.onPressed";
347
+ /**
348
+ * Cap event fired by every device that registers the `event-emitter`
349
+ * capability. Mirrors `eventEmitterCapability.events.onEvent`. Carries
350
+ * `{ deviceId, eventType, data, timestamp, seq }` — the device's EXACT
351
+ * declared event verbatim (NO normalization). Subscribers (UI event
352
+ * stream, advanced-notifier rules) react to fired events without
353
+ * holding a cap reference.
354
+ */
355
+ EventCategory["EventEmitted"] = "event-emitter.event";
356
+ /**
357
+ * Periodic snapshot of the per-node detection-pipeline engine
358
+ * registry (loaded engines, models resident, in-use cameras, idle
359
+ * TTL). Emitted ~0.2Hz (every 5 s) by every detection-pipeline
360
+ * process. The Engines tab subscribes to drive its inventory view
361
+ * without polling `pipelineExecutor.listLoadedEngines`.
362
+ */
363
+ EventCategory["PipelineEngineMetricsSnapshot"] = "pipeline.engine-metrics-snapshot";
364
+ /**
365
+ * Per-node detection-engine runtime-provisioning transition. Emitted by
366
+ * the detection-pipeline provider on every state change of its lazy
367
+ * engine-provisioning machine (idle → installing → verifying → ready,
368
+ * or → failed with a `nextRetryAt`). Payload is the
369
+ * `EngineProvisioningState` snapshot; `event.source.nodeId` carries the
370
+ * node. The Pipeline page subscribes to drive a live "installing
371
+ * OpenVINO… / ready" indicator per node without polling
372
+ * `pipelineExecutor.getEngineProvisioning`. Telemetry-grade (D8): the UI
373
+ * also reads the cap snapshot on mount / reconnect. Phase 2.
374
+ */
375
+ EventCategory["PipelineEngineProvisioning"] = "pipeline.engine-provisioning";
376
+ /**
377
+ * Cluster topology snapshot. Carries the same payload returned by
378
+ * `nodes.topology` (every reachable node + addons + processes).
379
+ * Emitted by the hub on any agent / addon lifecycle change
380
+ * (debounced) plus a periodic safety net. Replaces UI polling on
381
+ * `nodes.topology` — admin-ui dashboards subscribe to drive the
382
+ * cluster view directly from the event payload.
383
+ */
384
+ EventCategory["ClusterTopologySnapshot"] = "cluster.topology-snapshot";
385
+ /**
386
+ * Periodic per-node system metrics snapshot (CPU / memory / GPU /
387
+ * disk / network). Emitted ~0.2 Hz by the metrics-provider addon
388
+ * for each node. Drives the dashboard SystemStatus / ProcessResources
389
+ * widgets without polling `metricsProvider.getCurrent`.
390
+ */
391
+ EventCategory["MetricsNodeResourcesSnapshot"] = "metrics.node-resources-snapshot";
392
+ /**
393
+ * Periodic per-node process-tree snapshot (camstack-related pids
394
+ * with ghost / managed / root classification). Emitted ~0.2 Hz by
395
+ * the metrics-provider addon. Drives the Cluster → Processes tab
396
+ * without polling `metricsProvider.listNodeProcesses`.
397
+ */
398
+ EventCategory["MetricsNodeProcessesSnapshot"] = "metrics.node-processes-snapshot";
399
+ /**
400
+ * Capability binding change event emitted by `addon-pipeline-orchestrator`
401
+ * when a user changes which addon implements a cap on a node. Subscribed
402
+ * by every kernel process to update its local `preferredProviderRegistry`
403
+ * so future capability lookups respect the new binding.
404
+ */
405
+ /**
406
+ * A capability binding was changed for a node — addon X now provides
407
+ * capability `cap` on node `nodeId`. Lives under the generic
408
+ * `capability.*` namespace because capability bindings are a kernel-
409
+ * level concept used by many addons, not strictly pipeline-scoped.
410
+ */
411
+ EventCategory["CapabilityBindingChanged"] = "capability.binding-changed";
412
+ EventCategory["ModelDownloadProgress"] = "model.download.progress";
413
+ EventCategory["AgentRegistered"] = "agent.registered";
414
+ EventCategory["AgentUnregistered"] = "agent.unregistered";
415
+ EventCategory["AgentOnline"] = "agent.online";
416
+ EventCategory["AgentOffline"] = "agent.offline";
417
+ /** Forked worker process (e.g. hub/pipeline) connected to the broker. */
418
+ EventCategory["WorkerOnline"] = "worker.online";
419
+ /** Forked worker process disconnected from the broker. */
420
+ EventCategory["WorkerOffline"] = "worker.offline";
421
+ EventCategory["AgentTaskDispatched"] = "agent.task.dispatched";
422
+ EventCategory["AgentTaskAssigned"] = "agent.task.assigned";
423
+ EventCategory["AgentTrpcConnected"] = "agent.trpc.connected";
424
+ EventCategory["AgentWsConnected"] = "agent.ws.connected";
425
+ EventCategory["AgentWsDisconnected"] = "agent.ws.disconnected";
426
+ EventCategory["AgentBackupActivated"] = "agent.backup.activated";
427
+ EventCategory["OrchestrationSettingsUpdated"] = "orchestration.settings-updated";
428
+ /**
429
+ * Per-agent hwaccel preference changed (user override set, cleared, or
430
+ * re-probed). Observability event — decoders pull the current pref at
431
+ * `createSession`, so running sessions keep their current backend until
432
+ * they rotate naturally (camera add/remove, stream restart). Future
433
+ * work can wire a listener in stream-broker that force-rotates live
434
+ * sessions; for now this event powers logs + admin-UI toast feedback.
435
+ */
436
+ EventCategory["PipelineAgentHwaccelChanged"] = "pipeline.agent-hwaccel-changed";
437
+ EventCategory["MotionAnalysis"] = "detection.motion-analysis";
438
+ /** All raw motion zones from CCL before minArea filter — for UI debug overlay. */
439
+ EventCategory["MotionZonesRaw"] = "detection.motion-zones-raw";
440
+ /**
441
+ * Per-camera motion phase transition (`watching ↔ active`) emitted
442
+ * by the runner. Mirrors the `motion.onMotionChanged` cap event
443
+ * surface — payload `MotionOnMotionChangedPayload` carries
444
+ * `{deviceId, detected, timestamp, source, regions?}`. Subscribers
445
+ * include addons that need to react to motion state without
446
+ * polling the runtime-state mirror.
447
+ */
448
+ EventCategory["MotionOnMotionChanged"] = "motion.on-motion-changed";
449
+ EventCategory["DetectionResult"] = "detection.result";
450
+ EventCategory["DetectionRaw"] = "detection.raw";
451
+ EventCategory["DetectionCameraNative"] = "detection.camera-native";
452
+ /**
453
+ * Canonical per-chunk live audio pipeline output. Payload is
454
+ * `PipelineAudioInferenceResultPayload` carrying a full `AudioResult`
455
+ * (level + detections + debug). Lives on the pipeline.* namespace
456
+ * alongside `pipeline.inference-result` (video) for symmetry.
457
+ */
458
+ EventCategory["PipelineAudioInferenceResult"] = "pipeline.audio-inference-result";
459
+ EventCategory["DetectionPhaseTransition"] = "detection.phase-transition";
460
+ EventCategory["ProviderMotion"] = "provider.motion";
461
+ EventCategory["ProviderDetection"] = "provider.detection";
462
+ EventCategory["EnrichmentEmbeddingStored"] = "enrichment.embedding.stored";
463
+ EventCategory["EnrichmentSceneStateChanged"] = "enrichment.scene.state-changed";
464
+ EventCategory["EnrichmentActivitySummary"] = "enrichment.activity.summary";
465
+ EventCategory["PipelineAnalyticsTrackStarted"] = "pipeline-analytics.track-started";
466
+ EventCategory["PipelineAnalyticsTrackEnded"] = "pipeline-analytics.track-ended";
467
+ EventCategory["PipelineAnalyticsDetectionEvent"] = "pipeline-analytics.detection-event";
468
+ EventCategory["PipelineAnalyticsFrameTracked"] = "pipeline-analytics.frame-tracked";
469
+ EventCategory["FrigateLiveEvent"] = "frigate.live-event";
470
+ EventCategory["CameraStreamsProfileSlotsChanged"] = "camera-streams.onProfileSlotsChanged";
471
+ /**
472
+ * Stream-broker health watchdog. Per-broker (deviceId/profile) emission.
473
+ * `stream.offline` fires after STREAM_STALE_TIMEOUT_MS without an encoded
474
+ * packet on an active broker. `stream.online` fires on first packet
475
+ * after a stale gap (or on initial first packet). Payload includes
476
+ * the assigned camStreamId as `profileKey`.
477
+ */
478
+ EventCategory["StreamOnline"] = "stream.online";
479
+ EventCategory["StreamOffline"] = "stream.offline";
480
+ EventCategory["NetworkTunnelStarted"] = "network.tunnel.started";
481
+ EventCategory["NetworkTunnelStopped"] = "network.tunnel.stopped";
482
+ /** Fired by the `local-network` cap when the host's interface set
483
+ * changes (new IP from DHCP, VPN connect, docker bridge added). */
484
+ EventCategory["LocalNetworkChanged"] = "network.local.changed";
485
+ /**
486
+ * Fired by a `mesh-network` provider (Tailscale, …) when its
487
+ * mesh-reachable host changes (join / leave / MagicDNS or 100.x IP
488
+ * change). Lets `local-network` fold the mesh endpoint into its
489
+ * connection-endpoint list without a cross-package import. Payload
490
+ * carries the preferred host (`host: ''` = no longer reachable),
491
+ * the hub port, and the scheme. Telemetry-grade (D8): consumers
492
+ * pull-reconcile from the provider's `getStatus` on reconnect. */
493
+ EventCategory["MeshNetworkChanged"] = "network.mesh.changed";
494
+ EventCategory["BackupCompleted"] = "backup.completed";
495
+ EventCategory["BackupRestored"] = "backup.restored";
496
+ EventCategory["NotificationDispatched"] = "notification.dispatched";
497
+ EventCategory["NotificationFailed"] = "notification.failed";
498
+ EventCategory["DeviceUpdated"] = "device.updated";
499
+ /**
500
+ * Transport-level connectivity. Emitted by the device driver when
501
+ * the underlying control socket actually connects / disconnects
502
+ * (Baichuan TCP, ONVIF probe response, RTSP DESCRIBE, …) — NOT for
503
+ * power-state transitions on a battery camera. For battery wake /
504
+ * doze cycles see `DeviceAwake` / `DeviceSleeping`.
505
+ */
506
+ EventCategory["DeviceOnline"] = "device.online";
507
+ /**
508
+ * Stream-broker watchdog — emitted when no encoded packet has been
509
+ * received for STREAM_STALE_TIMEOUT_MS on an active broker (rtsp or
510
+ * push). Paired with DeviceOnline which fires on first packet after
511
+ * a stale gap. Payload: DeviceStreamHealthPayload.
512
+ */
513
+ EventCategory["DeviceOffline"] = "device.offline";
514
+ /**
515
+ * Battery cam woke up — physical power-state transition reported
516
+ * by the device firmware. Distinct from `DeviceOnline` so a UI panel
517
+ * watching power state doesn't flap on every UDP socket reconnect.
518
+ */
519
+ EventCategory["DeviceAwake"] = "device.awake";
520
+ /**
521
+ * Battery cam went to sleep. See `DeviceAwake`.
522
+ */
523
+ EventCategory["DeviceSleeping"] = "device.sleeping";
524
+ EventCategory["RetentionCleanup"] = "retention.cleanup";
525
+ /**
526
+ * Legacy bulk-update progress snapshot (payload `BulkUpdateState`). No longer
527
+ * emitted — F3 removed the coordinator that produced it; "Update all" now runs
528
+ * as one lifecycle engine job (`AddonsJobProgress`/`AddonsJobLog`). Retained
529
+ * (with `BulkUpdateState`) only to avoid regenerating the event maps; removed
530
+ * in F4 once live bulk progress is re-implemented over the engine events.
531
+ */
532
+ EventCategory["AddonsBulkUpdateProgress"] = "addons.bulk-update-progress";
533
+ EventCategory["AddonsJobProgress"] = "addons.job-progress";
534
+ EventCategory["AddonsJobLog"] = "addons.job-log";
535
+ /**
536
+ * A container's child visibility toggled (hidden/shown). Emitted by the
537
+ * `accessories` cap when a child device is hidden or revealed.
538
+ * Payload: `{ deviceId, childDeviceId, hidden }`.
539
+ */
540
+ EventCategory["AccessoriesChildVisibilityChanged"] = "accessories.onChildVisibilityChanged";
541
+ /**
542
+ * A container's child set changed (children added/removed/reordered).
543
+ * Payload: `{ deviceId, childDeviceIds, hiddenChildIds }`.
544
+ */
545
+ EventCategory["AccessoriesChanged"] = "accessories.onAccessoriesChanged";
546
+ return EventCategory;
547
+ }({});
548
+ //#endregion
549
+ //#region src/interfaces/config-ui.ts
550
+ /** Predefined tabs with standard label, icon, and sort order.
551
+ *
552
+ * Pipeline (renamed Orchestrator in the UI) holds the four
553
+ * pipeline-orchestrator sections: General, Object Detection, Audio,
554
+ * and Cluster Assignment. Motion stays its own tab.
555
+ * `streaming` remains for older payloads that haven't been retagged. */
556
+ var WELL_KNOWN_TABS = [
557
+ {
558
+ id: "overview",
559
+ label: "Overview",
560
+ icon: "layout-dashboard",
561
+ order: -10
562
+ },
563
+ {
564
+ id: "general",
565
+ label: "General",
566
+ icon: "settings",
567
+ order: 0
568
+ },
569
+ {
570
+ id: "image",
571
+ label: "Image",
572
+ icon: "image",
573
+ order: 5
574
+ },
575
+ {
576
+ id: "light",
577
+ label: "Light",
578
+ icon: "lightbulb",
579
+ order: 7
580
+ },
581
+ {
582
+ id: "motion",
583
+ label: "Motion",
584
+ icon: "activity",
585
+ order: 10
586
+ },
587
+ {
588
+ id: "audio",
589
+ label: "Audio",
590
+ icon: "mic",
591
+ order: 12
592
+ },
593
+ {
594
+ id: "alarms",
595
+ label: "Alarms",
596
+ icon: "bell-ring",
597
+ order: 13
598
+ },
599
+ {
600
+ id: "snapshot",
601
+ label: "Snapshot",
602
+ icon: "camera",
603
+ order: 15
604
+ },
605
+ {
606
+ id: "osd",
607
+ label: "OSD",
608
+ icon: "type",
609
+ order: 18
610
+ },
611
+ {
612
+ id: "stream-broker",
613
+ label: "Stream Broker",
614
+ icon: "radio",
615
+ order: 20
616
+ },
617
+ {
618
+ id: "streaming",
619
+ label: "Streaming",
620
+ icon: "video",
621
+ order: 35
622
+ },
623
+ {
624
+ id: "ptz",
625
+ label: "PTZ",
626
+ icon: "move",
627
+ order: 40
628
+ },
629
+ {
630
+ id: "consumables",
631
+ label: "Consumables",
632
+ icon: "recycle",
633
+ order: 44
634
+ },
635
+ {
636
+ id: "pipeline",
637
+ label: "Detection Pipeline",
638
+ icon: "cpu",
639
+ order: 39
640
+ },
641
+ {
642
+ id: "detection-pipeline",
643
+ label: "Detection pipeline",
644
+ icon: "cpu",
645
+ order: 39
646
+ },
647
+ {
648
+ id: "zones",
649
+ label: "Detection",
650
+ icon: "shapes",
651
+ order: 38
652
+ },
653
+ {
654
+ id: "analytics",
655
+ label: "Analytics",
656
+ icon: "activity",
657
+ order: 37
658
+ },
659
+ {
660
+ id: "live-stats",
661
+ label: "Live Stats",
662
+ icon: "activity",
663
+ order: 39
664
+ },
665
+ {
666
+ id: "recording",
667
+ label: "Recording",
668
+ icon: "circle-dot",
669
+ order: 40
670
+ },
671
+ {
672
+ id: "engine",
673
+ label: "Inference Engine",
674
+ icon: "zap",
675
+ order: 41
676
+ },
677
+ {
678
+ id: "scheduler",
679
+ label: "Scheduler",
680
+ icon: "list-checks",
681
+ order: 42
682
+ },
683
+ {
684
+ id: "decoder",
685
+ label: "Decoder",
686
+ icon: "film",
687
+ order: 43
688
+ },
689
+ {
690
+ id: "notifications",
691
+ label: "Notifications",
692
+ icon: "bell",
693
+ order: 50
694
+ },
695
+ {
696
+ id: "network",
697
+ label: "Network",
698
+ icon: "globe",
699
+ order: 60
700
+ },
701
+ {
702
+ id: "storage",
703
+ label: "Storage",
704
+ icon: "hard-drive",
705
+ order: 70
706
+ },
707
+ {
708
+ id: "advanced",
709
+ label: "Advanced",
710
+ icon: "wrench",
711
+ order: 100
712
+ }
713
+ ];
714
+ /** Lookup map for well-known tabs by ID. */
715
+ var WELL_KNOWN_TAB_MAP = Object.fromEntries(WELL_KNOWN_TABS.map((t) => [t.id, t]));
716
+ /**
717
+ * Field types that never carry a value (separator, info, button). Used by
718
+ * `hydrateSchema` to skip the `value` injection for structural-only fields.
719
+ */
720
+ function isValuelessField(field) {
721
+ return field.type === "separator" || field.type === "info" || field.type === "qr-code" || field.type === "button" || field.type === "object-array" || field.type === "widget" || field.type === "addon-action-button" || field.type === "device-action-button";
722
+ }
723
+ /**
724
+ * Merge a `ConfigUISchema` with a raw `values` record into a
725
+ * `ConfigUISchemaWithValues`. Used by every `get*Settings` backend endpoint
726
+ * before returning to the admin UI. Groups are walked recursively.
727
+ *
728
+ * For each leaf field:
729
+ * - structural fields (`separator`, `info`, `button`) pass through unchanged
730
+ * - other fields get `value = values[key] ?? field.default ?? null`
731
+ *
732
+ * Unknown keys in `values` (keys not declared in the schema) are silently
733
+ * ignored — they don't contribute to the output. Missing keys fall back to
734
+ * the schema default or `null`. This mirrors the old backend behaviour where
735
+ * `FormBuilder` rendered `values[key] ?? field.default`.
736
+ */
737
+ function hydrateSchema(schema, values) {
738
+ return {
739
+ ...schema.tabs ? { tabs: [...schema.tabs] } : {},
740
+ sections: schema.sections.map((section) => ({
741
+ ...section,
742
+ fields: section.fields.map((field) => hydrateField(field, values))
743
+ }))
744
+ };
745
+ }
746
+ function hydrateField(field, values) {
747
+ if (isValuelessField(field)) return field;
748
+ if (field.type === "group") {
749
+ const hydratedChildren = field.fields.map((child) => hydrateField(child, values));
750
+ return {
751
+ ...field,
752
+ fields: hydratedChildren
753
+ };
754
+ }
755
+ if (field.type === "sub-tabs") {
756
+ const hydratedTabs = field.tabs.map((tab) => ({
757
+ ...tab,
758
+ fields: tab.fields.map((child) => hydrateField(child, values))
759
+ }));
760
+ return {
761
+ ...field,
762
+ tabs: hydratedTabs
763
+ };
764
+ }
765
+ const key = field.key;
766
+ const storedValue = Object.prototype.hasOwnProperty.call(values, key) ? values[key] : void 0;
767
+ const defaultValue = field.default;
768
+ if (field.multiple) {
769
+ const stored = Array.isArray(storedValue) ? storedValue : storedValue !== void 0 && storedValue !== null ? [storedValue] : [];
770
+ const itemFallback = field.multiple.itemDefault !== void 0 ? field.multiple.itemDefault : defaultValue !== void 0 ? defaultValue : typeOf(field) === "string" ? "" : null;
771
+ const minCount = Math.max(field.multiple.min, stored.length);
772
+ const items = [];
773
+ for (let i = 0; i < minCount; i++) items.push(i < stored.length ? stored[i] : itemFallback);
774
+ return {
775
+ ...field,
776
+ value: items
777
+ };
778
+ }
779
+ const rawValue = storedValue !== void 0 ? storedValue : defaultValue !== void 0 ? defaultValue : null;
780
+ if (field.type === "password") return {
781
+ ...field,
782
+ value: ""
783
+ };
784
+ const value = field.type === "textarea" && field.isJson && rawValue !== null && typeof rawValue === "object" ? JSON.stringify(rawValue, null, 2) : rawValue;
785
+ return {
786
+ ...field,
787
+ value
788
+ };
789
+ }
790
+ /**
791
+ * Rough "value family" classifier used by `hydrateField`'s multiple
792
+ * fallback to pick a sensible zero-value when no `itemDefault` / no
793
+ * field `default` / no stored value is available.
794
+ */
795
+ function typeOf(field) {
796
+ switch (field.type) {
797
+ case "text":
798
+ case "textarea":
799
+ case "password":
800
+ case "color":
801
+ case "probe":
802
+ case "timezone":
803
+ case "datetime": return "string";
804
+ case "number":
805
+ case "slider": return "number";
806
+ case "boolean": return "boolean";
807
+ default: return "other";
808
+ }
809
+ }
810
+ //#endregion
811
+ //#region src/interfaces/event-bus.ts
812
+ /**
813
+ * Narrow a SystemEvent to a typed event by checking its category.
814
+ * Returns `true` (and narrows the type) if the category matches.
815
+ *
816
+ * @example
817
+ * ```typescript
818
+ * eventBus.subscribe({ category: 'addon.started' }, (event) => {
819
+ * if (isEvent(event, 'addon.started')) {
820
+ * event.data.addonId // ✓ typed as string
821
+ * }
822
+ * })
823
+ * ```
824
+ */
825
+ function isEvent(event, category) {
826
+ return event.category === category;
827
+ }
828
+ /**
829
+ * Create a typed event with less boilerplate.
830
+ *
831
+ * @example
832
+ * ```typescript
833
+ * eventBus.emit(createEvent('addon.started', { type: 'addon', id: 'pipeline' }, {
834
+ * addonId: 'pipeline',
835
+ * packageVersion: '0.1.8',
836
+ * }))
837
+ * ```
838
+ */
839
+ function createEvent(category, source, data) {
840
+ return {
841
+ id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2),
842
+ timestamp: /* @__PURE__ */ new Date(),
843
+ source,
844
+ category,
845
+ data
846
+ };
847
+ }
848
+ /**
849
+ * Emit a `system.ready-state` event for a capability. Caller supplies a
850
+ * per-process `generation` string — stable across a single process
851
+ * lifetime, changes on restart — which consumer-side registries use to
852
+ * derive a monotonic `epoch` without requiring the emitter to
853
+ * coordinate.
854
+ */
855
+ function emitReadiness(bus, params) {
856
+ const ts = params.ts ?? Date.now();
857
+ bus.emit(createEvent("system.ready-state", {
858
+ type: "capability",
859
+ id: params.capName,
860
+ nodeId: params.sourceNodeId
861
+ }, {
862
+ capName: params.capName,
863
+ scope: params.scope,
864
+ state: params.state,
865
+ generation: params.generation,
866
+ sourceNodeId: params.sourceNodeId,
867
+ ts
868
+ }));
869
+ }
870
+ //#endregion
871
+ //#region src/addon/durable-state.ts
872
+ /**
873
+ * Build a {@link DurableState} over a single store key. Transport-agnostic:
874
+ * `read`/`write` are the addon-store or device-store accessors. The whole
875
+ * validated value round-trips on every read/write — the schema is the
876
+ * single source of truth for what is persisted, so no hand-listed field
877
+ * set can drop a key on save.
878
+ */
879
+ function createDurableState(deps) {
880
+ const get = async () => {
881
+ const raw = (await deps.read())[deps.key];
882
+ if (raw === void 0) return deps.fallback;
883
+ const parsed = deps.schema.safeParse(raw);
884
+ if (!parsed.success) {
885
+ deps.onParseError?.(deps.key, parsed.error);
886
+ return deps.fallback;
887
+ }
888
+ return parsed.data;
889
+ };
890
+ const set = async (next) => {
891
+ const validated = deps.schema.parse(next);
892
+ await deps.write({ [deps.key]: validated });
893
+ };
894
+ const update = async (fn) => {
895
+ await set(fn(await get()));
896
+ };
897
+ return {
898
+ get,
899
+ set,
900
+ update
901
+ };
902
+ }
903
+ //#endregion
904
+ //#region src/addon/base-addon.ts
905
+ /**
906
+ * Base class for CamStack addons. Eliminates settings boilerplate:
907
+ *
908
+ * - Typed `config` property with automatic resolution from store + defaults
909
+ * - `getGlobalSettings()` / `updateGlobalSettings()` auto-implemented
910
+ * - `getAddonSettings()` / `updateAddonSettings()` auto-implemented
911
+ * - `getDeviceSettings()` / `updateDeviceSettings()` auto-implemented
912
+ * - `ctx` accessor for the AddonContext (no manual `this.ctxRef` storage)
913
+ * - `field()` helper that constrains field keys to TConfig keys
914
+ * - `schema()` helper that validates the full schema structure
915
+ *
916
+ * Subclasses override:
917
+ * - `onInitialize()` — addon-specific init logic, return ProviderRegistration[]
918
+ * - `onShutdown()` — cleanup
919
+ * - `globalSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
920
+ *
921
+ * @example
922
+ * ```ts
923
+ * interface MyConfig {
924
+ * maxRetries: number
925
+ * endpoint: string
926
+ * }
927
+ *
928
+ * export default class MyAddon extends BaseAddon<MyConfig> {
929
+ * constructor() {
930
+ * super({ maxRetries: 3, endpoint: 'http://localhost' })
931
+ * }
932
+ *
933
+ * protected globalSettingsSchema() {
934
+ * return this.schema({
935
+ * sections: [{
936
+ * id: 'main', title: 'Settings',
937
+ * fields: [
938
+ * this.field({ type: 'number', key: 'maxRetries', label: 'Max Retries', default: 3 }),
939
+ * this.field({ type: 'text', key: 'endpoint', label: 'Endpoint' }),
940
+ * ],
941
+ * }],
942
+ * })
943
+ * }
944
+ *
945
+ * protected async onInitialize(): Promise<ProviderRegistration[]> {
946
+ * const client = new Client(this.config.endpoint, this.config.maxRetries)
947
+ * return [{ capability: 'my-feature', provider: client }]
948
+ * }
949
+ * }
950
+ * ```
951
+ */
952
+ var BaseAddon = class {
953
+ _ctx = null;
954
+ _config;
955
+ /**
956
+ * Per-process random id used as the `generation` stamp on every
957
+ * `system.ready-state` event this addon emits. Constant for the
958
+ * lifetime of this addon instance (== one process boot); consumer-
959
+ * side registries derive a monotonic `epoch` by watching for
960
+ * generation transitions.
961
+ */
962
+ _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
963
+ /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
964
+ _registeredCapNames = [];
965
+ /** Default config values. Provided via constructor. */
966
+ defaults;
967
+ constructor(defaults) {
968
+ this.defaults = defaults;
969
+ this._config = { ...defaults };
970
+ }
971
+ /**
972
+ * Override to opt out of the automatic `system.ready-state` emission.
973
+ * Returns `true` by default — addons that don't map cleanly to the
974
+ * readiness protocol (e.g. pure collection providers whose readiness
975
+ * is already reported per-device by upstream) can override to `false`
976
+ * and emit manually.
977
+ */
978
+ get autoEmitReadiness() {
979
+ return true;
980
+ }
981
+ /** The AddonContext, available after initialize(). */
982
+ get ctx() {
983
+ if (!this._ctx) throw new Error(`${this.constructor.name}: ctx accessed before initialize()`);
984
+ return this._ctx;
985
+ }
986
+ /**
987
+ * Non-throwing ctx accessor for code paths that can legitimately run
988
+ * before `initialize()` has resolved — most commonly cap handlers the
989
+ * hub invokes eagerly at page load (device-details aggregator pings
990
+ * every addon's `getDeviceSettingsContribution` as soon as the page
991
+ * mounts). Prefer `ctx` for the normal case; reach for this only in
992
+ * capability methods that may be queried before the addon is wired up.
993
+ */
994
+ get ctxIfReady() {
995
+ return this._ctx;
996
+ }
997
+ /** Current resolved config (defaults merged with persisted store values). */
998
+ get config() {
999
+ return this._config;
1000
+ }
1001
+ async initialize(context) {
1002
+ this._ctx = context;
1003
+ await this.resolveConfig();
1004
+ const result = await this.onInitialize();
1005
+ this.emitLifecycle(EventCategory.AddonStarted);
1006
+ const normalized = normalizeAddonInitResult(result);
1007
+ const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
1008
+ this._registeredCapNames = providers.map((p) => p.capability.name);
1009
+ return normalized;
1010
+ }
1011
+ /**
1012
+ * Called by the isolated-process runner AFTER `broker.start()`.
1013
+ * In-process addons never need this because the hub broker is already
1014
+ * running when `initialize()` fires. For forked children the broker
1015
+ * starts AFTER `initialize()`, so the readiness emit inside initialize()
1016
+ * fires before the broker can broadcast it. This method re-emits the
1017
+ * ready state once the transport is live.
1018
+ */
1019
+ postBrokerStart() {
1020
+ if (this.autoEmitReadiness && this._registeredCapNames.length > 0) this.emitReadinessForProviders("ready");
1021
+ }
1022
+ /**
1023
+ * Called by the isolated-process runner / agent bootstrap once the broker
1024
+ * has started AND the hub node is connected — the moment `ctx.api.*` calls
1025
+ * to hub-provided capabilities become safe. Wrap any bootstrap work that
1026
+ * must query the hub (device restore, settings fetch, initial sync) in
1027
+ * `onHubConnected()` rather than `onInitialize()` — during `initialize()`
1028
+ * on a worker the broker is not yet connected and remote cap calls either
1029
+ * time out or throw "Service not found".
1030
+ *
1031
+ * Default implementation is a no-op. Override in subclasses that need it.
1032
+ * Never fires on hub-local in-process addons (the hub is its own node).
1033
+ */
1034
+ async onHubReachable() {}
1035
+ async shutdown() {
1036
+ this.emitLifecycle(EventCategory.AddonStopped);
1037
+ if (this.autoEmitReadiness) this.emitReadinessForProviders("down");
1038
+ await this.onShutdown();
1039
+ for (const unsub of this._subscriptions) unsub();
1040
+ this._subscriptions = [];
1041
+ this._ctx = null;
1042
+ }
1043
+ /** Addon-specific cleanup. Override if needed. */
1044
+ async onShutdown() {}
1045
+ /**
1046
+ * Called after config is resolved during updateGlobalSettings/updateAddonSettings.
1047
+ * Override to react to config changes (e.g. restart a sampler, reconnect a service).
1048
+ * Not called during initialize() — use onInitialize() for initial setup.
1049
+ */
1050
+ async onConfigChanged() {}
1051
+ /**
1052
+ * Create a ConfigField with `key` constrained to keys of TConfig.
1053
+ * Provides autocomplete and compile-time validation.
1054
+ */
1055
+ field(field) {
1056
+ return field;
1057
+ }
1058
+ /**
1059
+ * Create a full ConfigUISchema with typed sections.
1060
+ * Fields created via `this.field()` get key validation automatically.
1061
+ */
1062
+ schema(schema) {
1063
+ return schema;
1064
+ }
1065
+ /** Override to provide global-level settings UI schema. */
1066
+ globalSettingsSchema(_cap) {
1067
+ return null;
1068
+ }
1069
+ /** Override to provide device-level settings UI schema. */
1070
+ deviceSettingsSchema() {
1071
+ return null;
1072
+ }
1073
+ async getGlobalSettings(overlay, cap) {
1074
+ const schema = this.globalSettingsSchema(cap);
1075
+ if (!schema) return { sections: [] };
1076
+ const raw = await this._ctx?.settings?.readAddonStore() ?? {};
1077
+ return hydrateSchema(schema, overlay ? {
1078
+ ...raw,
1079
+ ...overlay
1080
+ } : raw);
1081
+ }
1082
+ async updateGlobalSettings(patch) {
1083
+ await this._ctx?.settings?.writeAddonStore(patch);
1084
+ await this.resolveConfig();
1085
+ await this.onConfigChanged();
1086
+ this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
1087
+ this.maybeAutoRestart(patch, this.globalSettingsSchema());
1088
+ }
1089
+ /**
1090
+ * If any field in `patch` is marked `requiresRestart` in `schema`,
1091
+ * schedule an addon restart for the next tick. Deferred via
1092
+ * `setImmediate` so the tRPC mutation that triggered the write has
1093
+ * time to return its response before the addon is torn down and
1094
+ * re-initialised by `AddonRegistryService.restartAddon`.
1095
+ */
1096
+ maybeAutoRestart(patch, schema) {
1097
+ if (!schema) return;
1098
+ const restartKeys = /* @__PURE__ */ new Set();
1099
+ for (const section of schema.sections) for (const field of section.fields) {
1100
+ if (field.type === "separator" || field.type === "info") continue;
1101
+ if (field.requiresRestart) restartKeys.add(field.key);
1102
+ }
1103
+ if (restartKeys.size === 0) return;
1104
+ if (!Object.keys(patch).some((k) => restartKeys.has(k))) return;
1105
+ const ctx = this._ctx;
1106
+ if (!ctx) return;
1107
+ const addonId = ctx.id;
1108
+ setImmediate(() => {
1109
+ ctx.api.addons?.restartAddon?.mutate({ addonId }).then(() => {
1110
+ ctx.logger.info("addon auto-restart triggered by restart-required setting change", { meta: { changedFields: Object.keys(patch).filter((k) => restartKeys.has(k)) } });
1111
+ }).catch((err) => {
1112
+ ctx.logger.error("addon auto-restart failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
1113
+ });
1114
+ });
1115
+ }
1116
+ async getDeviceSettings(deviceId) {
1117
+ const schema = this.deviceSettingsSchema();
1118
+ if (!schema) return { sections: [] };
1119
+ return hydrateSchema(schema, await this._ctx?.settings?.readDeviceStore(deviceId) ?? {});
1120
+ }
1121
+ async updateDeviceSettings(deviceId, patch) {
1122
+ await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
1123
+ }
1124
+ _subscriptions = [];
1125
+ /**
1126
+ * Subscribe to an event bus category. The subscription is automatically
1127
+ * unsubscribed on shutdown — no manual cleanup needed.
1128
+ *
1129
+ * @example
1130
+ * ```ts
1131
+ * this.subscribe({ category: EventCategory.DeviceRegistered }, (event) => {
1132
+ * void this.handleDevice(event)
1133
+ * })
1134
+ * ```
1135
+ */
1136
+ /**
1137
+ * Subscribe to `system.ready-state` events for one or more capabilities.
1138
+ * Abstracts the boilerplate type-narrowing + nodeId extraction that every
1139
+ * resilience subscription duplicates. Cleanup is automatic (registered on
1140
+ * `_subscriptions` like a normal `subscribe` call).
1141
+ */
1142
+ watchCapability(capNames, handlers) {
1143
+ const nameSet = new Set(Array.isArray(capNames) ? capNames : [capNames]);
1144
+ this.subscribe({ category: "system.ready-state" }, (event) => {
1145
+ const data = event.data;
1146
+ if (typeof data.capName !== "string") return;
1147
+ if (!nameSet.has(data.capName)) return;
1148
+ if (data.scope?.type !== "node") return;
1149
+ const nodeId = data.scope.nodeId;
1150
+ if (typeof nodeId !== "string" || nodeId.length === 0) return;
1151
+ const capName = data.capName;
1152
+ if (data.state === "down") handlers.onDown?.(nodeId, capName);
1153
+ else if (data.state === "ready") handlers.onReady?.(nodeId, capName);
1154
+ });
1155
+ }
1156
+ subscribe(filter, handler) {
1157
+ const unsub = this.ctx.eventBus.subscribe(filter, handler);
1158
+ this._subscriptions.push(unsub);
1159
+ }
1160
+ emitLifecycle(category, data) {
1161
+ try {
1162
+ const ctx = this._ctx;
1163
+ if (!ctx) return;
1164
+ ctx.eventBus.emit({
1165
+ id: `${ctx.id}-${Date.now()}`,
1166
+ timestamp: /* @__PURE__ */ new Date(),
1167
+ source: {
1168
+ type: "addon",
1169
+ id: ctx.id,
1170
+ nodeId: ctx.kernel.localNodeId ?? "hub"
1171
+ },
1172
+ category,
1173
+ data: {
1174
+ addonId: ctx.id,
1175
+ ...data
1176
+ }
1177
+ });
1178
+ } catch {}
1179
+ }
1180
+ /**
1181
+ * Emit a `system.ready-state` event for every capability this addon
1182
+ * registered at init. Scope is `{type:'node', nodeId}` — readiness is
1183
+ * tied to the node that hosts the provider. Consumers that care about
1184
+ * per-device readiness can subscribe with `{type:'device', ...}` if
1185
+ * the provider emits at finer granularity (collection addons may
1186
+ * opt-out via `autoEmitReadiness` and emit manually).
1187
+ */
1188
+ emitReadinessForProviders(state) {
1189
+ const ctx = this._ctx;
1190
+ if (!ctx) return;
1191
+ if (this._registeredCapNames.length === 0) return;
1192
+ const rawNodeId = ctx.kernel?.localNodeId ?? "hub";
1193
+ const nodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
1194
+ for (const capName of this._registeredCapNames) try {
1195
+ emitReadiness(ctx.eventBus, {
1196
+ capName,
1197
+ scope: {
1198
+ type: "node",
1199
+ nodeId
1200
+ },
1201
+ state,
1202
+ generation: this._readinessGeneration,
1203
+ sourceNodeId: nodeId
1204
+ });
1205
+ } catch {}
1206
+ }
1207
+ /**
1208
+ * Resolve the shared models directory path via the storage capability.
1209
+ * Falls back to `camstack-data/models` if storage is unavailable.
1210
+ * Used by inference addons (detection-pipeline, audio-classifier, embedding-encoder).
1211
+ */
1212
+ async resolveModelsDir() {
1213
+ return this.ctx.api.storage.resolve.query({
1214
+ location: "models",
1215
+ relativePath: ""
1216
+ }).catch(() => "camstack-data/models");
1217
+ }
1218
+ /**
1219
+ * Access the runtime capability registry for in-process provider lookups.
1220
+ * Returns null if the registry is not available (e.g. on agents).
1221
+ * Used by addons that consume other capabilities directly (snapshot, stream-broker, enrichment).
1222
+ */
1223
+ get capabilities() {
1224
+ return this.ctx.capabilities ?? null;
1225
+ }
1226
+ /**
1227
+ * Resolve config by merging defaults with persisted store values.
1228
+ * Called automatically during initialize() and after every updateSettings().
1229
+ *
1230
+ * The merge is shallow: each key in `defaults` is checked against the store.
1231
+ * Only keys present in defaults are read — the store can contain extra keys
1232
+ * (e.g. from older versions) without polluting the typed config.
1233
+ */
1234
+ async resolveConfig() {
1235
+ const stored = await this.readAddonStoreWithRetry();
1236
+ const resolved = { ...this.defaults };
1237
+ for (const key of Object.keys(this.defaults)) {
1238
+ const storedValue = stored[key];
1239
+ if (storedValue !== void 0 && storedValue !== null) {
1240
+ const defaultType = typeof this.defaults[key];
1241
+ if (typeof storedValue === defaultType) resolved[key] = storedValue;
1242
+ }
1243
+ }
1244
+ this._config = resolved;
1245
+ }
1246
+ /**
1247
+ * Typed durable handle over ONE key of this addon's store. The whole
1248
+ * Zod-validated value round-trips on every read/write — no hand-listed
1249
+ * fields, so a field can never be silently dropped on persist. Reads use
1250
+ * the same retry budget as config resolution; a corrupt/legacy blob logs
1251
+ * a warning and falls back rather than crashing boot.
1252
+ */
1253
+ state(key, schema, fallback) {
1254
+ return createDurableState({
1255
+ key,
1256
+ schema,
1257
+ fallback,
1258
+ read: () => this.readAddonStoreWithRetry(),
1259
+ write: async (patch) => {
1260
+ await this._ctx?.settings?.writeAddonStore(patch);
1261
+ },
1262
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
1263
+ addonId: this._ctx?.id,
1264
+ key: k,
1265
+ error: String(e)
1266
+ } })
1267
+ });
1268
+ }
1269
+ /** Per-device variant of {@link state}, backed by the per-device store. */
1270
+ deviceState(deviceId, key, schema, fallback) {
1271
+ return createDurableState({
1272
+ key,
1273
+ schema,
1274
+ fallback,
1275
+ read: async () => await this._ctx?.settings?.readDeviceStore(deviceId) ?? {},
1276
+ write: async (patch) => {
1277
+ await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
1278
+ },
1279
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored device ${deviceId} "${k}" failed validation — using fallback`, { meta: {
1280
+ addonId: this._ctx?.id,
1281
+ deviceId,
1282
+ key: k,
1283
+ error: String(e)
1284
+ } })
1285
+ });
1286
+ }
1287
+ /**
1288
+ * Wrap `ctx.settings.readAddonStore()` with a short retry budget so a
1289
+ * transient settings-store outage (mid-restart of sqlite-settings, tsx-watch
1290
+ * swap, or any race where the SqliteSettingsBackend is between shutdown and
1291
+ * re-initialize) doesn't propagate up into `initialize()` and leave the
1292
+ * addon permanently broken.
1293
+ *
1294
+ * Retry only the two known infra fingerprints. Anything else propagates so
1295
+ * real bugs surface immediately. After the budget expires we fall back to
1296
+ * `{}` (defaults) — the addon's first successful patch will rehydrate.
1297
+ */
1298
+ async readAddonStoreWithRetry() {
1299
+ const settings = this._ctx?.settings;
1300
+ if (!settings) return {};
1301
+ const delaysMs = [
1302
+ 150,
1303
+ 350,
1304
+ 600,
1305
+ 900
1306
+ ];
1307
+ let lastErr;
1308
+ for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
1309
+ return await settings.readAddonStore() ?? {};
1310
+ } catch (err) {
1311
+ lastErr = err;
1312
+ const msg = err instanceof Error ? err.message : String(err);
1313
+ if (!(msg.includes("SqliteSettingsBackend not initialized") || msg.includes("provider not available"))) throw err;
1314
+ if (attempt === delaysMs.length) break;
1315
+ await new Promise((r) => setTimeout(r, delaysMs[attempt]));
1316
+ }
1317
+ this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
1318
+ return {};
1319
+ }
1320
+ };
1321
+ /**
1322
+ * Normalize an `ICamstackAddon.initialize()` return value into the
1323
+ * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
1324
+ * envelopes pass through; void stays void.
1325
+ *
1326
+ * Exported so the kernel boot path and the backend addon registry can
1327
+ * share the same normalizer instead of duplicating the shim. Addons that
1328
+ * extend `BaseAddon` already emit the envelope, so this helper is only a
1329
+ * safety net for direct `ICamstackAddon` implementations (mostly tests).
1330
+ */
1331
+ function normalizeAddonInitResult(result) {
1332
+ if (result == null) return;
1333
+ if (Array.isArray(result)) return { providers: result };
1334
+ return result;
1335
+ }
1336
+ //#endregion
1337
+ //#region src/capabilities/schemas/streaming-shared.ts
1338
+ /** Shared Zod schemas used across streaming capabilities. */
1339
+ var CamProfileSchema = z.enum([
1340
+ "high",
1341
+ "mid",
1342
+ "low"
1343
+ ]);
1344
+ /** Canonical ordering. Hard-coded; never sorted. */
1345
+ var CAM_PROFILE_ORDER = [
1346
+ "high",
1347
+ "mid",
1348
+ "low"
1349
+ ];
1350
+ var CamStreamKindSchema = z.enum([
1351
+ "pull-rtsp",
1352
+ "pull-rtmp",
1353
+ "pull-http",
1354
+ "pull-rfc4571",
1355
+ "push-annexb",
1356
+ "derived"
1357
+ ]);
1358
+ var CamStreamResolutionSchema = z.object({
1359
+ width: z.number().int().positive(),
1360
+ height: z.number().int().positive()
1361
+ });
1362
+ var CameraStreamSchema = z.object({
1363
+ /** Stable, provider-assigned id unique within the (deviceId) scope. */
1364
+ camStreamId: z.string().min(1),
1365
+ deviceId: z.number().int().nonnegative(),
1366
+ kind: CamStreamKindSchema,
1367
+ /** Required for pull-* kinds. Ignored for push-annexb. */
1368
+ url: z.string().optional(),
1369
+ codec: z.string().optional(),
1370
+ resolution: CamStreamResolutionSchema.optional(),
1371
+ fps: z.number().positive().optional(),
1372
+ /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
1373
+ label: z.string().optional(),
1374
+ /**
1375
+ * Device-level features the publisher advertised (e.g. `battery-operated`).
1376
+ * The broker, snapshot orchestrator, and prebuffer manager all consult
1377
+ * this list to derive policy — relaxed stall watchdog for battery
1378
+ * cams, prebuffer off by default, longer snapshot rate-limit, etc.
1379
+ *
1380
+ * Single source of truth replacing per-stream flags like the
1381
+ * historical `allowStall`: if the publisher knows the camera is
1382
+ * battery-powered, every downstream service derives the right policy
1383
+ * from this list.
1384
+ */
1385
+ deviceFeatures: z.array(z.string()).optional(),
1386
+ /**
1387
+ * Whether this stream participates in the broker's automatic profile
1388
+ * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
1389
+ * use `false` when they want a stream to be SELECTABLE in the UI but not
1390
+ * picked by default — e.g. Reolink publishes its native Baichuan streams
1391
+ * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
1392
+ * mirrors as `autoEligible: false` (still pickable per slot, just not
1393
+ * the auto choice). Manual `assignProfile` calls remain valid for
1394
+ * non-eligible streams.
1395
+ */
1396
+ autoEligible: z.boolean().optional(),
1397
+ /**
1398
+ * Transport-specific opaque metadata. The broker passes it through to
1399
+ * the source reader without inspecting it. Currently used by
1400
+ * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
1401
+ * route RTP packets to the right depacketizer without an in-band
1402
+ * DESCRIBE phase). Other kinds typically leave it undefined.
1403
+ */
1404
+ metadata: z.record(z.string(), z.unknown()).optional()
1405
+ });
1406
+ var ProfileSlotStatusSchema = z.enum([
1407
+ "unassigned",
1408
+ "idle",
1409
+ "connecting",
1410
+ "streaming",
1411
+ "error"
1412
+ ]);
1413
+ var ProfileSlotSchema = z.object({
1414
+ deviceId: z.number().int().nonnegative(),
1415
+ profile: CamProfileSchema,
1416
+ /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
1417
+ brokerId: z.string(),
1418
+ /** `null` when the profile is unassigned. */
1419
+ sourceCamStreamId: z.string().nullable(),
1420
+ status: ProfileSlotStatusSchema,
1421
+ resolution: CamStreamResolutionSchema.optional(),
1422
+ codec: z.string().optional(),
1423
+ preBufferSec: z.number().nonnegative().optional(),
1424
+ errorMessage: z.string().optional()
1425
+ });
1426
+ /** The canonical consumer-facing broker id for a device profile. */
1427
+ function makeProfileBrokerId(deviceId, profile) {
1428
+ return `${deviceId}/${profile}`;
1429
+ }
1430
+ /**
1431
+ * The broker id keying a physical SOURCE stream: `${deviceId}/${camStreamId}`.
1432
+ * This is the broker's internal source key (`brokerIdFor` in the manager) — a
1433
+ * `camStreamId` like `native:main` — as opposed to the public profile alias
1434
+ * {@link makeProfileBrokerId}. Used to resolve a source's restream endpoint
1435
+ * (e.g. routing a transcode's ffmpeg input through the broker so it rides the
1436
+ * single source dial instead of opening a second camera connection).
1437
+ */
1438
+ function makeSourceBrokerId(deviceId, camStreamId) {
1439
+ return `${deviceId}/${camStreamId}`;
1440
+ }
1441
+ /**
1442
+ * Inverse of {@link makeProfileBrokerId}. Returns `null` when the string is
1443
+ * not a canonical `${deviceId}/${profile}` id — e.g. a broker-internal
1444
+ * cam-stream id (`5/native:main`), a derived/adaptive ref, or a malformed
1445
+ * value. So a caller can safely distinguish "addresses a public profile"
1446
+ * from "addresses something broker-internal".
1447
+ */
1448
+ function parseProfileBrokerId(brokerId) {
1449
+ const slash = brokerId.indexOf("/");
1450
+ if (slash <= 0) return null;
1451
+ const idPart = brokerId.slice(0, slash);
1452
+ const profilePart = brokerId.slice(slash + 1);
1453
+ if (!/^\d+$/.test(idPart)) return null;
1454
+ const parsed = CamProfileSchema.safeParse(profilePart);
1455
+ if (!parsed.success) return null;
1456
+ return {
1457
+ deviceId: Number(idPart),
1458
+ profile: parsed.data
1459
+ };
1460
+ }
1461
+ /**
1462
+ * The profile slots worth consuming/recording: ASSIGNED only
1463
+ * (`sourceCamStreamId != null`), DEDUPED by physical source so the same
1464
+ * camera encoder is never subscribed/recorded twice, ordered high→mid→low.
1465
+ * Optionally scoped to one `deviceId`. Pure; never mutates the input.
1466
+ *
1467
+ * Recording `main` and `mid` when both point at the same `sourceCamStreamId`
1468
+ * would dial the camera's internal stream twice for no benefit — this is the
1469
+ * dedup that prevents that.
1470
+ */
1471
+ function selectAssignedProfileSlots(slots, deviceId) {
1472
+ const ordered = [...slots].filter((s) => deviceId === void 0 || s.deviceId === deviceId).toSorted((a, b) => CAM_PROFILE_ORDER.indexOf(a.profile) - CAM_PROFILE_ORDER.indexOf(b.profile));
1473
+ const seenSources = /* @__PURE__ */ new Set();
1474
+ const out = [];
1475
+ for (const s of ordered) {
1476
+ const src = s.sourceCamStreamId;
1477
+ if (src === null) continue;
1478
+ if (seenSources.has(src)) continue;
1479
+ seenSources.add(src);
1480
+ out.push(s);
1481
+ }
1482
+ return out;
1483
+ }
1484
+ /**
1485
+ * Zod schema for StreamSourceEntry — the canonical stream descriptor
1486
+ * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
1487
+ */
1488
+ var StreamSourceEntrySchema$1 = z.object({
1489
+ id: z.string(),
1490
+ label: z.string(),
1491
+ protocol: z.enum([
1492
+ "rtsp",
1493
+ "rtmp",
1494
+ "annexb",
1495
+ "http-mjpeg",
1496
+ "webrtc",
1497
+ "custom"
1498
+ ]),
1499
+ url: z.string().optional(),
1500
+ resolution: z.object({
1501
+ width: z.number(),
1502
+ height: z.number()
1503
+ }).readonly().optional(),
1504
+ fps: z.number().optional(),
1505
+ bitrate: z.number().optional(),
1506
+ codec: z.string().optional(),
1507
+ profileHint: CamProfileSchema.optional()
1508
+ });
1509
+ var StreamSourceSchema = z.object({
1510
+ type: z.string(),
1511
+ url: z.string(),
1512
+ videoCodec: z.string().optional(),
1513
+ audioCodec: z.string().optional(),
1514
+ metadata: z.record(z.string(), z.unknown()).readonly().optional()
1515
+ });
1516
+ var EncodedPacketSchema = z.object({
1517
+ type: z.enum(["video", "audio"]),
1518
+ data: z.instanceof(Uint8Array),
1519
+ pts: z.number(),
1520
+ dts: z.number(),
1521
+ keyframe: z.boolean(),
1522
+ codec: z.string()
1523
+ });
1524
+ var DecodedFrameSchema = z.object({
1525
+ data: z.instanceof(Uint8Array),
1526
+ width: z.number(),
1527
+ height: z.number(),
1528
+ format: z.enum([
1529
+ "jpeg",
1530
+ "rgb",
1531
+ "bgr",
1532
+ "yuv420",
1533
+ "gray"
1534
+ ]),
1535
+ timestamp: z.number()
1536
+ });
1537
+ /**
1538
+ * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
1539
+ * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
1540
+ * interface in `interfaces/frame-handle.ts` field-for-field so a decoder cap
1541
+ * method (`pullHandles`) can carry handles over tRPC / Moleculer.
1542
+ *
1543
+ * The `satisfies` assertion below pins this schema to the interface: if a
1544
+ * field is added to / removed from `FrameHandle` without a matching schema
1545
+ * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
1546
+ * `FrameHandle` and the build fails — no silent drift.
1547
+ */
1548
+ var FrameHandleSchema = z.object({
1549
+ shmId: z.string(),
1550
+ slot: z.number().int().nonnegative(),
1551
+ seq: z.number().int().nonnegative(),
1552
+ width: z.number().int().positive(),
1553
+ height: z.number().int().positive(),
1554
+ format: z.enum([
1555
+ "jpeg",
1556
+ "rgb",
1557
+ "bgr",
1558
+ "yuv420",
1559
+ "gray"
1560
+ ]),
1561
+ pts: z.number(),
1562
+ byteLength: z.number().int().nonnegative(),
1563
+ nodeId: z.string(),
1564
+ slotCount: z.number().int().positive()
1565
+ });
1566
+ /**
1567
+ * Pixel format a frame-handle subscriber can request from the broker
1568
+ * (Phase 5 / D9). The packed, raster subset of `FrameFormat` — the formats a
1569
+ * `frameSink: 'shm'` decoder session can write into a `FrameRing` slot.
1570
+ * `jpeg` is excluded: a variable-length stream is not a fixed-stride ring
1571
+ * payload, and no decoded-frame consumer requests it over the shm plane.
1572
+ */
1573
+ var FrameHandleFormatSchema = z.enum([
1574
+ "rgb",
1575
+ "bgr",
1576
+ "yuv420",
1577
+ "gray"
1578
+ ]);
1579
+ /**
1580
+ * Input for `stream-broker.subscribeFrames` (Phase 5 / D9). A consumer asks the
1581
+ * broker for a `FrameHandle` stream of one `format`; the broker maintains one
1582
+ * shared-memory ring per `(brokerId, format)` actually requested, so a `gray`
1583
+ * subscriber (motion) and an `rgb` subscriber (detection) each read the format
1584
+ * they asked for with no broker-side conversion.
1585
+ */
1586
+ var SubscribeFramesInputSchema = z.object({
1587
+ brokerId: z.string(),
1588
+ format: FrameHandleFormatSchema,
1589
+ /**
1590
+ * Optional reader-side cadence hint in frames per second. The broker does
1591
+ * NOT throttle — latest-wins ring reads drop frames implicitly for a slow
1592
+ * consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
1593
+ * the consumer can pace its own `pullFrameHandles` polling.
1594
+ */
1595
+ maxFps: z.number().positive().optional(),
1596
+ /** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
1597
+ tag: z.string().optional()
1598
+ });
1599
+ /**
1600
+ * Result of `stream-broker.subscribeFrames`. The consumer then polls
1601
+ * `pullFrameHandles({ subscriptionId, maxCount })` and feeds each returned
1602
+ * `FrameHandle` to a `FrameRingReader`.
1603
+ */
1604
+ var SubscribeFramesResultSchema = z.object({
1605
+ /** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
1606
+ subscriptionId: z.string(),
1607
+ /** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
1608
+ maxFps: z.number().nonnegative()
1609
+ });
1610
+ /**
1611
+ * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
1612
+ * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
1613
+ * sample bytes plus the track parameters a consumer needs to interpret them.
1614
+ *
1615
+ * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
1616
+ * Float32 PCM), so unlike video frames they ship their bytes INLINE over
1617
+ * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
1618
+ * (the wire-serialisable supertype of `Buffer`) to match `DecodedFrameSchema`
1619
+ * / `EncodedPacketSchema`'s precedent; a `Buffer` is assignable to it.
1620
+ */
1621
+ var DecodedAudioChunkSchema = z.object({
1622
+ data: z.instanceof(Uint8Array),
1623
+ sampleRate: z.number().int().positive(),
1624
+ channels: z.number().int().positive(),
1625
+ timestamp: z.number()
1626
+ });
1627
+ /**
1628
+ * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). The
1629
+ * handle-free, ship-bytes-inline analogue of `subscribeFrames`: a consumer
1630
+ * asks the broker for the decoded audio-chunk stream of one broker; the
1631
+ * broker keeps a per-subscription bounded FIFO queue the consumer drains via
1632
+ * `pullAudioChunks`.
1633
+ */
1634
+ var SubscribeAudioChunksInputSchema = z.object({
1635
+ brokerId: z.string(),
1636
+ /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
1637
+ tag: z.string().optional()
1638
+ });
1639
+ /** Result of `stream-broker.subscribeAudioChunks`. */
1640
+ var SubscribeAudioChunksResultSchema = z.object({
1641
+ /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
1642
+ subscriptionId: z.string() });
1643
+ var BrokerStatusSchema = z.enum([
1644
+ "idle",
1645
+ "connecting",
1646
+ "streaming",
1647
+ "error",
1648
+ "stopped"
1649
+ ]);
1650
+ var BrokerStatsSchema = z.object({
1651
+ status: BrokerStatusSchema,
1652
+ inputFps: z.number(),
1653
+ decodeFps: z.number(),
1654
+ encodedSubscribers: z.number(),
1655
+ decodedSubscribers: z.number(),
1656
+ uptimeMs: z.number(),
1657
+ bitrateKbps: z.number(),
1658
+ idrIntervalMs: z.number(),
1659
+ codec: z.string().optional(),
1660
+ totalBytes: z.number(),
1661
+ packetCount: z.number(),
1662
+ rtspClients: z.number(),
1663
+ pipeClients: z.number(),
1664
+ preBufferSec: z.number(),
1665
+ preBufferMs: z.number(),
1666
+ preBufferPackets: z.number(),
1667
+ /**
1668
+ * Moleculer node id of the decoder provider currently servicing this
1669
+ * stream's decoded subscribers. `null` until the deferred decoder is
1670
+ * created (no decoded clients yet, or codec not detected). Surfaces the
1671
+ * runtime decoder placement so the UI can show "Decoder: <agent>" without
1672
+ * a separate cap call per broker.
1673
+ */
1674
+ decoderNodeId: z.string().nullable(),
1675
+ /**
1676
+ * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
1677
+ * when the stream has no audio track or the broker is in cold start.
1678
+ * `supported = false` means the codec was detected but the local
1679
+ * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
1680
+ * AAC pipeline wired). Surfaced in the UI device-overview so operators
1681
+ * can pre-pick the audio-analysis model that matches the codec.
1682
+ */
1683
+ audio: z.object({
1684
+ codec: z.string(),
1685
+ sampleRate: z.number(),
1686
+ channels: z.number(),
1687
+ supported: z.boolean()
1688
+ }).nullable().optional()
1689
+ });
1690
+ /**
1691
+ * Exporter-facing "profile restream" entry. Returned by
1692
+ * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
1693
+ * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
1694
+ * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
1695
+ * RTSP restream that aliases the profile's assigned source broker, so
1696
+ * every consumer dialling a profile converges on the same single pull.
1697
+ * `codec`/`resolution` describe the assigned SOURCE camStream (for the
1698
+ * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
1699
+ * (raw per-camStream, live-view only).
1700
+ */
1701
+ var ProfileRtspEntrySchema = z.object({
1702
+ profile: CamProfileSchema,
1703
+ /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
1704
+ brokerId: z.string(),
1705
+ url: z.string(),
1706
+ mutedUrl: z.string(),
1707
+ enabled: z.boolean(),
1708
+ codec: z.string().optional(),
1709
+ resolution: CamStreamResolutionSchema.optional()
1710
+ });
1711
+ //#endregion
1712
+ //#region src/readiness/readiness-registry.ts
1713
+ var ReadinessTimeoutError = class extends Error {
1714
+ capName;
1715
+ scope;
1716
+ waitedMs;
1717
+ constructor(capName, scope, waitedMs) {
1718
+ super(`Timed out waiting for ${capName} (${scopeKey(scope)}) to become ready after ${waitedMs}ms`);
1719
+ this.name = "ReadinessTimeoutError";
1720
+ this.capName = capName;
1721
+ this.scope = scope;
1722
+ this.waitedMs = waitedMs;
1723
+ }
1724
+ };
1725
+ /**
1726
+ * Build a canonical string key for a `(capName, scope)` pair. Used as
1727
+ * the snapshot map key and for log/debug output.
1728
+ */
1729
+ function readinessKey(capName, scope) {
1730
+ return `${capName}|${scopeKey(scope)}`;
1731
+ }
1732
+ function scopeKey(scope) {
1733
+ switch (scope.type) {
1734
+ case "global": return "global";
1735
+ case "node": return `node:${scope.nodeId}`;
1736
+ case "device": return `device:${scope.deviceId}`;
1737
+ }
1738
+ }
1739
+ function scopesEqual(a, b) {
1740
+ if (a.type !== b.type) return false;
1741
+ if (a.type === "global" || b.type === "global") return true;
1742
+ if (a.type === "node" && b.type === "node") return a.nodeId === b.nodeId;
1743
+ if (a.type === "device" && b.type === "device") return a.deviceId === b.deviceId;
1744
+ return false;
1745
+ }
1746
+ var ReadinessRegistry = class {
1747
+ bus;
1748
+ sourceNodeId;
1749
+ logger;
1750
+ now;
1751
+ generation;
1752
+ snapshot = /* @__PURE__ */ new Map();
1753
+ subscriptions = /* @__PURE__ */ new Set();
1754
+ unsubscribeBus;
1755
+ unsubscribeAgentOffline;
1756
+ constructor(options) {
1757
+ this.bus = options.eventBus;
1758
+ this.sourceNodeId = options.sourceNodeId;
1759
+ this.logger = options.logger;
1760
+ this.now = options.now ?? (() => Date.now());
1761
+ this.generation = options.generation ?? randomGeneration();
1762
+ this.unsubscribeBus = this.bus.subscribe({ category: "system.ready-state" }, (event) => this.ingest(event.data));
1763
+ this.unsubscribeAgentOffline = this.bus.subscribe({ category: "agent.offline" }, (event) => this.synthesizeDownForNode(event.data.agentId));
1764
+ if (typeof this.bus.getRecent === "function") try {
1765
+ const recent = this.bus.getRecent({ category: "system.ready-state" });
1766
+ for (const event of recent) this.ingest(event.data);
1767
+ } catch {}
1768
+ }
1769
+ /** Release the event-bus subscription. Idempotent. */
1770
+ close() {
1771
+ this.unsubscribeBus();
1772
+ this.unsubscribeAgentOffline();
1773
+ this.subscriptions.clear();
1774
+ }
1775
+ /** Current snapshot for a `(capName, scope)` pair, or `null` if never seen. */
1776
+ get(capName, scope) {
1777
+ return this.snapshot.get(readinessKey(capName, scope)) ?? null;
1778
+ }
1779
+ /**
1780
+ * Serializable snapshot for cross-process transport. Returns an array
1781
+ * of `ReadinessRecord` plain objects — safe for MsgPack / JSON
1782
+ * transport. Used by the hub's `$readiness.getSnapshot` Moleculer
1783
+ * action; consumers hydrate their local registry from the result.
1784
+ */
1785
+ getSnapshotForTransport() {
1786
+ return Array.from(this.snapshot.values());
1787
+ }
1788
+ /**
1789
+ * Hydrate the snapshot from an authoritative source. Entries already
1790
+ * present locally are skipped — live deltas (received via the event
1791
+ * bus subscription) always take precedence over the snapshot. For
1792
+ * each newly hydrated entry, a one-shot transition is dispatched to
1793
+ * matching subscriptions so pending `awaitReady` callers unblock
1794
+ * without having to wait for a fresh event.
1795
+ *
1796
+ * Local `epoch` is reset to 1 per entry — consumer-side epoch is
1797
+ * derived from observed generation transitions, so this mirrors the
1798
+ * value that would have been assigned had the consumer observed the
1799
+ * first `ready` event directly.
1800
+ */
1801
+ hydrate(records) {
1802
+ const now = this.now();
1803
+ for (const record of records) {
1804
+ const key = readinessKey(record.capName, record.scope);
1805
+ if (this.snapshot.has(key)) continue;
1806
+ const hydrated = {
1807
+ capName: record.capName,
1808
+ scope: record.scope,
1809
+ state: record.state,
1810
+ generation: record.generation,
1811
+ epoch: 1,
1812
+ lastChange: now,
1813
+ sourceNodeId: record.sourceNodeId
1814
+ };
1815
+ this.snapshot.set(key, hydrated);
1816
+ if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → ${record.state} (hydrated, gen=${record.generation.slice(0, 6)})`);
1817
+ const transition = {
1818
+ capName: record.capName,
1819
+ scope: record.scope,
1820
+ state: record.state,
1821
+ epoch: 1,
1822
+ generation: record.generation,
1823
+ sourceNodeId: "hydrated",
1824
+ ts: now,
1825
+ durationInPrevState: 0
1826
+ };
1827
+ for (const sub of this.subscriptions) {
1828
+ if (sub.capName !== record.capName) continue;
1829
+ if (!scopesEqual(sub.scope, record.scope)) continue;
1830
+ try {
1831
+ sub.handler(transition);
1832
+ } catch (err) {
1833
+ this.logger?.warn(`readiness hydrate handler threw for ${record.capName}: ${err.message ?? String(err)}`);
1834
+ }
1835
+ }
1836
+ }
1837
+ }
1838
+ /** Shallow copy of the full snapshot — mainly for diagnostics/tests. */
1839
+ getAll() {
1840
+ return new Map(this.snapshot);
1841
+ }
1842
+ /**
1843
+ * Emit a `ready` transition for a locally-owned capability. The
1844
+ * payload carries this registry's `generation` so remote registries
1845
+ * can derive their own `epoch`.
1846
+ */
1847
+ emitReady(capName, scope) {
1848
+ this.emitTransition(capName, scope, "ready");
1849
+ }
1850
+ emitStarting(capName, scope) {
1851
+ this.emitTransition(capName, scope, "starting");
1852
+ }
1853
+ emitDown(capName, scope) {
1854
+ this.emitTransition(capName, scope, "down");
1855
+ }
1856
+ /**
1857
+ * One-shot: resolve once the cap is `ready`. Reads the snapshot
1858
+ * first — if already ready, resolves synchronously on the next tick.
1859
+ *
1860
+ * Default behaviour is **wait indefinitely** (timeoutMs = `Infinity`):
1861
+ * the orchestrator and other callers never want to attach a camera
1862
+ * with empty steps just because a cap took longer than 30s to come
1863
+ * up. Pass an explicit finite `timeoutMs` to bound the wait, or an
1864
+ * `AbortSignal` to cancel externally. `Infinity` is honoured natively
1865
+ * — no `setTimeout` is registered (Node's setTimeout silently clamps
1866
+ * values above ~24.8d, so we must skip the timer entirely).
1867
+ */
1868
+ awaitReady(capName, scope, opts = {}) {
1869
+ const timeoutMs = opts.timeoutMs ?? Number.POSITIVE_INFINITY;
1870
+ const start = this.now();
1871
+ if (this.get(capName, scope)?.state === "ready") return Promise.resolve();
1872
+ if (opts.signal?.aborted) return Promise.reject(opts.signal.reason ?? /* @__PURE__ */ new Error("aborted"));
1873
+ return new Promise((resolve, reject) => {
1874
+ let settled = false;
1875
+ const unsubscribe = this.onReadyState(capName, scope, (t) => {
1876
+ if (t.state !== "ready") return;
1877
+ if (settled) return;
1878
+ settled = true;
1879
+ cleanup();
1880
+ resolve();
1881
+ });
1882
+ const timer = !Number.isFinite(timeoutMs) ? null : setTimeout(() => {
1883
+ if (settled) return;
1884
+ settled = true;
1885
+ cleanup();
1886
+ reject(new ReadinessTimeoutError(capName, scope, this.now() - start));
1887
+ }, timeoutMs);
1888
+ const pendingLogTimer = setInterval(() => {
1889
+ if (settled) return;
1890
+ this.logger?.warn(`readiness: still awaiting ${capName} (${scopeKey(scope)}) after ${this.now() - start}ms`);
1891
+ }, 3e4);
1892
+ if (typeof pendingLogTimer.unref === "function") pendingLogTimer.unref();
1893
+ const onAbort = () => {
1894
+ if (settled) return;
1895
+ settled = true;
1896
+ cleanup();
1897
+ reject(opts.signal?.reason ?? /* @__PURE__ */ new Error("aborted"));
1898
+ };
1899
+ opts.signal?.addEventListener("abort", onAbort, { once: true });
1900
+ function cleanup() {
1901
+ unsubscribe();
1902
+ if (timer !== null) clearTimeout(timer);
1903
+ clearInterval(pendingLogTimer);
1904
+ opts.signal?.removeEventListener("abort", onAbort);
1905
+ }
1906
+ });
1907
+ }
1908
+ /**
1909
+ * Observable: every transition (starting/ready/down) dispatches to
1910
+ * `handler`. On subscription, if the snapshot has a current state,
1911
+ * the handler fires ONCE asynchronously with that state as the
1912
+ * initial transition (duration = 0) so late subscribers can pick up
1913
+ * current state without racing the next transition.
1914
+ *
1915
+ * Returns an unsubscribe function.
1916
+ */
1917
+ onReadyState(capName, scope, handler) {
1918
+ const sub = {
1919
+ capName,
1920
+ scope,
1921
+ handler
1922
+ };
1923
+ this.subscriptions.add(sub);
1924
+ const current = this.get(capName, scope);
1925
+ if (current !== null) queueMicrotask(() => {
1926
+ if (!this.subscriptions.has(sub)) return;
1927
+ handler({
1928
+ capName,
1929
+ scope,
1930
+ state: current.state,
1931
+ epoch: current.epoch,
1932
+ generation: current.generation,
1933
+ sourceNodeId: this.sourceNodeId,
1934
+ ts: current.lastChange,
1935
+ durationInPrevState: 0
1936
+ });
1937
+ });
1938
+ return () => {
1939
+ this.subscriptions.delete(sub);
1940
+ };
1941
+ }
1942
+ emitTransition(capName, scope, state) {
1943
+ const ts = this.now();
1944
+ this.bus.emit(createEvent("system.ready-state", {
1945
+ type: "capability",
1946
+ id: capName,
1947
+ nodeId: this.sourceNodeId
1948
+ }, {
1949
+ capName,
1950
+ scope,
1951
+ state,
1952
+ generation: this.generation,
1953
+ sourceNodeId: this.sourceNodeId,
1954
+ ts
1955
+ }));
1956
+ }
1957
+ /**
1958
+ * Update snapshot + dispatch to subscribers. Idempotent: same
1959
+ * `generation + state` replay is a no-op for both snapshot and
1960
+ * subscribers.
1961
+ *
1962
+ * Defensive against malformed payloads (legal reason: a mock bus's
1963
+ * `getRecent` may ignore the category filter and replay unrelated
1964
+ * events when we hydrate at construction time).
1965
+ */
1966
+ ingest(payload) {
1967
+ if (typeof payload?.capName !== "string") return;
1968
+ if (payload.state !== "ready" && payload.state !== "starting" && payload.state !== "down") return;
1969
+ if (typeof payload.generation !== "string") return;
1970
+ if (payload.scope?.type !== "global" && payload.scope?.type !== "node" && payload.scope?.type !== "device") return;
1971
+ const key = readinessKey(payload.capName, payload.scope);
1972
+ const prev = this.snapshot.get(key) ?? null;
1973
+ const now = this.now();
1974
+ let epoch;
1975
+ if (prev === null) epoch = payload.state === "ready" ? 1 : 0;
1976
+ else if (prev.generation !== payload.generation && payload.state === "ready") epoch = prev.epoch + 1;
1977
+ else epoch = prev.epoch;
1978
+ if (prev !== null && prev.generation === payload.generation && prev.state === payload.state) return;
1979
+ const durationInPrevState = prev === null ? 0 : Math.max(0, now - prev.lastChange);
1980
+ const next = {
1981
+ capName: payload.capName,
1982
+ scope: payload.scope,
1983
+ state: payload.state,
1984
+ generation: payload.generation,
1985
+ epoch,
1986
+ lastChange: now,
1987
+ sourceNodeId: payload.sourceNodeId
1988
+ };
1989
+ this.snapshot.set(key, next);
1990
+ const transition = {
1991
+ capName: payload.capName,
1992
+ scope: payload.scope,
1993
+ state: payload.state,
1994
+ epoch,
1995
+ generation: payload.generation,
1996
+ sourceNodeId: payload.sourceNodeId,
1997
+ ts: payload.ts,
1998
+ durationInPrevState
1999
+ };
2000
+ if (this.logger) this.logger.debug(`readiness: ${payload.capName} (${scopeKey(payload.scope)}) → ${payload.state} epoch=${epoch} gen=${payload.generation.slice(0, 6)} (prev ${durationInPrevState}ms)`);
2001
+ for (const sub of this.subscriptions) {
2002
+ if (sub.capName !== payload.capName) continue;
2003
+ if (!scopesEqual(sub.scope, payload.scope)) continue;
2004
+ try {
2005
+ sub.handler(transition);
2006
+ } catch (err) {
2007
+ this.logger?.warn(`readiness handler threw for ${payload.capName}: ${err.message ?? String(err)}`);
2008
+ }
2009
+ }
2010
+ }
2011
+ /**
2012
+ * `agent.offline` demux. When an agent disconnects ungracefully,
2013
+ * its BaseAddon's shutdown hook doesn't fire — consumers would
2014
+ * otherwise keep waiting on a stale `ready`. Walk the snapshot and
2015
+ * synthesize a `down` transition for every node-scoped record bound
2016
+ * to the disconnected `nodeId`, skipping records already `down`.
2017
+ *
2018
+ * Synthesis is LOCAL: we don't re-emit on the bus. Every registry
2019
+ * (one per process) receives the same `agent.offline` and reacts
2020
+ * independently, which means no duplicated downs and no central
2021
+ * supervisor required.
2022
+ */
2023
+ synthesizeDownForNode(offlineNodeId) {
2024
+ const now = this.now();
2025
+ for (const [key, record] of this.snapshot) {
2026
+ if (record.state === "down") continue;
2027
+ if (!(record.scope.type === "node" && record.scope.nodeId === offlineNodeId || record.scope.type === "device" && record.sourceNodeId === offlineNodeId)) continue;
2028
+ const durationInPrevState = Math.max(0, now - record.lastChange);
2029
+ const next = {
2030
+ ...record,
2031
+ state: "down",
2032
+ lastChange: now
2033
+ };
2034
+ this.snapshot.set(key, next);
2035
+ const transition = {
2036
+ capName: record.capName,
2037
+ scope: record.scope,
2038
+ state: "down",
2039
+ epoch: record.epoch,
2040
+ generation: record.generation,
2041
+ sourceNodeId: this.sourceNodeId,
2042
+ ts: now,
2043
+ durationInPrevState
2044
+ };
2045
+ if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → down (synthesized from agent.offline ${offlineNodeId})`);
2046
+ for (const sub of this.subscriptions) {
2047
+ if (sub.capName !== record.capName) continue;
2048
+ if (!scopesEqual(sub.scope, record.scope)) continue;
2049
+ try {
2050
+ sub.handler(transition);
2051
+ } catch (err) {
2052
+ this.logger?.warn(`readiness handler threw during agent.offline demux for ${record.capName}: ${err.message ?? String(err)}`);
2053
+ }
2054
+ }
2055
+ }
2056
+ }
2057
+ };
2058
+ function randomGeneration() {
2059
+ if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
2060
+ return Math.random().toString(36).slice(2, 14);
2061
+ }
2062
+ /**
2063
+ * Given a list of `(capName, scope)` pairs that a just-disconnected
2064
+ * node owned, emit a `down` transition for each. Callers (kernel
2065
+ * supervisor, hub `$node.disconnected` handler) wire this up once they
2066
+ * know which caps the node held.
2067
+ */
2068
+ function emitDownForOwnedCaps(registry, owned) {
2069
+ for (const { capName, scope } of owned) registry.emitDown(capName, scope);
2070
+ }
2071
+ //#endregion
2072
+ //#region src/interfaces/addon-data-plane.ts
2073
+ /**
2074
+ * Per-listener shared-secret header the hub injects on every reverse-proxied
2075
+ * data-plane request and the addon's framework wrapper validates — so only the
2076
+ * hub can reach the addon's `127.0.0.1` listener. Defined here (shared by the
2077
+ * core proxy and the kernel listener) to keep both off a kernel↔core import.
2078
+ */
2079
+ var DATAPLANE_SECRET_HEADER = "x-camstack-dataplane-secret";
2080
+ //#endregion
2081
+ //#region src/utils/json-safe.ts
2082
+ /**
2083
+ * Type-safe JSON parsing helpers.
2084
+ *
2085
+ * `JSON.parse` is typed as `any` in lib.es5.d.ts, which triggers
2086
+ * `no-unsafe-*` ESLint rules and destroys downstream inference. These
2087
+ * wrappers return `unknown` — callers narrow structurally via type
2088
+ * guards, `typeof` checks, or helpers like `asRecord`/`asString`.
2089
+ */
2090
+ /**
2091
+ * Parse JSON and return it as `unknown` — the only entry point for untrusted JSON.
2092
+ *
2093
+ * The optional generic overload `parseJsonUnknown<T>(text)` returns `T` for
2094
+ * call sites that know the shape at parse time (e.g. MQTT payloads with a
2095
+ * known protocol schema). This is a **type-level bridge only** — no runtime
2096
+ * validation is performed. Callers that need runtime validation should parse
2097
+ * as `unknown` and narrow via Zod or structural guards.
2098
+ */
2099
+ function parseJsonUnknown(text) {
2100
+ return JSON.parse(text);
2101
+ }
2102
+ /** Narrow an unknown value to a plain `Record<string, unknown>` or return null. */
2103
+ function asJsonObject(value) {
2104
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
2105
+ return { ...value };
2106
+ }
2107
+ /** Narrow an unknown value to a `readonly unknown[]` or return an empty array. */
2108
+ function asJsonArray(value) {
2109
+ return Array.isArray(value) ? value : [];
2110
+ }
2111
+ /** Safe string extraction from an unknown record field. */
2112
+ function asString(value, fallback = "") {
2113
+ return typeof value === "string" ? value : fallback;
2114
+ }
2115
+ /** Safe number extraction from an unknown record field. */
2116
+ function asNumber(value, fallback = 0) {
2117
+ return typeof value === "number" ? value : fallback;
2118
+ }
2119
+ /** Safe boolean extraction from an unknown record field. */
2120
+ function asBoolean(value, fallback = false) {
2121
+ return typeof value === "boolean" ? value : fallback;
2122
+ }
2123
+ /** Parse JSON + narrow to object in one step. */
2124
+ function parseJsonObject(text) {
2125
+ try {
2126
+ return asJsonObject(parseJsonUnknown(text));
2127
+ } catch {
2128
+ return null;
2129
+ }
2130
+ }
2131
+ /** Parse JSON + narrow to array in one step. */
2132
+ function parseJsonArray(text) {
2133
+ try {
2134
+ const parsed = parseJsonUnknown(text);
2135
+ return Array.isArray(parsed) ? parsed : null;
2136
+ } catch {
2137
+ return null;
2138
+ }
2139
+ }
2140
+ //#endregion
2141
+ //#region src/device/device-type.ts
2142
+ var DeviceType = /* @__PURE__ */ function(DeviceType) {
2143
+ DeviceType["Camera"] = "camera";
2144
+ DeviceType["Hub"] = "hub";
2145
+ DeviceType["Light"] = "light";
2146
+ DeviceType["Siren"] = "siren";
2147
+ DeviceType["Switch"] = "switch";
2148
+ DeviceType["Sensor"] = "sensor";
2149
+ DeviceType["Thermostat"] = "thermostat";
2150
+ DeviceType["Button"] = "button";
2151
+ /** Generic stateless event emitter — carries a device's EXACT declared
2152
+ * event vocabulary verbatim (no normalization). Installed with the
2153
+ * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
2154
+ * HA bus events (e.g. `zha_event`, generic). */
2155
+ DeviceType["EventEmitter"] = "event-emitter";
2156
+ /** Firmware/software update entity — current vs available version,
2157
+ * updatable flag, update state, and an install action. Installed with
2158
+ * the `update` cap. Sources: Homematic firmware-update channels (and
2159
+ * reusable by other providers, e.g. HA `update.*` entities). */
2160
+ DeviceType["Update"] = "update";
2161
+ DeviceType["Generic"] = "generic";
2162
+ /** Generic notification delivery target (HA `notify.<service>`, future
2163
+ * Telegram / Discord / ntfy / SMTP, …). One device per delivery
2164
+ * endpoint; the `notifier` cap defines the send surface. */
2165
+ DeviceType["Notifier"] = "notifier";
2166
+ /** Pre-recorded action sequence with optional parameters
2167
+ * (HA `script.*`). Runnable via `script-runner` cap. */
2168
+ DeviceType["Script"] = "script";
2169
+ /** Automation rule (HA `automation.*`) — enable/disable + manual
2170
+ * trigger surface exposed via `automation-control` cap. */
2171
+ DeviceType["Automation"] = "automation";
2172
+ /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
2173
+ DeviceType["Lock"] = "lock";
2174
+ /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
2175
+ * `valve.*`). `cover` cap with sub-roles for variant. */
2176
+ DeviceType["Cover"] = "cover";
2177
+ /** Pipe / water / gas valve with open/close/stop and optional
2178
+ * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
2179
+ * modelled on the same open/closed lifecycle. */
2180
+ DeviceType["Valve"] = "valve";
2181
+ /** Humidifier / dehumidifier with on/off + target humidity + mode
2182
+ * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
2183
+ * modelled on the same target / mode lifecycle. */
2184
+ DeviceType["Humidifier"] = "humidifier";
2185
+ /** Water heater / boiler with target temperature + operation mode +
2186
+ * away mode (HA `water_heater.*`). `water-heater` cap — a
2187
+ * climate-family actuator. */
2188
+ DeviceType["WaterHeater"] = "water-heater";
2189
+ /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
2190
+ DeviceType["Fan"] = "fan";
2191
+ /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
2192
+ * the camera surface — those use `Camera`. `media-player` cap. */
2193
+ DeviceType["MediaPlayer"] = "media-player";
2194
+ /** Security panel / alarm system (HA `alarm_control_panel.*`).
2195
+ * `alarm-panel` cap. */
2196
+ DeviceType["AlarmPanel"] = "alarm-panel";
2197
+ /** Generic user-settable input (HA `number` / `input_number` / `select`
2198
+ * / `input_select` / `text` / `input_text` / `input_datetime`).
2199
+ * Sub-type via `DeviceRole`: NumericControl / SelectControl /
2200
+ * TextControl / DateTimeControl. */
2201
+ DeviceType["Control"] = "control";
2202
+ /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
2203
+ * `presence` cap. */
2204
+ DeviceType["Presence"] = "presence";
2205
+ /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
2206
+ * `weather` cap. */
2207
+ DeviceType["Weather"] = "weather";
2208
+ /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
2209
+ DeviceType["Vacuum"] = "vacuum";
2210
+ /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
2211
+ * `lawn-mower-control` cap. */
2212
+ DeviceType["LawnMower"] = "lawn-mower";
2213
+ /** Physical HA device group — parent container for entity-children
2214
+ * adopted from a single HA device entry. Not renderable as a
2215
+ * standalone device; exists only to anchor child entities. */
2216
+ DeviceType["Container"] = "container";
2217
+ /** Single still-image entity (HA `image.*`). Read-only display of an
2218
+ * `entity_picture` signed URL the browser loads directly. `image` cap. */
2219
+ DeviceType["Image"] = "image";
2220
+ return DeviceType;
2221
+ }({});
2222
+ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
2223
+ DeviceFeature["BatteryOperated"] = "battery-operated";
2224
+ DeviceFeature["Rebootable"] = "rebootable";
2225
+ /**
2226
+ * Device supports an on-demand re-sync of its derived spec with its
2227
+ * upstream source — drives the generic Re-sync button. The owning
2228
+ * provider implements the action via the `device-adoption.resync` cap.
2229
+ */
2230
+ DeviceFeature["Resyncable"] = "resyncable";
2231
+ DeviceFeature["NativeSnapshot"] = "native-snapshot";
2232
+ DeviceFeature["DoorbellButton"] = "doorbell-button";
2233
+ DeviceFeature["TwoWayAudio"] = "two-way-audio";
2234
+ DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
2235
+ /**
2236
+ * Camera supports the on-firmware autotrack subsystem (subject-
2237
+ * following). Distinct from `PanTiltZoom` because not every PTZ
2238
+ * camera ships autotrack — the admin UI uses this flag to gate
2239
+ * the autotrack toggle / settings card without re-deriving from
2240
+ * the cap registry. Mirrors `ptz-autotrack` cap registration:
2241
+ * driver sets this feature when probe confirms the firmware
2242
+ * surface, and registers the cap in the same code path.
2243
+ */
2244
+ DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
2245
+ /**
2246
+ * Accessory exposes a "trigger on motion" toggle — the parent camera's
2247
+ * motion detection automatically activates this device. Mirrors
2248
+ * `motion-trigger` cap registration: drivers set this feature in the
2249
+ * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
2250
+ *
2251
+ * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
2252
+ * fast scalar without binding fetch), notifier rules, and `listAll`
2253
+ * filters that want "all devices with on-motion behaviour".
2254
+ */
2255
+ DeviceFeature["MotionTrigger"] = "motion-trigger";
2256
+ /** Light supports rgb-triplet color via `color` cap. */
2257
+ DeviceFeature["LightColorRgb"] = "light-color-rgb";
2258
+ /** Light supports HSV color via `color` cap. */
2259
+ DeviceFeature["LightColorHsv"] = "light-color-hsv";
2260
+ /** Light supports color-temperature (mired) via `color` cap. */
2261
+ DeviceFeature["LightColorMired"] = "light-color-mired";
2262
+ /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
2263
+ * targetHigh). Gates the range slider UI. */
2264
+ DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
2265
+ /** Thermostat exposes target humidity and/or current humidity
2266
+ * readings. Gates the humidity controls. */
2267
+ DeviceFeature["ClimateHumidity"] = "climate-humidity";
2268
+ /** Thermostat exposes a fan-mode selector. */
2269
+ DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
2270
+ /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
2271
+ DeviceFeature["ClimatePreset"] = "climate-preset";
2272
+ /** Cover exposes intermediate position control (0..100). Gates the
2273
+ * position slider UI. */
2274
+ DeviceFeature["CoverPositionable"] = "cover-positionable";
2275
+ /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
2276
+ DeviceFeature["CoverTilt"] = "cover-tilt";
2277
+ /** Valve exposes intermediate position control (0..100). Gates the
2278
+ * position slider / drag surface UI. */
2279
+ DeviceFeature["ValvePositionable"] = "valve-positionable";
2280
+ /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
2281
+ DeviceFeature["FanSpeed"] = "fan-speed";
2282
+ /** Fan exposes a preset mode selector. */
2283
+ DeviceFeature["FanPreset"] = "fan-preset";
2284
+ /** Fan exposes blade direction (forward/reverse) — typical of
2285
+ * ceiling fans. */
2286
+ DeviceFeature["FanDirection"] = "fan-direction";
2287
+ /** Fan exposes an oscillation toggle. */
2288
+ DeviceFeature["FanOscillating"] = "fan-oscillating";
2289
+ /** Lock requires a PIN code on lock/unlock. Gates the code-entry
2290
+ * field on the UI lock-controls panel. */
2291
+ DeviceFeature["LockPinRequired"] = "lock-pin-required";
2292
+ /** Lock supports a latch-release ("open door") action distinct from
2293
+ * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
2294
+ * `supported_features`. Gates the Open Door button in the UI. */
2295
+ DeviceFeature["LockOpen"] = "lock-open";
2296
+ /** Media player exposes a seek-to-position surface. */
2297
+ DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
2298
+ /** Media player exposes a volume-level setter. */
2299
+ DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
2300
+ /** Media player exposes a mute toggle distinct from volume=0. */
2301
+ DeviceFeature["MediaPlayerMute"] = "media-player-mute";
2302
+ /** Media player exposes a shuffle toggle. */
2303
+ DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
2304
+ /** Media player exposes a repeat mode (off / all / one). */
2305
+ DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
2306
+ /** Media player exposes a source / input selector. */
2307
+ DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
2308
+ /** Media player exposes a play-arbitrary-media surface (URL / id). */
2309
+ DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
2310
+ /** Media player exposes next-track. */
2311
+ DeviceFeature["MediaPlayerNext"] = "media-player-next";
2312
+ /** Media player exposes previous-track. */
2313
+ DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
2314
+ /** Media player exposes stop distinct from pause. */
2315
+ DeviceFeature["MediaPlayerStop"] = "media-player-stop";
2316
+ /** Alarm panel requires a PIN code on arm/disarm. */
2317
+ DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
2318
+ /** Presence device carries GPS coordinates (lat/lng/accuracy) in
2319
+ * addition to a textual location. */
2320
+ DeviceFeature["PresenceGps"] = "presence-gps";
2321
+ /** Notifier accepts an inline / URL image attachment. */
2322
+ DeviceFeature["NotifierImage"] = "notifier-image";
2323
+ /** Notifier accepts a priority hint (high/normal/low). */
2324
+ DeviceFeature["NotifierPriority"] = "notifier-priority";
2325
+ /** Notifier accepts a free-form `data` payload for platform-specific
2326
+ * fields. */
2327
+ DeviceFeature["NotifierData"] = "notifier-data";
2328
+ /** Notifier supports interactive action buttons / callbacks. */
2329
+ DeviceFeature["NotifierActions"] = "notifier-actions";
2330
+ /** Notifier supports per-call recipient targeting (multi-user). */
2331
+ DeviceFeature["NotifierRecipients"] = "notifier-recipients";
2332
+ /** Script runner accepts a variables map on each run invocation. */
2333
+ DeviceFeature["ScriptVariables"] = "script-variables";
2334
+ /** Automation `trigger` accepts a skipCondition flag — fires the
2335
+ * automation's actions while bypassing its condition block. */
2336
+ DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
2337
+ return DeviceFeature;
2338
+ }({});
2339
+ var ChargingStatus = /* @__PURE__ */ function(ChargingStatus) {
2340
+ ChargingStatus["ChargingDC"] = "charging-dc";
2341
+ ChargingStatus["ChargingSolar"] = "charging-solar";
2342
+ ChargingStatus["NotCharging"] = "not-charging";
2343
+ return ChargingStatus;
2344
+ }({});
2345
+ /**
2346
+ * Semantic role a device plays within its parent. Populated by driver
2347
+ * addons when creating accessory devices (Reolink siren/floodlight/
2348
+ * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
2349
+ * admin UI to pick icons, labels, and widgets — a `Switch` with
2350
+ * `role: Floodlight` renders as a bulb with a brightness slider,
2351
+ * whereas a `Switch` with `role: Siren` renders as a klaxon.
2352
+ *
2353
+ * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
2354
+ * sqlite as a nullable TEXT column — old rows keep working unchanged.
2355
+ */
2356
+ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
2357
+ DeviceRole["Siren"] = "siren";
2358
+ DeviceRole["Floodlight"] = "floodlight";
2359
+ DeviceRole["Spotlight"] = "spotlight";
2360
+ DeviceRole["PirSensor"] = "pir-sensor";
2361
+ DeviceRole["Chime"] = "chime";
2362
+ DeviceRole["Autotrack"] = "autotrack";
2363
+ DeviceRole["Nightvision"] = "nightvision";
2364
+ DeviceRole["PrivacyMask"] = "privacy-mask";
2365
+ DeviceRole["Doorbell"] = "doorbell";
2366
+ /** Virtual HA toggle (input_boolean.*) — distinguishable from a
2367
+ * real Switch device for UI rendering / export adapters. */
2368
+ DeviceRole["BinaryHelper"] = "binary-helper";
2369
+ /** Generic motion / occupancy / moving event source. Distinct from
2370
+ * the camera accessory PirSensor role: that one is a camera child;
2371
+ * this is a standalone HA / 3rd-party motion sensor. */
2372
+ DeviceRole["MotionSensor"] = "motion-sensor";
2373
+ DeviceRole["ContactSensor"] = "contact-sensor";
2374
+ DeviceRole["LeakSensor"] = "leak-sensor";
2375
+ DeviceRole["SmokeSensor"] = "smoke-sensor";
2376
+ DeviceRole["COSensor"] = "co-sensor";
2377
+ DeviceRole["GasSensor"] = "gas-sensor";
2378
+ DeviceRole["TamperSensor"] = "tamper-sensor";
2379
+ DeviceRole["VibrationSensor"] = "vibration-sensor";
2380
+ DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
2381
+ DeviceRole["SoundSensor"] = "sound-sensor";
2382
+ /** Fallback for `binary_sensor` without a known `device_class`. */
2383
+ DeviceRole["BinarySensor"] = "binary-sensor";
2384
+ DeviceRole["TemperatureSensor"] = "temperature-sensor";
2385
+ DeviceRole["HumiditySensor"] = "humidity-sensor";
2386
+ DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
2387
+ DeviceRole["PressureSensor"] = "pressure-sensor";
2388
+ DeviceRole["PowerSensor"] = "power-sensor";
2389
+ DeviceRole["EnergySensor"] = "energy-sensor";
2390
+ DeviceRole["VoltageSensor"] = "voltage-sensor";
2391
+ DeviceRole["CurrentSensor"] = "current-sensor";
2392
+ DeviceRole["AirQualitySensor"] = "air-quality-sensor";
2393
+ /** Battery level (numeric % via `sensor` OR low-bool via
2394
+ * `binary_sensor` — the cap distinguishes via the value type). */
2395
+ DeviceRole["BatterySensor"] = "battery-sensor";
2396
+ /** Fallback for `sensor` numeric without a known `device_class`. */
2397
+ DeviceRole["NumericSensor"] = "numeric-sensor";
2398
+ /** String / enum state (HA `sensor` with `state_class: enum` or
2399
+ * `attributes.options`). */
2400
+ DeviceRole["EnumSensor"] = "enum-sensor";
2401
+ /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
2402
+ * or `date`). The slice carries the raw ISO string verbatim (hosted on
2403
+ * the `enum-sensor` cap); the UI renders it locale-formatted. */
2404
+ DeviceRole["DateTimeSensor"] = "datetime-sensor";
2405
+ /** Last-resort fallback when nothing else matches. */
2406
+ DeviceRole["GenericSensor"] = "generic-sensor";
2407
+ DeviceRole["NumericControl"] = "numeric-control";
2408
+ DeviceRole["SelectControl"] = "select-control";
2409
+ DeviceRole["TextControl"] = "text-control";
2410
+ DeviceRole["DateTimeControl"] = "datetime-control";
2411
+ /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
2412
+ * rich features (image, priority, channel routing). */
2413
+ DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
2414
+ /** Chat / messaging service (HA `notify.telegram_*`,
2415
+ * `notify.discord_*`, etc.). */
2416
+ DeviceRole["MessagingNotifier"] = "messaging-notifier";
2417
+ /** Email-based delivery (HA `notify.smtp`, etc.). */
2418
+ DeviceRole["EmailNotifier"] = "email-notifier";
2419
+ /** Fallback when the notifier service name doesn't match a known
2420
+ * pattern. */
2421
+ DeviceRole["GenericNotifier"] = "generic-notifier";
2422
+ return DeviceRole;
2423
+ }({});
2424
+ //#endregion
2425
+ //#region src/capabilities/capability-definition.ts
2426
+ /**
2427
+ * Generic types for capability definitions.
2428
+ *
2429
+ * A capability is defined with Zod schemas for methods, events, and settings.
2430
+ * TypeScript types are inferred via z.infer<> — zero duplication.
2431
+ *
2432
+ * Pattern:
2433
+ * 1. Define Zod schemas for data, methods, settings
2434
+ * 2. Export const capabilityDef = { ... } satisfies CapabilityDefinition
2435
+ * 3. Export type IProvider = InferProvider<typeof capabilityDef>
2436
+ * 4. Addon implements IProvider
2437
+ * 5. Registry auto-mounts tRPC router from definition.methods
2438
+ */
2439
+ /**
2440
+ * Output schema shared by the contribution + live methods.
2441
+ *
2442
+ * Mirrors the `ConfigUISchemaWithValues` shape (sections[] + optional
2443
+ * tabs[]) without importing from `../interfaces/config-ui.js` — a
2444
+ * concrete-but-lenient Zod object keeps tRPC output inference happy
2445
+ * (using `z.unknown()` here collapses unrelated router branches to
2446
+ * `unknown` when the generator re-inlines the huge AppRouter type).
2447
+ *
2448
+ * `.passthrough()` on sections/fields accepts whatever FormBuilder
2449
+ * extensions the caller adds (showWhen, displayScale, …) without
2450
+ * rebuilding every time a new field kind is introduced.
2451
+ */
2452
+ var ContributionSectionSchema = z.object({
2453
+ id: z.string(),
2454
+ title: z.string(),
2455
+ description: z.string().optional(),
2456
+ style: z.enum(["card", "accordion"]).optional(),
2457
+ defaultCollapsed: z.boolean().optional(),
2458
+ columns: z.union([
2459
+ z.literal(1),
2460
+ z.literal(2),
2461
+ z.literal(3),
2462
+ z.literal(4)
2463
+ ]).optional(),
2464
+ tab: z.string().optional(),
2465
+ location: z.enum(["settings", "top-tab"]).optional(),
2466
+ order: z.number().optional(),
2467
+ fields: z.array(z.any())
2468
+ });
2469
+ var ContributionTabSchema = z.object({
2470
+ id: z.string(),
2471
+ label: z.string(),
2472
+ icon: z.string(),
2473
+ order: z.number().optional()
2474
+ });
2475
+ var ContributionOutputSchema = z.object({
2476
+ tabs: z.array(ContributionTabSchema).optional(),
2477
+ sections: z.array(ContributionSectionSchema)
2478
+ }).nullable();
2479
+ var DEVICE_SETTINGS_CONTRIBUTION_METHODS = {
2480
+ getDeviceSettingsContribution: {
2481
+ input: z.object({ deviceId: z.number() }),
2482
+ output: ContributionOutputSchema,
2483
+ kind: "query",
2484
+ auth: "protected"
2485
+ },
2486
+ getDeviceLiveContribution: {
2487
+ input: z.object({ deviceId: z.number() }),
2488
+ output: ContributionOutputSchema,
2489
+ kind: "query",
2490
+ auth: "protected"
2491
+ },
2492
+ applyDeviceSettingsPatch: {
2493
+ input: z.object({
2494
+ deviceId: z.number(),
2495
+ patch: z.record(z.string(), z.unknown())
2496
+ }),
2497
+ output: z.object({ success: z.literal(true) }),
2498
+ kind: "mutation",
2499
+ auth: "admin"
2500
+ }
2501
+ };
2502
+ /**
2503
+ * Schema for the `getStatus` method auto-injected when a cap declares
2504
+ * `status`. The runtime shape of the output is the cap's own
2505
+ * `status.schema` — but at codegen time we need a concrete Zod to emit
2506
+ * a typed tRPC route, so we keep the output as `z.unknown().nullable()`
2507
+ * here and tighten it on the client side via the generated
2508
+ * `CapStatusTypeMap` (see `scripts/generate-cap-status-types.ts`).
2509
+ */
2510
+ var DEVICE_STATUS_METHOD = { getStatus: {
2511
+ input: z.object({ deviceId: z.number() }),
2512
+ output: z.unknown().nullable(),
2513
+ kind: "query",
2514
+ auth: "protected"
2515
+ } };
2516
+ /**
2517
+ * Expand a cap def's methods map with every auto-injected method:
2518
+ * - `exposesDeviceSettings: true` → 3 contribution methods
2519
+ * - `status: {...}` → `getStatus`
2520
+ *
2521
+ * Callers walk `expandCapMethods(def)` instead of `def.methods` when
2522
+ * they need the effective runtime method surface (Moleculer actions,
2523
+ * proxy shape, tRPC router entries). The cap def stays the single
2524
+ * source of truth. Providers still declare their concrete `methods`
2525
+ * block; the expansion happens at every consumption point, so there's
2526
+ * no accidental divergence between the declared surface and what's
2527
+ * actually mounted.
2528
+ */
2529
+ function expandCapMethods(def) {
2530
+ let out = def.methods;
2531
+ if (def.exposesDeviceSettings) out = {
2532
+ ...DEVICE_SETTINGS_CONTRIBUTION_METHODS,
2533
+ ...out
2534
+ };
2535
+ if (def.status) out = {
2536
+ ...DEVICE_STATUS_METHOD,
2537
+ ...out
2538
+ };
2539
+ return out;
2540
+ }
2541
+ /** Shorthand to define a method schema */
2542
+ function method(input, output, options) {
2543
+ return {
2544
+ input,
2545
+ output,
2546
+ kind: options?.kind ?? "query",
2547
+ auth: options?.auth ?? "protected",
2548
+ ...options?.access !== void 0 ? { access: options.access } : {},
2549
+ timeoutMs: options?.timeoutMs
2550
+ };
2551
+ }
2552
+ /** Shorthand to define an event schema */
2553
+ function event(data) {
2554
+ return { data };
2555
+ }
2556
+ /** Type guard: does this cap declare the D14 device-config archetype? */
2557
+ function isDeviceConfigCap(def) {
2558
+ return def?.deviceConfig !== void 0;
2559
+ }
2560
+ //#endregion
2561
+ //#region src/device/device-state-handle.ts
2562
+ var DEVICE_STATE_EVENT_CATEGORY = "device.state-changed";
2563
+ /**
2564
+ * Lazy tRPC source — the default behavior. Maintains a per-key local
2565
+ * cache (`{deviceId}:{capName}` → last slice), one shared
2566
+ * `live.onEvent` bridge that fans out into the listener map, and
2567
+ * `refresh` round-trips through `deviceState.getCapSlice`.
2568
+ *
2569
+ * The bridge is opened on first `watch()` and closed when the last
2570
+ * watcher unsubscribes — keeps idle handles cheap.
2571
+ */
2572
+ function createLazyTrpcSource(api) {
2573
+ const cache = /* @__PURE__ */ new Map();
2574
+ const listeners = /* @__PURE__ */ new Map();
2575
+ let bridge = null;
2576
+ const keyOf = (deviceId, capName) => `${deviceId}:${capName}`;
2577
+ const ensureBridge = () => {
2578
+ if (bridge) return;
2579
+ if (!api.live?.onEvent) return;
2580
+ bridge = api.live.onEvent.subscribe({ category: DEVICE_STATE_EVENT_CATEGORY }, { onData: (evt) => {
2581
+ const data = evt.data;
2582
+ if (!data || typeof data.deviceId !== "number" || typeof data.capName !== "string") return;
2583
+ const k = keyOf(data.deviceId, data.capName);
2584
+ cache.set(k, data.slice);
2585
+ const set = listeners.get(k);
2586
+ if (!set) return;
2587
+ for (const cb of set) try {
2588
+ cb(data.slice);
2589
+ } catch {}
2590
+ } });
2591
+ };
2592
+ const closeBridgeIfIdle = () => {
2593
+ if (!bridge) return;
2594
+ if (listeners.size > 0) return;
2595
+ bridge.unsubscribe();
2596
+ bridge = null;
2597
+ };
2598
+ return {
2599
+ read(deviceId, capName) {
2600
+ return cache.get(keyOf(deviceId, capName));
2601
+ },
2602
+ async refresh(deviceId, capName) {
2603
+ const slice = await api.deviceState.getCapSlice.query({
2604
+ deviceId,
2605
+ capName
2606
+ });
2607
+ const k = keyOf(deviceId, capName);
2608
+ cache.set(k, slice ?? void 0);
2609
+ const set = listeners.get(k);
2610
+ if (set) for (const cb of set) try {
2611
+ cb(slice ?? void 0);
2612
+ } catch {}
2613
+ },
2614
+ watch(deviceId, capName, cb) {
2615
+ const k = keyOf(deviceId, capName);
2616
+ let set = listeners.get(k);
2617
+ if (!set) {
2618
+ set = /* @__PURE__ */ new Set();
2619
+ listeners.set(k, set);
2620
+ }
2621
+ set.add(cb);
2622
+ ensureBridge();
2623
+ return () => {
2624
+ set.delete(cb);
2625
+ if (set.size === 0) listeners.delete(k);
2626
+ closeBridgeIfIdle();
2627
+ };
2628
+ },
2629
+ async write(deviceId, capName, slice) {
2630
+ await api.deviceState.setCapSlice.mutate({
2631
+ deviceId,
2632
+ capName,
2633
+ slice
2634
+ });
2635
+ }
2636
+ };
2637
+ }
2638
+ /**
2639
+ * Mirror source — reads from a shared map populated by a
2640
+ * `SystemManager` warm-boot + push event handler. Refresh is a no-op
2641
+ * (the SystemManager owns the update loop). `watch()` registers in a
2642
+ * shared listener set — the SystemManager calls these from its single
2643
+ * `device.state-changed` subscription.
2644
+ *
2645
+ * The mirror map and listener map are passed in by reference so the
2646
+ * SystemManager can mutate both as events arrive.
2647
+ */
2648
+ function createMirrorSource(mirror, listeners, api) {
2649
+ const keyOf = (deviceId, capName) => `${deviceId}:${capName}`;
2650
+ return {
2651
+ read(deviceId, capName) {
2652
+ return mirror.get(deviceId)?.get(capName);
2653
+ },
2654
+ async refresh() {},
2655
+ watch(deviceId, capName, cb) {
2656
+ const k = keyOf(deviceId, capName);
2657
+ let set = listeners.get(k);
2658
+ if (!set) {
2659
+ set = /* @__PURE__ */ new Set();
2660
+ listeners.set(k, set);
2661
+ }
2662
+ set.add(cb);
2663
+ return () => {
2664
+ set.delete(cb);
2665
+ if (set.size === 0) listeners.delete(k);
2666
+ };
2667
+ },
2668
+ async write(deviceId, capName, slice) {
2669
+ if (!api) throw new Error("createMirrorSource: write requires an api reference — pass it as the third argument when constructing the source");
2670
+ await api.deviceState.setCapSlice.mutate({
2671
+ deviceId,
2672
+ capName,
2673
+ slice
2674
+ });
2675
+ }
2676
+ };
2677
+ }
2678
+ /**
2679
+ * Build a `SliceHandle<T>` bound to `(deviceId, capName)`. Caller
2680
+ * provides the type parameter — codegen passes
2681
+ * `InferRuntimeState<typeof <cap>>` so consumers see the cap's typed
2682
+ * shape:
2683
+ *
2684
+ * const dev = createDeviceProxy(api, binding)
2685
+ * dev.state.battery.value // BatteryStatus | undefined
2686
+ * dev.state.battery.value?.percentage
2687
+ *
2688
+ * Two-arg form (deprecated path, kept for spec compatibility):
2689
+ * passes the api directly, builds a per-handle lazy source. New
2690
+ * code should pre-build a single source and reuse it across handles
2691
+ * via the explicit `source` form.
2692
+ */
2693
+ function createSliceHandle(source, deviceId, capName) {
2694
+ const src = isSource(source) ? source : createLazyTrpcSource(source);
2695
+ return {
2696
+ get value() {
2697
+ return src.read(deviceId, capName);
2698
+ },
2699
+ refresh() {
2700
+ return src.refresh(deviceId, capName);
2701
+ },
2702
+ subscribe(cb) {
2703
+ const unwatch = src.watch(deviceId, capName, (slice) => {
2704
+ try {
2705
+ cb(slice);
2706
+ } catch {}
2707
+ });
2708
+ try {
2709
+ cb(src.read(deviceId, capName));
2710
+ } catch {}
2711
+ if (src.read(deviceId, capName) === void 0) src.refresh(deviceId, capName).catch(() => void 0);
2712
+ return unwatch;
2713
+ },
2714
+ async set(slice) {
2715
+ await src.write(deviceId, capName, slice);
2716
+ },
2717
+ async patch(partial) {
2718
+ const next = {
2719
+ ...src.read(deviceId, capName),
2720
+ ...partial
2721
+ };
2722
+ await src.write(deviceId, capName, next);
2723
+ }
2724
+ };
2725
+ }
2726
+ function isSource(x) {
2727
+ return typeof x.read === "function" && typeof x.watch === "function";
2728
+ }
2729
+ //#endregion
2730
+ //#region src/generated/device-proxy.ts
2731
+ /**
2732
+ * Build a DeviceProxy that pre-binds deviceId + nodeId on every method call
2733
+ * and dispatches through the existing cap-router tRPC procedures.
2734
+ *
2735
+ * Optional `opts.stateSource` lets a SystemManager pass a shared mirror
2736
+ * source so every device proxy reads from the same in-memory map and
2737
+ * shares the warm-boot/push-event update loop. When omitted, a per-proxy
2738
+ * lazy tRPC source is created — the path used by `ctx.fetchDevice` and
2739
+ * `BackendClient.fetchDevice` for one-off reads.
2740
+ *
2741
+ * The returned proxy's `binding` field is set to the input `binding`
2742
+ * by default — callers that want to expose a different value (e.g. the
2743
+ * SDK's `System` patches it from a shared cache) can overwrite the
2744
+ * field on the returned object.
2745
+ */
2746
+ function createDeviceProxy(api, binding, opts) {
2747
+ const typedApi = api;
2748
+ const stateSource = opts?.stateSource ?? createLazyTrpcSource(api);
2749
+ /** Merge `{deviceId, nodeId?}` into the caller-supplied input. */
2750
+ function mergeInput(input, nodeId) {
2751
+ const base = typeof input === "object" && input !== null ? input : {};
2752
+ return nodeId !== void 0 ? {
2753
+ ...base,
2754
+ deviceId: binding.deviceId,
2755
+ nodeId
2756
+ } : {
2757
+ ...base,
2758
+ deviceId: binding.deviceId
2759
+ };
2760
+ }
2761
+ /**
2762
+ * Invoke `api.<capProp>.<method>.{query|mutate|subscribe}(merged)`.
2763
+ * Returns `any` so the caller-side per-method declaration assigns
2764
+ * cleanly into the strongly-typed `InferDeviceProxyCap<...>` shape on
2765
+ * the `DeviceProxy` interface — the proxy's contract is enforced by
2766
+ * the interface, not the dispatch helper.
2767
+ */
2768
+ function callLeaf(capProp, method, kind, merged, push) {
2769
+ const leaf = typedApi[capProp]?.[method];
2770
+ if (!leaf) throw new Error(`DeviceProxy: api has no '${capProp}.${method}'`);
2771
+ const fn = leaf;
2772
+ if (kind === "mutation") return fn.mutate(merged);
2773
+ if (kind === "subscription") return fn.subscribe(merged, push);
2774
+ return fn.query(merged);
2775
+ }
2776
+ /**
2777
+ * Device-scoped cap dispatch. Looks up the binding entry for `capName`
2778
+ * to pin the call to the worker that owns the per-device provider; when
2779
+ * no entry exists (cluster-wide singletons like `zones` /
2780
+ * `zone-rules` / `audio-metrics` that don't register per-device
2781
+ * natives), nodeId is omitted and the cap-router's `resolveProvider`
2782
+ * falls through to the local provider — a Moleculer bridge proxy when
2783
+ * the actual singleton lives in a worker.
2784
+ */
2785
+ function dispatch(capName, capProp, method, kind, input, push) {
2786
+ return callLeaf(capProp, method, kind, mergeInput(input, binding.entries.find((e) => e.capName === capName)?.providerNodeId), push);
2787
+ }
2788
+ /**
2789
+ * System-cap dispatch. No binding gate — system caps are cluster-wide
2790
+ * singletons; the nodeId is left absent so caps that load-balance (e.g.
2791
+ * `pipeline-runner`) can resolve their own target node.
2792
+ */
2793
+ function dispatchSystem(capProp, method, kind, input, push) {
2794
+ return callLeaf(capProp, method, kind, mergeInput(input, void 0), push);
2795
+ }
2796
+ return {
2797
+ deviceId: binding.deviceId,
2798
+ binding,
2799
+ state: {
2800
+ airQualitySensor: createSliceHandle(stateSource, binding.deviceId, "air-quality-sensor"),
2801
+ alarmPanel: createSliceHandle(stateSource, binding.deviceId, "alarm-panel"),
2802
+ ambientLightSensor: createSliceHandle(stateSource, binding.deviceId, "ambient-light-sensor"),
2803
+ audioMetrics: createSliceHandle(stateSource, binding.deviceId, "audio-metrics"),
2804
+ automationControl: createSliceHandle(stateSource, binding.deviceId, "automation-control"),
2805
+ battery: createSliceHandle(stateSource, binding.deviceId, "battery"),
2806
+ binary: createSliceHandle(stateSource, binding.deviceId, "binary"),
2807
+ brightness: createSliceHandle(stateSource, binding.deviceId, "brightness"),
2808
+ cameraStreams: createSliceHandle(stateSource, binding.deviceId, "camera-streams"),
2809
+ carbonMonoxide: createSliceHandle(stateSource, binding.deviceId, "carbon-monoxide"),
2810
+ climateControl: createSliceHandle(stateSource, binding.deviceId, "climate-control"),
2811
+ color: createSliceHandle(stateSource, binding.deviceId, "color"),
2812
+ connectivity: createSliceHandle(stateSource, binding.deviceId, "connectivity"),
2813
+ consumables: createSliceHandle(stateSource, binding.deviceId, "consumables"),
2814
+ contact: createSliceHandle(stateSource, binding.deviceId, "contact"),
2815
+ control: createSliceHandle(stateSource, binding.deviceId, "control"),
2816
+ cover: createSliceHandle(stateSource, binding.deviceId, "cover"),
2817
+ deviceDiscovery: createSliceHandle(stateSource, binding.deviceId, "device-discovery"),
2818
+ deviceStatus: createSliceHandle(stateSource, binding.deviceId, "device-status"),
2819
+ doorbell: createSliceHandle(stateSource, binding.deviceId, "doorbell"),
2820
+ enumSensor: createSliceHandle(stateSource, binding.deviceId, "enum-sensor"),
2821
+ eventEmitter: createSliceHandle(stateSource, binding.deviceId, "event-emitter"),
2822
+ fanControl: createSliceHandle(stateSource, binding.deviceId, "fan-control"),
2823
+ featureProbe: createSliceHandle(stateSource, binding.deviceId, "feature-probe"),
2824
+ flood: createSliceHandle(stateSource, binding.deviceId, "flood"),
2825
+ gas: createSliceHandle(stateSource, binding.deviceId, "gas"),
2826
+ humidifier: createSliceHandle(stateSource, binding.deviceId, "humidifier"),
2827
+ humiditySensor: createSliceHandle(stateSource, binding.deviceId, "humidity-sensor"),
2828
+ image: createSliceHandle(stateSource, binding.deviceId, "image"),
2829
+ lawnMowerControl: createSliceHandle(stateSource, binding.deviceId, "lawn-mower-control"),
2830
+ lockControl: createSliceHandle(stateSource, binding.deviceId, "lock-control"),
2831
+ mediaPlayer: createSliceHandle(stateSource, binding.deviceId, "media-player"),
2832
+ motion: createSliceHandle(stateSource, binding.deviceId, "motion"),
2833
+ motionTrigger: createSliceHandle(stateSource, binding.deviceId, "motion-trigger"),
2834
+ motionZones: createSliceHandle(stateSource, binding.deviceId, "motion-zones"),
2835
+ nativeObjectDetection: createSliceHandle(stateSource, binding.deviceId, "native-object-detection"),
2836
+ notifier: createSliceHandle(stateSource, binding.deviceId, "notifier"),
2837
+ numericSensor: createSliceHandle(stateSource, binding.deviceId, "numeric-sensor"),
2838
+ powerMeter: createSliceHandle(stateSource, binding.deviceId, "power-meter"),
2839
+ presence: createSliceHandle(stateSource, binding.deviceId, "presence"),
2840
+ pressureSensor: createSliceHandle(stateSource, binding.deviceId, "pressure-sensor"),
2841
+ privacyMask: createSliceHandle(stateSource, binding.deviceId, "privacy-mask"),
2842
+ ptzAutotrack: createSliceHandle(stateSource, binding.deviceId, "ptz-autotrack"),
2843
+ scriptRunner: createSliceHandle(stateSource, binding.deviceId, "script-runner"),
2844
+ smoke: createSliceHandle(stateSource, binding.deviceId, "smoke"),
2845
+ streamParams: createSliceHandle(stateSource, binding.deviceId, "stream-params"),
2846
+ switch: createSliceHandle(stateSource, binding.deviceId, "switch"),
2847
+ tamper: createSliceHandle(stateSource, binding.deviceId, "tamper"),
2848
+ temperatureSensor: createSliceHandle(stateSource, binding.deviceId, "temperature-sensor"),
2849
+ update: createSliceHandle(stateSource, binding.deviceId, "update"),
2850
+ vacuumControl: createSliceHandle(stateSource, binding.deviceId, "vacuum-control"),
2851
+ valve: createSliceHandle(stateSource, binding.deviceId, "valve"),
2852
+ vibration: createSliceHandle(stateSource, binding.deviceId, "vibration"),
2853
+ waterHeater: createSliceHandle(stateSource, binding.deviceId, "water-heater"),
2854
+ weather: createSliceHandle(stateSource, binding.deviceId, "weather"),
2855
+ zoneAnalytics: createSliceHandle(stateSource, binding.deviceId, "zone-analytics"),
2856
+ zoneRules: createSliceHandle(stateSource, binding.deviceId, "zone-rules"),
2857
+ zones: createSliceHandle(stateSource, binding.deviceId, "zones")
2858
+ },
2859
+ accessories: {
2860
+ setChildHidden: (input) => dispatch("accessories", "accessories", "setChildHidden", "mutation", input),
2861
+ getStatus: (input) => dispatch("accessories", "accessories", "getStatus", "query", input)
2862
+ },
2863
+ airQualitySensor: { getStatus: (input) => dispatch("air-quality-sensor", "airQualitySensor", "getStatus", "query", input) },
2864
+ alarmPanel: {
2865
+ arm: (input) => dispatch("alarm-panel", "alarmPanel", "arm", "mutation", input),
2866
+ disarm: (input) => dispatch("alarm-panel", "alarmPanel", "disarm", "mutation", input),
2867
+ trigger: (input) => dispatch("alarm-panel", "alarmPanel", "trigger", "mutation", input),
2868
+ getStatus: (input) => dispatch("alarm-panel", "alarmPanel", "getStatus", "query", input)
2869
+ },
2870
+ ambientLightSensor: { getStatus: (input) => dispatch("ambient-light-sensor", "ambientLightSensor", "getStatus", "query", input) },
2871
+ audioAnalysis: {
2872
+ resolveDeviceSettings: (input) => dispatch("audio-analysis", "audioAnalysis", "resolveDeviceSettings", "query", input),
2873
+ getDeviceSettingsContribution: (input) => dispatch("audio-analysis", "audioAnalysis", "getDeviceSettingsContribution", "query", input),
2874
+ getDeviceLiveContribution: (input) => dispatch("audio-analysis", "audioAnalysis", "getDeviceLiveContribution", "query", input),
2875
+ applyDeviceSettingsPatch: (input) => dispatch("audio-analysis", "audioAnalysis", "applyDeviceSettingsPatch", "mutation", input)
2876
+ },
2877
+ audioMetrics: {
2878
+ getCurrentSnapshot: (input) => dispatch("audio-metrics", "audioMetrics", "getCurrentSnapshot", "query", input),
2879
+ getHistory: (input) => dispatch("audio-metrics", "audioMetrics", "getHistory", "query", input)
2880
+ },
2881
+ automationControl: {
2882
+ enable: (input) => dispatch("automation-control", "automationControl", "enable", "mutation", input),
2883
+ disable: (input) => dispatch("automation-control", "automationControl", "disable", "mutation", input),
2884
+ trigger: (input) => dispatch("automation-control", "automationControl", "trigger", "mutation", input),
2885
+ getStatus: (input) => dispatch("automation-control", "automationControl", "getStatus", "query", input)
2886
+ },
2887
+ battery: {
2888
+ wakeForStream: (input) => dispatch("battery", "battery", "wakeForStream", "mutation", input),
2889
+ getStatus: (input) => dispatch("battery", "battery", "getStatus", "query", input)
2890
+ },
2891
+ binary: { getStatus: (input) => dispatch("binary", "binary", "getStatus", "query", input) },
2892
+ brightness: {
2893
+ setBrightness: (input) => dispatch("brightness", "brightness", "setBrightness", "mutation", input),
2894
+ getStatus: (input) => dispatch("brightness", "brightness", "getStatus", "query", input)
2895
+ },
2896
+ button: { press: (input) => dispatch("button", "button", "press", "mutation", input) },
2897
+ cameraCredentials: {
2898
+ getCredentials: (input) => dispatch("camera-credentials", "cameraCredentials", "getCredentials", "query", input),
2899
+ getStatus: (input) => dispatch("camera-credentials", "cameraCredentials", "getStatus", "query", input)
2900
+ },
2901
+ cameraStreams: {
2902
+ getCameraStreams: (input) => dispatch("camera-streams", "cameraStreams", "getCameraStreams", "query", input),
2903
+ getBrokerStreams: (input) => dispatch("camera-streams", "cameraStreams", "getBrokerStreams", "query", input),
2904
+ getRtspEntries: (input) => dispatch("camera-streams", "cameraStreams", "getRtspEntries", "query", input),
2905
+ getProfileRtspEntries: (input) => dispatch("camera-streams", "cameraStreams", "getProfileRtspEntries", "query", input),
2906
+ pickStream: (input) => dispatch("camera-streams", "cameraStreams", "pickStream", "query", input)
2907
+ },
2908
+ carbonMonoxide: { getStatus: (input) => dispatch("carbon-monoxide", "carbonMonoxide", "getStatus", "query", input) },
2909
+ climateControl: {
2910
+ setMode: (input) => dispatch("climate-control", "climateControl", "setMode", "mutation", input),
2911
+ setFanMode: (input) => dispatch("climate-control", "climateControl", "setFanMode", "mutation", input),
2912
+ setPreset: (input) => dispatch("climate-control", "climateControl", "setPreset", "mutation", input),
2913
+ setTarget: (input) => dispatch("climate-control", "climateControl", "setTarget", "mutation", input),
2914
+ setTargetRange: (input) => dispatch("climate-control", "climateControl", "setTargetRange", "mutation", input),
2915
+ setTargetHumidity: (input) => dispatch("climate-control", "climateControl", "setTargetHumidity", "mutation", input),
2916
+ getStatus: (input) => dispatch("climate-control", "climateControl", "getStatus", "query", input)
2917
+ },
2918
+ color: {
2919
+ setColor: (input) => dispatch("color", "color", "setColor", "mutation", input),
2920
+ getStatus: (input) => dispatch("color", "color", "getStatus", "query", input)
2921
+ },
2922
+ connectivity: { getStatus: (input) => dispatch("connectivity", "connectivity", "getStatus", "query", input) },
2923
+ consumables: {
2924
+ reset: (input) => dispatch("consumables", "consumables", "reset", "mutation", input),
2925
+ getStatus: (input) => dispatch("consumables", "consumables", "getStatus", "query", input)
2926
+ },
2927
+ contact: { getStatus: (input) => dispatch("contact", "contact", "getStatus", "query", input) },
2928
+ control: {
2929
+ setValue: (input) => dispatch("control", "control", "setValue", "mutation", input),
2930
+ getStatus: (input) => dispatch("control", "control", "getStatus", "query", input)
2931
+ },
2932
+ cover: {
2933
+ open: (input) => dispatch("cover", "cover", "open", "mutation", input),
2934
+ close: (input) => dispatch("cover", "cover", "close", "mutation", input),
2935
+ stop: (input) => dispatch("cover", "cover", "stop", "mutation", input),
2936
+ setPosition: (input) => dispatch("cover", "cover", "setPosition", "mutation", input),
2937
+ setTiltPosition: (input) => dispatch("cover", "cover", "setTiltPosition", "mutation", input),
2938
+ getStatus: (input) => dispatch("cover", "cover", "getStatus", "query", input)
2939
+ },
2940
+ detectionPipeline: {
2941
+ getDeviceSettingsContribution: (input) => dispatch("detection-pipeline", "detectionPipeline", "getDeviceSettingsContribution", "query", input),
2942
+ getDeviceLiveContribution: (input) => dispatch("detection-pipeline", "detectionPipeline", "getDeviceLiveContribution", "query", input),
2943
+ applyDeviceSettingsPatch: (input) => dispatch("detection-pipeline", "detectionPipeline", "applyDeviceSettingsPatch", "mutation", input)
2944
+ },
2945
+ deviceDiscovery: {
2946
+ listDiscovered: (input) => dispatch("device-discovery", "deviceDiscovery", "listDiscovered", "query", input),
2947
+ refreshDiscovery: (input) => dispatch("device-discovery", "deviceDiscovery", "refreshDiscovery", "mutation", input),
2948
+ adoptDevice: (input) => dispatch("device-discovery", "deviceDiscovery", "adoptDevice", "mutation", input),
2949
+ releaseDevice: (input) => dispatch("device-discovery", "deviceDiscovery", "releaseDevice", "mutation", input),
2950
+ getStatus: (input) => dispatch("device-discovery", "deviceDiscovery", "getStatus", "query", input)
2951
+ },
2952
+ deviceOps: {
2953
+ getStreamSources: (input) => dispatch("device-ops", "deviceOps", "getStreamSources", "query", input),
2954
+ getConfigEntries: (input) => dispatch("device-ops", "deviceOps", "getConfigEntries", "query", input),
2955
+ setConfig: (input) => dispatch("device-ops", "deviceOps", "setConfig", "mutation", input),
2956
+ runAction: (input) => dispatch("device-ops", "deviceOps", "runAction", "mutation", input),
2957
+ removeDevice: (input) => dispatch("device-ops", "deviceOps", "removeDevice", "mutation", input),
2958
+ getSettingsSchema: (input) => dispatch("device-ops", "deviceOps", "getSettingsSchema", "query", input),
2959
+ getRawState: (input) => dispatch("device-ops", "deviceOps", "getRawState", "query", input)
2960
+ },
2961
+ deviceStatus: { getStatus: (input) => dispatch("device-status", "deviceStatus", "getStatus", "query", input) },
2962
+ doorbell: { getStatus: (input) => dispatch("doorbell", "doorbell", "getStatus", "query", input) },
2963
+ enumSensor: { getStatus: (input) => dispatch("enum-sensor", "enumSensor", "getStatus", "query", input) },
2964
+ eventEmitter: { getStatus: (input) => dispatch("event-emitter", "eventEmitter", "getStatus", "query", input) },
2965
+ events: {
2966
+ getEvents: (input) => dispatch("events", "events", "getEvents", "query", input),
2967
+ getEventThumbnail: (input) => dispatch("events", "events", "getEventThumbnail", "query", input),
2968
+ getEventClipUrl: (input) => dispatch("events", "events", "getEventClipUrl", "query", input)
2969
+ },
2970
+ fanControl: {
2971
+ setPercentage: (input) => dispatch("fan-control", "fanControl", "setPercentage", "mutation", input),
2972
+ setPreset: (input) => dispatch("fan-control", "fanControl", "setPreset", "mutation", input),
2973
+ setDirection: (input) => dispatch("fan-control", "fanControl", "setDirection", "mutation", input),
2974
+ setOscillating: (input) => dispatch("fan-control", "fanControl", "setOscillating", "mutation", input),
2975
+ getStatus: (input) => dispatch("fan-control", "fanControl", "getStatus", "query", input)
2976
+ },
2977
+ featureProbe: { getStatus: (input) => dispatch("feature-probe", "featureProbe", "getStatus", "query", input) },
2978
+ flood: { getStatus: (input) => dispatch("flood", "flood", "getStatus", "query", input) },
2979
+ gas: { getStatus: (input) => dispatch("gas", "gas", "getStatus", "query", input) },
2980
+ humidifier: {
2981
+ setOn: (input) => dispatch("humidifier", "humidifier", "setOn", "mutation", input),
2982
+ setTargetHumidity: (input) => dispatch("humidifier", "humidifier", "setTargetHumidity", "mutation", input),
2983
+ setMode: (input) => dispatch("humidifier", "humidifier", "setMode", "mutation", input),
2984
+ getStatus: (input) => dispatch("humidifier", "humidifier", "getStatus", "query", input)
2985
+ },
2986
+ humiditySensor: { getStatus: (input) => dispatch("humidity-sensor", "humiditySensor", "getStatus", "query", input) },
2987
+ image: { getStatus: (input) => dispatch("image", "image", "getStatus", "query", input) },
2988
+ intercom: {
2989
+ startSession: (input) => dispatch("intercom", "intercom", "startSession", "mutation", input),
2990
+ handleAnswer: (input) => dispatch("intercom", "intercom", "handleAnswer", "mutation", input),
2991
+ stopSession: (input) => dispatch("intercom", "intercom", "stopSession", "mutation", input),
2992
+ startTalkSession: (input) => dispatch("intercom", "intercom", "startTalkSession", "mutation", input),
2993
+ pushTalkAudio: (input) => dispatch("intercom", "intercom", "pushTalkAudio", "mutation", input),
2994
+ endTalkSession: (input) => dispatch("intercom", "intercom", "endTalkSession", "mutation", input),
2995
+ getStatus: (input) => dispatch("intercom", "intercom", "getStatus", "query", input)
2996
+ },
2997
+ lawnMowerControl: {
2998
+ startMowing: (input) => dispatch("lawn-mower-control", "lawnMowerControl", "startMowing", "mutation", input),
2999
+ pause: (input) => dispatch("lawn-mower-control", "lawnMowerControl", "pause", "mutation", input),
3000
+ dock: (input) => dispatch("lawn-mower-control", "lawnMowerControl", "dock", "mutation", input),
3001
+ getStatus: (input) => dispatch("lawn-mower-control", "lawnMowerControl", "getStatus", "query", input)
3002
+ },
3003
+ lockControl: {
3004
+ lock: (input) => dispatch("lock-control", "lockControl", "lock", "mutation", input),
3005
+ unlock: (input) => dispatch("lock-control", "lockControl", "unlock", "mutation", input),
3006
+ open: (input) => dispatch("lock-control", "lockControl", "open", "mutation", input),
3007
+ getStatus: (input) => dispatch("lock-control", "lockControl", "getStatus", "query", input)
3008
+ },
3009
+ mediaPlayer: {
3010
+ play: (input) => dispatch("media-player", "mediaPlayer", "play", "mutation", input),
3011
+ pause: (input) => dispatch("media-player", "mediaPlayer", "pause", "mutation", input),
3012
+ stop: (input) => dispatch("media-player", "mediaPlayer", "stop", "mutation", input),
3013
+ next: (input) => dispatch("media-player", "mediaPlayer", "next", "mutation", input),
3014
+ previous: (input) => dispatch("media-player", "mediaPlayer", "previous", "mutation", input),
3015
+ seek: (input) => dispatch("media-player", "mediaPlayer", "seek", "mutation", input),
3016
+ setVolume: (input) => dispatch("media-player", "mediaPlayer", "setVolume", "mutation", input),
3017
+ setMute: (input) => dispatch("media-player", "mediaPlayer", "setMute", "mutation", input),
3018
+ setShuffle: (input) => dispatch("media-player", "mediaPlayer", "setShuffle", "mutation", input),
3019
+ setRepeat: (input) => dispatch("media-player", "mediaPlayer", "setRepeat", "mutation", input),
3020
+ selectSource: (input) => dispatch("media-player", "mediaPlayer", "selectSource", "mutation", input),
3021
+ playMedia: (input) => dispatch("media-player", "mediaPlayer", "playMedia", "mutation", input),
3022
+ getStatus: (input) => dispatch("media-player", "mediaPlayer", "getStatus", "query", input)
3023
+ },
3024
+ motion: {
3025
+ isDetected: (input) => dispatch("motion", "motion", "isDetected", "query", input),
3026
+ getStatus: (input) => dispatch("motion", "motion", "getStatus", "query", input)
3027
+ },
3028
+ motionDetection: {
3029
+ analyze: (input) => dispatch("motion-detection", "motionDetection", "analyze", "mutation", input),
3030
+ removeCamera: (input) => dispatch("motion-detection", "motionDetection", "removeCamera", "mutation", input),
3031
+ reset: (input) => dispatch("motion-detection", "motionDetection", "reset", "mutation", input),
3032
+ getDeviceSettingsContribution: (input) => dispatch("motion-detection", "motionDetection", "getDeviceSettingsContribution", "query", input),
3033
+ getDeviceLiveContribution: (input) => dispatch("motion-detection", "motionDetection", "getDeviceLiveContribution", "query", input),
3034
+ applyDeviceSettingsPatch: (input) => dispatch("motion-detection", "motionDetection", "applyDeviceSettingsPatch", "mutation", input)
3035
+ },
3036
+ motionTrigger: {
3037
+ setMotionTrigger: (input) => dispatch("motion-trigger", "motionTrigger", "setMotionTrigger", "mutation", input),
3038
+ getStatus: (input) => dispatch("motion-trigger", "motionTrigger", "getStatus", "query", input)
3039
+ },
3040
+ motionZones: {
3041
+ getOptions: (input) => dispatch("motion-zones", "motionZones", "getOptions", "query", input),
3042
+ setZone: (input) => dispatch("motion-zones", "motionZones", "setZone", "mutation", input),
3043
+ getStatus: (input) => dispatch("motion-zones", "motionZones", "getStatus", "query", input)
3044
+ },
3045
+ nativeObjectDetection: {
3046
+ setEnabled: (input) => dispatch("native-object-detection", "nativeObjectDetection", "setEnabled", "mutation", input),
3047
+ getStatus: (input) => dispatch("native-object-detection", "nativeObjectDetection", "getStatus", "query", input)
3048
+ },
3049
+ notifier: {
3050
+ send: (input) => dispatch("notifier", "notifier", "send", "mutation", input),
3051
+ cancel: (input) => dispatch("notifier", "notifier", "cancel", "mutation", input),
3052
+ getStatus: (input) => dispatch("notifier", "notifier", "getStatus", "query", input)
3053
+ },
3054
+ numericSensor: { getStatus: (input) => dispatch("numeric-sensor", "numericSensor", "getStatus", "query", input) },
3055
+ osd: {
3056
+ setOverlay: (input) => dispatch("osd", "osd", "setOverlay", "mutation", input),
3057
+ getStatus: (input) => dispatch("osd", "osd", "getStatus", "query", input)
3058
+ },
3059
+ pipelineAnalytics: {
3060
+ getActiveTracks: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getActiveTracks", "query", input),
3061
+ getTrack: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrack", "query", input),
3062
+ listTracks: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listTracks", "query", input),
3063
+ clearTracks: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "clearTracks", "mutation", input),
3064
+ getMotionEvents: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getMotionEvents", "query", input),
3065
+ getObjectEvents: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getObjectEvents", "query", input),
3066
+ getAudioEvents: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getAudioEvents", "query", input),
3067
+ getEventDensity: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getEventDensity", "query", input),
3068
+ pruneEventsBefore: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "pruneEventsBefore", "mutation", input),
3069
+ getEventMedia: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getEventMedia", "query", input),
3070
+ getTrackMedia: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrackMedia", "query", input),
3071
+ getDeviceSettingsContribution: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getDeviceSettingsContribution", "query", input),
3072
+ getDeviceLiveContribution: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getDeviceLiveContribution", "query", input),
3073
+ applyDeviceSettingsPatch: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "applyDeviceSettingsPatch", "mutation", input)
3074
+ },
3075
+ powerMeter: { getStatus: (input) => dispatch("power-meter", "powerMeter", "getStatus", "query", input) },
3076
+ presence: { getStatus: (input) => dispatch("presence", "presence", "getStatus", "query", input) },
3077
+ pressureSensor: { getStatus: (input) => dispatch("pressure-sensor", "pressureSensor", "getStatus", "query", input) },
3078
+ privacyMask: {
3079
+ getOptions: (input) => dispatch("privacy-mask", "privacyMask", "getOptions", "query", input),
3080
+ setMask: (input) => dispatch("privacy-mask", "privacyMask", "setMask", "mutation", input),
3081
+ getStatus: (input) => dispatch("privacy-mask", "privacyMask", "getStatus", "query", input)
3082
+ },
3083
+ ptz: {
3084
+ move: (input) => dispatch("ptz", "ptz", "move", "mutation", input),
3085
+ continuousMove: (input) => dispatch("ptz", "ptz", "continuousMove", "mutation", input),
3086
+ stop: (input) => dispatch("ptz", "ptz", "stop", "mutation", input),
3087
+ getPresets: (input) => dispatch("ptz", "ptz", "getPresets", "query", input),
3088
+ goToPreset: (input) => dispatch("ptz", "ptz", "goToPreset", "mutation", input),
3089
+ savePreset: (input) => dispatch("ptz", "ptz", "savePreset", "mutation", input),
3090
+ deletePreset: (input) => dispatch("ptz", "ptz", "deletePreset", "mutation", input),
3091
+ getOptions: (input) => dispatch("ptz", "ptz", "getOptions", "query", input),
3092
+ goHome: (input) => dispatch("ptz", "ptz", "goHome", "mutation", input),
3093
+ getPosition: (input) => dispatch("ptz", "ptz", "getPosition", "query", input),
3094
+ setAutofocus: (input) => dispatch("ptz", "ptz", "setAutofocus", "mutation", input),
3095
+ getStatus: (input) => dispatch("ptz", "ptz", "getStatus", "query", input)
3096
+ },
3097
+ ptzAutotrack: {
3098
+ getStatus: (input) => dispatch("ptz-autotrack", "ptzAutotrack", "getStatus", "query", input),
3099
+ setEnabled: (input) => dispatch("ptz-autotrack", "ptzAutotrack", "setEnabled", "mutation", input),
3100
+ getSettings: (input) => dispatch("ptz-autotrack", "ptzAutotrack", "getSettings", "query", input),
3101
+ setSettings: (input) => dispatch("ptz-autotrack", "ptzAutotrack", "setSettings", "mutation", input)
3102
+ },
3103
+ reboot: { reboot: (input) => dispatch("reboot", "reboot", "reboot", "mutation", input) },
3104
+ scriptRunner: {
3105
+ run: (input) => dispatch("script-runner", "scriptRunner", "run", "mutation", input),
3106
+ stop: (input) => dispatch("script-runner", "scriptRunner", "stop", "mutation", input),
3107
+ getStatus: (input) => dispatch("script-runner", "scriptRunner", "getStatus", "query", input)
3108
+ },
3109
+ smoke: { getStatus: (input) => dispatch("smoke", "smoke", "getStatus", "query", input) },
3110
+ snapshot: {
3111
+ getSnapshot: (input) => dispatch("snapshot", "snapshot", "getSnapshot", "query", input),
3112
+ invalidateCache: (input) => dispatch("snapshot", "snapshot", "invalidateCache", "mutation", input),
3113
+ getStatus: (input) => dispatch("snapshot", "snapshot", "getStatus", "query", input),
3114
+ getDeviceSettingsContribution: (input) => dispatch("snapshot", "snapshot", "getDeviceSettingsContribution", "query", input),
3115
+ getDeviceLiveContribution: (input) => dispatch("snapshot", "snapshot", "getDeviceLiveContribution", "query", input),
3116
+ applyDeviceSettingsPatch: (input) => dispatch("snapshot", "snapshot", "applyDeviceSettingsPatch", "mutation", input)
3117
+ },
3118
+ streamCatalog: { getCatalog: (input) => dispatch("stream-catalog", "streamCatalog", "getCatalog", "query", input) },
3119
+ streamParams: {
3120
+ getOptions: (input) => dispatch("stream-params", "streamParams", "getOptions", "query", input),
3121
+ setProfile: (input) => dispatch("stream-params", "streamParams", "setProfile", "mutation", input),
3122
+ getConfigSchema: (input) => dispatch("stream-params", "streamParams", "getConfigSchema", "query", input),
3123
+ getStatus: (input) => dispatch("stream-params", "streamParams", "getStatus", "query", input)
3124
+ },
3125
+ switch: {
3126
+ setState: (input) => dispatch("switch", "switch", "setState", "mutation", input),
3127
+ getStatus: (input) => dispatch("switch", "switch", "getStatus", "query", input)
3128
+ },
3129
+ tamper: { getStatus: (input) => dispatch("tamper", "tamper", "getStatus", "query", input) },
3130
+ temperatureSensor: { getStatus: (input) => dispatch("temperature-sensor", "temperatureSensor", "getStatus", "query", input) },
3131
+ update: {
3132
+ installUpdate: (input) => dispatch("update", "update", "installUpdate", "mutation", input),
3133
+ getStatus: (input) => dispatch("update", "update", "getStatus", "query", input)
3134
+ },
3135
+ vacuumControl: {
3136
+ start: (input) => dispatch("vacuum-control", "vacuumControl", "start", "mutation", input),
3137
+ pause: (input) => dispatch("vacuum-control", "vacuumControl", "pause", "mutation", input),
3138
+ stop: (input) => dispatch("vacuum-control", "vacuumControl", "stop", "mutation", input),
3139
+ returnToBase: (input) => dispatch("vacuum-control", "vacuumControl", "returnToBase", "mutation", input),
3140
+ locate: (input) => dispatch("vacuum-control", "vacuumControl", "locate", "mutation", input),
3141
+ setFanSpeed: (input) => dispatch("vacuum-control", "vacuumControl", "setFanSpeed", "mutation", input),
3142
+ getStatus: (input) => dispatch("vacuum-control", "vacuumControl", "getStatus", "query", input)
3143
+ },
3144
+ valve: {
3145
+ open: (input) => dispatch("valve", "valve", "open", "mutation", input),
3146
+ close: (input) => dispatch("valve", "valve", "close", "mutation", input),
3147
+ stop: (input) => dispatch("valve", "valve", "stop", "mutation", input),
3148
+ setPosition: (input) => dispatch("valve", "valve", "setPosition", "mutation", input),
3149
+ getStatus: (input) => dispatch("valve", "valve", "getStatus", "query", input)
3150
+ },
3151
+ vibration: { getStatus: (input) => dispatch("vibration", "vibration", "getStatus", "query", input) },
3152
+ videoclips: {
3153
+ listClips: (input) => dispatch("videoclips", "videoclips", "listClips", "query", input),
3154
+ getClipPlayback: (input) => dispatch("videoclips", "videoclips", "getClipPlayback", "query", input)
3155
+ },
3156
+ waterHeater: {
3157
+ setTargetTemp: (input) => dispatch("water-heater", "waterHeater", "setTargetTemp", "mutation", input),
3158
+ setOperationMode: (input) => dispatch("water-heater", "waterHeater", "setOperationMode", "mutation", input),
3159
+ setAway: (input) => dispatch("water-heater", "waterHeater", "setAway", "mutation", input),
3160
+ getStatus: (input) => dispatch("water-heater", "waterHeater", "getStatus", "query", input)
3161
+ },
3162
+ weather: { getStatus: (input) => dispatch("weather", "weather", "getStatus", "query", input) },
3163
+ webrtcSession: {
3164
+ listStreams: (input) => dispatch("webrtc-session", "webrtcSession", "listStreams", "query", input),
3165
+ createSession: (input) => dispatch("webrtc-session", "webrtcSession", "createSession", "mutation", input),
3166
+ handleOffer: (input) => dispatch("webrtc-session", "webrtcSession", "handleOffer", "mutation", input),
3167
+ handleAnswer: (input) => dispatch("webrtc-session", "webrtcSession", "handleAnswer", "mutation", input),
3168
+ addIceCandidate: (input) => dispatch("webrtc-session", "webrtcSession", "addIceCandidate", "mutation", input),
3169
+ getIceCandidates: (input) => dispatch("webrtc-session", "webrtcSession", "getIceCandidates", "query", input),
3170
+ closeSession: (input) => dispatch("webrtc-session", "webrtcSession", "closeSession", "mutation", input),
3171
+ hasAdaptiveBitrate: (input) => dispatch("webrtc-session", "webrtcSession", "hasAdaptiveBitrate", "query", input),
3172
+ getSessionState: (input) => dispatch("webrtc-session", "webrtcSession", "getSessionState", "query", input)
3173
+ },
3174
+ zoneAnalytics: {
3175
+ getCurrentSnapshot: (input) => dispatch("zone-analytics", "zoneAnalytics", "getCurrentSnapshot", "query", input),
3176
+ getZoneHistory: (input) => dispatch("zone-analytics", "zoneAnalytics", "getZoneHistory", "query", input),
3177
+ getCameraHistory: (input) => dispatch("zone-analytics", "zoneAnalytics", "getCameraHistory", "query", input),
3178
+ getUnzonedHistory: (input) => dispatch("zone-analytics", "zoneAnalytics", "getUnzonedHistory", "query", input)
3179
+ },
3180
+ zoneRules: {
3181
+ listRules: (input) => dispatch("zone-rules", "zoneRules", "listRules", "query", input),
3182
+ setRules: (input) => dispatch("zone-rules", "zoneRules", "setRules", "mutation", input)
3183
+ },
3184
+ zones: {
3185
+ listZones: (input) => dispatch("zones", "zones", "listZones", "query", input),
3186
+ addZone: (input) => dispatch("zones", "zones", "addZone", "mutation", input),
3187
+ removeZone: (input) => dispatch("zones", "zones", "removeZone", "mutation", input),
3188
+ updateZone: (input) => dispatch("zones", "zones", "updateZone", "mutation", input)
3189
+ },
3190
+ addonSettings: {
3191
+ getDeviceSettings: (input) => dispatchSystem("addonSettings", "getDeviceSettings", "query", input),
3192
+ updateDeviceSettings: (input) => dispatchSystem("addonSettings", "updateDeviceSettings", "mutation", input)
3193
+ },
3194
+ cameraPipelineConfig: {
3195
+ getDeviceSettingsContribution: (input) => dispatchSystem("cameraPipelineConfig", "getDeviceSettingsContribution", "query", input),
3196
+ getDeviceLiveContribution: (input) => dispatchSystem("cameraPipelineConfig", "getDeviceLiveContribution", "query", input),
3197
+ applyDeviceSettingsPatch: (input) => dispatchSystem("cameraPipelineConfig", "applyDeviceSettingsPatch", "mutation", input)
3198
+ },
3199
+ deviceAdoption: { getStatus: (input) => dispatchSystem("deviceAdoption", "getStatus", "query", input) },
3200
+ deviceExport: {
3201
+ getDeviceSettingsContribution: (input) => dispatchSystem("deviceExport", "getDeviceSettingsContribution", "query", input),
3202
+ getDeviceLiveContribution: (input) => dispatchSystem("deviceExport", "getDeviceLiveContribution", "query", input),
3203
+ applyDeviceSettingsPatch: (input) => dispatchSystem("deviceExport", "applyDeviceSettingsPatch", "mutation", input)
3204
+ },
3205
+ deviceManager: {
3206
+ loadConfig: (input) => dispatchSystem("deviceManager", "loadConfig", "query", input),
3207
+ loadRuntimeState: (input) => dispatchSystem("deviceManager", "loadRuntimeState", "query", input),
3208
+ loadMeta: (input) => dispatchSystem("deviceManager", "loadMeta", "query", input),
3209
+ setName: (input) => dispatchSystem("deviceManager", "setName", "mutation", input),
3210
+ setLocation: (input) => dispatchSystem("deviceManager", "setLocation", "mutation", input),
3211
+ setType: (input) => dispatchSystem("deviceManager", "setType", "mutation", input),
3212
+ setIntegrationId: (input) => dispatchSystem("deviceManager", "setIntegrationId", "mutation", input),
3213
+ setLinkDeviceId: (input) => dispatchSystem("deviceManager", "setLinkDeviceId", "mutation", input),
3214
+ setPrimaryChildEntityId: (input) => dispatchSystem("deviceManager", "setPrimaryChildEntityId", "mutation", input),
3215
+ setChildLayout: (input) => dispatchSystem("deviceManager", "setChildLayout", "mutation", input),
3216
+ setDeviceLinks: (input) => dispatchSystem("deviceManager", "setDeviceLinks", "mutation", input),
3217
+ getWireableFields: (input) => dispatchSystem("deviceManager", "getWireableFields", "query", input),
3218
+ setRole: (input) => dispatchSystem("deviceManager", "setRole", "mutation", input),
3219
+ applyInitialMeta: (input) => dispatchSystem("deviceManager", "applyInitialMeta", "mutation", input),
3220
+ setMetadata: (input) => dispatchSystem("deviceManager", "setMetadata", "mutation", input),
3221
+ setDisabled: (input) => dispatchSystem("deviceManager", "setDisabled", "mutation", input),
3222
+ getDevice: (input) => dispatchSystem("deviceManager", "getDevice", "query", input),
3223
+ getStreamSources: (input) => dispatchSystem("deviceManager", "getStreamSources", "query", input),
3224
+ getConfigSchema: (input) => dispatchSystem("deviceManager", "getConfigSchema", "query", input),
3225
+ getSettingsSchema: (input) => dispatchSystem("deviceManager", "getSettingsSchema", "query", input),
3226
+ updateConfig: (input) => dispatchSystem("deviceManager", "updateConfig", "mutation", input),
3227
+ enable: (input) => dispatchSystem("deviceManager", "enable", "mutation", input),
3228
+ disable: (input) => dispatchSystem("deviceManager", "disable", "mutation", input),
3229
+ remove: (input) => dispatchSystem("deviceManager", "remove", "mutation", input),
3230
+ getStreamProfileMap: (input) => dispatchSystem("deviceManager", "getStreamProfileMap", "query", input),
3231
+ setStreamProfileMap: (input) => dispatchSystem("deviceManager", "setStreamProfileMap", "mutation", input),
3232
+ probeStreams: (input) => dispatchSystem("deviceManager", "probeStreams", "mutation", input),
3233
+ getBindings: (input) => dispatchSystem("deviceManager", "getBindings", "query", input),
3234
+ getAllBindings: (input) => dispatchSystem("deviceManager", "getAllBindings", "query", input),
3235
+ setWrapperActive: (input) => dispatchSystem("deviceManager", "setWrapperActive", "mutation", input),
3236
+ getDeviceSettingsAggregate: (input) => dispatchSystem("deviceManager", "getDeviceSettingsAggregate", "query", input),
3237
+ getDeviceLiveInfoAggregate: (input) => dispatchSystem("deviceManager", "getDeviceLiveInfoAggregate", "query", input),
3238
+ getDeviceAggregate: (input) => dispatchSystem("deviceManager", "getDeviceAggregate", "query", input),
3239
+ runDeviceAction: (input) => dispatchSystem("deviceManager", "runDeviceAction", "mutation", input),
3240
+ updateDeviceField: (input) => dispatchSystem("deviceManager", "updateDeviceField", "mutation", input),
3241
+ updateDeviceFieldsBatch: (input) => dispatchSystem("deviceManager", "updateDeviceFieldsBatch", "mutation", input),
3242
+ testField: (input) => dispatchSystem("deviceManager", "testField", "mutation", input),
3243
+ getDeviceStatusAggregate: (input) => dispatchSystem("deviceManager", "getDeviceStatusAggregate", "query", input)
3244
+ },
3245
+ deviceState: {
3246
+ getSnapshot: (input) => dispatchSystem("deviceState", "getSnapshot", "query", input),
3247
+ getCapSlice: (input) => dispatchSystem("deviceState", "getCapSlice", "query", input),
3248
+ setCapSlice: (input) => dispatchSystem("deviceState", "setCapSlice", "mutation", input)
3249
+ },
3250
+ faceGallery: { getFaceByTrack: (input) => dispatchSystem("faceGallery", "getFaceByTrack", "query", input) },
3251
+ networkQuality: {
3252
+ getDeviceStats: (input) => dispatchSystem("networkQuality", "getDeviceStats", "query", input),
3253
+ reportClientStats: (input) => dispatchSystem("networkQuality", "reportClientStats", "mutation", input)
3254
+ },
3255
+ pipelineExecutor: {
3256
+ runPipeline: (input) => dispatchSystem("pipelineExecutor", "runPipeline", "mutation", input),
3257
+ runPipelineBatch: (input) => dispatchSystem("pipelineExecutor", "runPipelineBatch", "mutation", input)
3258
+ },
3259
+ pipelineOrchestrator: {
3260
+ assignPipeline: (input) => dispatchSystem("pipelineOrchestrator", "assignPipeline", "mutation", input),
3261
+ unassignPipeline: (input) => dispatchSystem("pipelineOrchestrator", "unassignPipeline", "mutation", input),
3262
+ getPipelineAssignment: (input) => dispatchSystem("pipelineOrchestrator", "getPipelineAssignment", "query", input),
3263
+ getCameraMetrics: (input) => dispatchSystem("pipelineOrchestrator", "getCameraMetrics", "query", input),
3264
+ assignDecoder: (input) => dispatchSystem("pipelineOrchestrator", "assignDecoder", "mutation", input),
3265
+ unassignDecoder: (input) => dispatchSystem("pipelineOrchestrator", "unassignDecoder", "mutation", input),
3266
+ assignAudio: (input) => dispatchSystem("pipelineOrchestrator", "assignAudio", "mutation", input),
3267
+ unassignAudio: (input) => dispatchSystem("pipelineOrchestrator", "unassignAudio", "mutation", input),
3268
+ getAudioAssignment: (input) => dispatchSystem("pipelineOrchestrator", "getAudioAssignment", "query", input),
3269
+ getAudioAssignments: (input) => dispatchSystem("pipelineOrchestrator", "getAudioAssignments", "query", input),
3270
+ getDecoderAssignment: (input) => dispatchSystem("pipelineOrchestrator", "getDecoderAssignment", "query", input),
3271
+ getCameraSettings: (input) => dispatchSystem("pipelineOrchestrator", "getCameraSettings", "query", input),
3272
+ setCameraStepToggle: (input) => dispatchSystem("pipelineOrchestrator", "setCameraStepToggle", "mutation", input),
3273
+ getCameraStepOverrides: (input) => dispatchSystem("pipelineOrchestrator", "getCameraStepOverrides", "query", input),
3274
+ setCameraStepOverride: (input) => dispatchSystem("pipelineOrchestrator", "setCameraStepOverride", "mutation", input),
3275
+ setCameraPipelineForAgent: (input) => dispatchSystem("pipelineOrchestrator", "setCameraPipelineForAgent", "mutation", input),
3276
+ resolvePipeline: (input) => dispatchSystem("pipelineOrchestrator", "resolvePipeline", "query", input),
3277
+ getCameraStatus: (input) => dispatchSystem("pipelineOrchestrator", "getCameraStatus", "query", input),
3278
+ getDeviceSettingsContribution: (input) => dispatchSystem("pipelineOrchestrator", "getDeviceSettingsContribution", "query", input),
3279
+ getDeviceLiveContribution: (input) => dispatchSystem("pipelineOrchestrator", "getDeviceLiveContribution", "query", input),
3280
+ applyDeviceSettingsPatch: (input) => dispatchSystem("pipelineOrchestrator", "applyDeviceSettingsPatch", "mutation", input)
3281
+ },
3282
+ pipelineRunner: {
3283
+ detachCamera: (input) => dispatchSystem("pipelineRunner", "detachCamera", "mutation", input),
3284
+ getCameraMetrics: (input) => dispatchSystem("pipelineRunner", "getCameraMetrics", "query", input)
3285
+ },
3286
+ plateGallery: {
3287
+ listPlates: (input) => dispatchSystem("plateGallery", "listPlates", "query", input),
3288
+ getPlateByTrack: (input) => dispatchSystem("plateGallery", "getPlateByTrack", "query", input)
3289
+ },
3290
+ recording: {
3291
+ getAvailability: (input) => dispatchSystem("recording", "getAvailability", "query", input),
3292
+ getDaysWithRecordings: (input) => dispatchSystem("recording", "getDaysWithRecordings", "query", input),
3293
+ getPlaybackManifest: (input) => dispatchSystem("recording", "getPlaybackManifest", "query", input),
3294
+ getDeviceConfig: (input) => dispatchSystem("recording", "getDeviceConfig", "query", input),
3295
+ locateSegment: (input) => dispatchSystem("recording", "locateSegment", "query", input),
3296
+ readSegmentBytes: (input) => dispatchSystem("recording", "readSegmentBytes", "query", input),
3297
+ setDeviceConfig: (input) => dispatchSystem("recording", "setDeviceConfig", "mutation", input),
3298
+ rescanStorage: (input) => dispatchSystem("recording", "rescanStorage", "mutation", input),
3299
+ pruneFootage: (input) => dispatchSystem("recording", "pruneFootage", "mutation", input),
3300
+ getStatus: (input) => dispatchSystem("recording", "getStatus", "query", input),
3301
+ getDeviceSettingsContribution: (input) => dispatchSystem("recording", "getDeviceSettingsContribution", "query", input),
3302
+ getDeviceLiveContribution: (input) => dispatchSystem("recording", "getDeviceLiveContribution", "query", input),
3303
+ applyDeviceSettingsPatch: (input) => dispatchSystem("recording", "applyDeviceSettingsPatch", "mutation", input)
3304
+ },
3305
+ snapshotProvider: {
3306
+ supportsDevice: (input) => dispatchSystem("snapshotProvider", "supportsDevice", "query", input),
3307
+ getSnapshot: (input) => dispatchSystem("snapshotProvider", "getSnapshot", "query", input)
3308
+ },
3309
+ streamBroker: {
3310
+ publishCameraStream: (input) => dispatchSystem("streamBroker", "publishCameraStream", "mutation", input),
3311
+ retractCameraStream: (input) => dispatchSystem("streamBroker", "retractCameraStream", "mutation", input),
3312
+ assignProfile: (input) => dispatchSystem("streamBroker", "assignProfile", "mutation", input),
3313
+ unassignProfile: (input) => dispatchSystem("streamBroker", "unassignProfile", "mutation", input),
3314
+ restartProfile: (input) => dispatchSystem("streamBroker", "restartProfile", "mutation", input),
3315
+ getDeviceSettingsContribution: (input) => dispatchSystem("streamBroker", "getDeviceSettingsContribution", "query", input),
3316
+ getDeviceLiveContribution: (input) => dispatchSystem("streamBroker", "getDeviceLiveContribution", "query", input),
3317
+ applyDeviceSettingsPatch: (input) => dispatchSystem("streamBroker", "applyDeviceSettingsPatch", "mutation", input)
3318
+ }
3319
+ };
3320
+ }
3321
+ //#endregion
3322
+ //#region src/capabilities/admin-ui.cap.ts
3323
+ var StaticDirOutputSchema = z.object({ staticDir: z.string() });
3324
+ var VersionOutputSchema = z.object({ version: z.string() });
3325
+ var adminUiCapability = {
3326
+ name: "admin-ui",
3327
+ scope: "system",
3328
+ mode: "singleton",
3329
+ internal: true,
3330
+ methods: {
3331
+ getStaticDir: method(z.void(), StaticDirOutputSchema),
3332
+ getVersion: method(z.void(), VersionOutputSchema)
3333
+ }
3334
+ };
3335
+ //#endregion
3336
+ //#region src/capabilities/device-ops.cap.ts
3337
+ /**
3338
+ * device-ops — device-scoped cap that unifies the per-IDevice operations
3339
+ * previously routed through the `.device-ops` Moleculer bridge service.
3340
+ *
3341
+ * Each worker that hosts live `IDevice` instances auto-registers a native
3342
+ * provider for this cap (per device) backed by its local
3343
+ * `DeviceRegistry`. Hub-side callers reach it transparently through
3344
+ * `ctx.fetchDevice(id).deviceOps.*` — the DeviceProxy injects
3345
+ * `deviceId` + `nodeId` and dispatches through the standard cap-router,
3346
+ * so there's no parallel bridge path anymore.
3347
+ *
3348
+ * The surface is intentionally small — every method corresponds to a
3349
+ * single action on the live `IDevice` (or `ICameraDevice` for
3350
+ * `getStreamSources`). Richer orchestration (enable/disable with
3351
+ * integration plumbing, bulk updates) stays in the `device-manager` cap;
3352
+ * `device-ops` is the per-device primitive the device-manager routes to.
3353
+ */
3354
+ var StreamSourceEntrySchema = z.object({
3355
+ id: z.string(),
3356
+ label: z.string(),
3357
+ protocol: z.enum([
3358
+ "rtsp",
3359
+ "rtmp",
3360
+ "annexb",
3361
+ "http-mjpeg",
3362
+ "webrtc",
3363
+ "custom"
3364
+ ]),
3365
+ url: z.string().optional(),
3366
+ resolution: z.object({
3367
+ width: z.number(),
3368
+ height: z.number()
3369
+ }).optional(),
3370
+ fps: z.number().optional(),
3371
+ bitrate: z.number().optional(),
3372
+ codec: z.string().optional(),
3373
+ profileHint: CamProfileSchema.optional(),
3374
+ sdp: z.string().optional()
3375
+ });
3376
+ var ConfigEntrySchema = z.object({
3377
+ key: z.string(),
3378
+ value: z.unknown()
3379
+ });
3380
+ var RawStateResultSchema = z.object({
3381
+ /** Originating provider id, e.g. 'homeassistant' | 'reolink' | 'hikvision'. */
3382
+ source: z.string(),
3383
+ /** Opaque, DISPLAY-SAFE upstream blob (no secrets/PII). */
3384
+ data: z.record(z.string(), z.unknown())
3385
+ });
3386
+ var deviceOpsCapability = {
3387
+ name: "device-ops",
3388
+ scope: "device",
3389
+ deviceNative: true,
3390
+ mode: "singleton",
3391
+ methods: {
3392
+ /**
3393
+ * Return stream sources for camera-like devices. Non-camera devices
3394
+ * return an empty array (the bridge did the same; preserved for compat).
3395
+ */
3396
+ getStreamSources: method(z.object({ deviceId: z.number() }), z.array(StreamSourceEntrySchema)),
3397
+ /**
3398
+ * Return the device's config entries (key + current value). Used by
3399
+ * the device-manager aggregator when reading the driver's schema+values.
3400
+ */
3401
+ getConfigEntries: method(z.object({ deviceId: z.number() }), z.array(ConfigEntrySchema)),
3402
+ /**
3403
+ * Bulk-apply a config patch via `IDevice.config.setAll`. Covers the
3404
+ * updateConfig / setStreamProfileMap / enable-as-config paths from
3405
+ * the old bridge.
3406
+ */
3407
+ setConfig: method(z.object({
3408
+ deviceId: z.number(),
3409
+ values: z.record(z.string(), z.unknown())
3410
+ }), z.void(), { kind: "mutation" }),
3411
+ /**
3412
+ * Invoke a device custom action on a forked/remote device (the
3413
+ * cross-process transport for `IDevice.runDeviceAction`). Mirrors
3414
+ * `setConfig` — the device-manager calls this when the device is not
3415
+ * hub-local.
3416
+ */
3417
+ runAction: method(z.object({
3418
+ deviceId: z.number(),
3419
+ action: z.string().min(1),
3420
+ input: z.unknown()
3421
+ }), z.unknown(), { kind: "mutation" }),
3422
+ /**
3423
+ * Invoke `IDevice.removeDevice()` so the driver can release resources
3424
+ * (close sockets, stop background tasks, …). The device-manager still
3425
+ * performs its own persistence cleanup before/after this call.
3426
+ */
3427
+ removeDevice: method(z.object({ deviceId: z.number() }), z.void(), { kind: "mutation" }),
3428
+ /**
3429
+ * Build the ConfigUISchema (FormBuilder input shape) from the device's
3430
+ * Zod config schema. Runs on the worker that owns the IDevice so the
3431
+ * Zod types stay local (they're function references, not
3432
+ * serializable). Returns a fully JSON-serializable schema with
3433
+ * sections/fields the admin UI renders directly.
3434
+ *
3435
+ * Needed because the hub-side `device-manager.getSettingsSchema`
3436
+ * couldn't reach forked-worker devices — it had no registry entry
3437
+ * and no cross-process lookup, so the UI silently rendered an empty
3438
+ * settings panel for every worker-owned device.
3439
+ *
3440
+ * Returns `null` when the device isn't found on this worker.
3441
+ */
3442
+ getSettingsSchema: method(z.object({ deviceId: z.number() }), z.unknown().nullable()),
3443
+ /**
3444
+ * Opt-in: return the device's RAW upstream state (the provider's
3445
+ * cached values) as a display-safe `{ source, data }` blob. Returns
3446
+ * `null` when the device exposes no raw state — the State panel hides
3447
+ * its Raw toggle in that case. One-shot (read the provider's existing
3448
+ * cache; no upstream round-trip).
3449
+ */
3450
+ getRawState: method(z.object({ deviceId: z.number() }), RawStateResultSchema.nullable(), { auth: "protected" })
3451
+ }
3452
+ };
3453
+ //#endregion
3454
+ //#region src/utils/sleep.ts
3455
+ /**
3456
+ * Promise-based timer helpers — used everywhere the codebase needs to
3457
+ * wait, back off, or schedule a retry. Before these helpers landed, each
3458
+ * call site re-implemented `new Promise(r => setTimeout(r, ms))` inline,
3459
+ * with subtle variations (some swallowing cancellation, some not). Two
3460
+ * shapes cover every observed use case:
3461
+ *
3462
+ * - {@link sleep} for a plain, uncancellable wait — the default choice.
3463
+ * - {@link sleepCancellable} for a wait that wakes early when an
3464
+ * abort signal trips, used by long-running pollers whose teardown
3465
+ * must stop a pending backoff promptly.
3466
+ */
3467
+ /**
3468
+ * Resolve after `ms` milliseconds. Never rejects, never cancels. The
3469
+ * sleep cannot be interrupted; for a wakeable variant use
3470
+ * {@link sleepCancellable}.
3471
+ *
3472
+ * `ms <= 0` resolves on the next microtask via `setTimeout(0)`, which
3473
+ * still gives the event loop a chance to drain — useful for breaking
3474
+ * up tight async loops without changing call-site semantics.
3475
+ */
3476
+ function sleep(ms) {
3477
+ return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
3478
+ }
3479
+ /**
3480
+ * Resolve after `ms` milliseconds OR when `signal.aborted` flips,
3481
+ * whichever fires first. The returned promise never rejects — aborted
3482
+ * sleeps resolve normally so callers can model "wait or wake" without
3483
+ * try/catch noise.
3484
+ *
3485
+ * await sleepCancellable(backoffMs, lifecycle.abortSignal)
3486
+ * if (lifecycle.aborted) return
3487
+ *
3488
+ * If `signal` is already aborted at call time the helper resolves on
3489
+ * the next microtask.
3490
+ */
3491
+ function sleepCancellable(ms, signal) {
3492
+ if (signal.aborted) return Promise.resolve();
3493
+ return new Promise((resolve) => {
3494
+ const timer = setTimeout(() => {
3495
+ signal.removeEventListener("abort", onAbort);
3496
+ resolve();
3497
+ }, Math.max(0, ms));
3498
+ const onAbort = () => {
3499
+ clearTimeout(timer);
3500
+ signal.removeEventListener("abort", onAbort);
3501
+ resolve();
3502
+ };
3503
+ signal.addEventListener("abort", onAbort, { once: true });
3504
+ });
3505
+ }
3506
+ //#endregion
3507
+ export { SubscribeFramesInputSchema as $, ReadinessTimeoutError as A, CameraStreamSchema as B, asNumber as C, parseJsonUnknown as D, parseJsonObject as E, BrokerStatusSchema as F, FrameHandleSchema as G, DecodedFrameSchema as H, CAM_PROFILE_ORDER as I, ProfileSlotStatusSchema as J, ProfileRtspEntrySchema as K, CamProfileSchema as L, readinessKey as M, scopeKey as N, DATAPLANE_SECRET_HEADER as O, BrokerStatsSchema as P, SubscribeAudioChunksResultSchema as Q, CamStreamKindSchema as R, asJsonObject as S, parseJsonArray as T, EncodedPacketSchema as U, DecodedAudioChunkSchema as V, FrameHandleFormatSchema as W, StreamSourceSchema as X, StreamSourceEntrySchema$1 as Y, SubscribeAudioChunksInputSchema as Z, DeviceFeature as _, adminUiCapability as a, BaseAddon as at, asBoolean as b, createMirrorSource as c, createEvent as ct, DEVICE_STATUS_METHOD as d, WELL_KNOWN_TABS as dt, SubscribeFramesResultSchema as et, event as f, WELL_KNOWN_TAB_MAP as ft, ChargingStatus as g, method as h, DisposerChain as ht, deviceOpsCapability as i, selectAssignedProfileSlots as it, emitDownForOwnedCaps as j, ReadinessRegistry as k, createSliceHandle as l, emitReadiness as lt, isDeviceConfigCap as m, EventCategory as mt, sleepCancellable as n, makeSourceBrokerId as nt, createDeviceProxy as o, normalizeAddonInitResult as ot, expandCapMethods as p, hydrateSchema as pt, ProfileSlotSchema as q, RawStateResultSchema as r, parseProfileBrokerId as rt, createLazyTrpcSource as s, createDurableState as st, sleep as t, makeProfileBrokerId as tt, DEVICE_SETTINGS_CONTRIBUTION_METHODS as u, isEvent as ut, DeviceRole as v, asString as w, asJsonArray as x, DeviceType as y, CamStreamResolutionSchema as z };