@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
package/dist/index.mjs CHANGED
@@ -1,3 +1,5 @@
1
+ import { $ as SubscribeFramesInputSchema, A as ReadinessTimeoutError, B as CameraStreamSchema, C as asNumber, D as parseJsonUnknown, E as parseJsonObject, F as BrokerStatusSchema, G as FrameHandleSchema, H as DecodedFrameSchema, I as CAM_PROFILE_ORDER, J as ProfileSlotStatusSchema, K as ProfileRtspEntrySchema, L as CamProfileSchema, M as readinessKey, N as scopeKey, O as DATAPLANE_SECRET_HEADER, P as BrokerStatsSchema, Q as SubscribeAudioChunksResultSchema, R as CamStreamKindSchema, S as asJsonObject, T as parseJsonArray, U as EncodedPacketSchema, V as DecodedAudioChunkSchema, W as FrameHandleFormatSchema, X as StreamSourceSchema, Y as StreamSourceEntrySchema, Z as SubscribeAudioChunksInputSchema, _ as DeviceFeature, a as adminUiCapability, at as BaseAddon, b as asBoolean, c as createMirrorSource, ct as createEvent, d as DEVICE_STATUS_METHOD, dt as WELL_KNOWN_TABS, et as SubscribeFramesResultSchema, f as event, ft as WELL_KNOWN_TAB_MAP, g as ChargingStatus, h as method, ht as DisposerChain, i as deviceOpsCapability, it as selectAssignedProfileSlots, j as emitDownForOwnedCaps, k as ReadinessRegistry, l as createSliceHandle, lt as emitReadiness, m as isDeviceConfigCap, mt as EventCategory, n as sleepCancellable, nt as makeSourceBrokerId, o as createDeviceProxy, ot as normalizeAddonInitResult, p as expandCapMethods, pt as hydrateSchema, q as ProfileSlotSchema, r as RawStateResultSchema, rt as parseProfileBrokerId, s as createLazyTrpcSource, st as createDurableState, t as sleep, tt as makeProfileBrokerId, u as DEVICE_SETTINGS_CONTRIBUTION_METHODS, ut as isEvent, v as DeviceRole, w as asString, x as asJsonArray, y as DeviceType, z as CamStreamResolutionSchema } from "./sleep-D7JeS58T.mjs";
2
+ import { t as errMsg } from "./err-msg-IQTHeDzc.mjs";
1
3
  import { z } from "zod";
2
4
  //#region src/health/wiring-health.ts
3
5
  /**
@@ -60,71 +62,6 @@ var MODEL_FORMATS = [
60
62
  "pt"
61
63
  ];
62
64
  //#endregion
63
- //#region src/disposer-chain.ts
64
- var DisposerChain = class {
65
- disposers = [];
66
- disposed = false;
67
- onError;
68
- constructor(opts = {}) {
69
- this.onError = opts.onError ?? ((err, index) => {
70
- console.error(`[DisposerChain] disposer #${index} threw`, err);
71
- });
72
- }
73
- /**
74
- * Register a teardown callback. Returns an unregister function so
75
- * callers can drop a single disposer without disposing the whole
76
- * chain.
77
- *
78
- * If the chain has already been disposed, the callback runs immediately
79
- * (sync) — this matches the “register-after-shutdown” edge case where
80
- * an addon's late initialization races with kernel restart.
81
- */
82
- add(fn) {
83
- if (this.disposed) {
84
- try {
85
- const result = fn();
86
- if (result && typeof result.then === "function") result.catch((err) => this.onError(err, -1));
87
- } catch (err) {
88
- this.onError(err, -1);
89
- }
90
- return () => void 0;
91
- }
92
- this.disposers.push(fn);
93
- return () => {
94
- const idx = this.disposers.indexOf(fn);
95
- if (idx >= 0) this.disposers.splice(idx, 1);
96
- };
97
- }
98
- /** True after `dispose()` has been called at least once. */
99
- get isDisposed() {
100
- return this.disposed;
101
- }
102
- /** Number of disposers currently registered. */
103
- get size() {
104
- return this.disposers.length;
105
- }
106
- /**
107
- * Run every registered disposer in LIFO order. Idempotent: subsequent
108
- * calls do nothing. Awaits async disposers so callers can sequence
109
- * shutdown → restart correctly.
110
- */
111
- async dispose() {
112
- if (this.disposed) return;
113
- this.disposed = true;
114
- const drain = this.disposers.slice().toReversed();
115
- this.disposers = [];
116
- for (let i = 0; i < drain.length; i++) {
117
- const fn = drain[i];
118
- try {
119
- const result = fn();
120
- if (result && typeof result.then === "function") await result;
121
- } catch (err) {
122
- this.onError(err, drain.length - 1 - i);
123
- }
124
- }
125
- }
126
- };
127
- //#endregion
128
65
  //#region src/interfaces/addon.ts
129
66
  var DEFAULT_ADDON_PLACEMENT = "hub-only";
130
67
  /**
@@ -149,1299 +86,29 @@ function isAgentOnlyPlacement(decl) {
149
86
  return resolveAddonExecution(decl).placement === "agent-only";
150
87
  }
151
88
  /**
152
- * Convenience accessor — the declared co-location group label, or
153
- * `undefined` when the addon declares no group (→ its own dedicated
154
- * runner). Callers that need the runner id should use `resolveRunnerId`.
155
- */
156
- function resolveAddonGroup(decl) {
157
- return resolveAddonExecution(decl).group;
158
- }
159
- /**
160
- * Resolve the runner id an addon belongs to. This is the single
161
- * authority for the hub↔runner topology:
162
- * - declared `group` present → the runner id is the group name
163
- * (shared with every other addon declaring the same group);
164
- * - no `group` declared → the runner id is the addon id itself
165
- * (its own dedicated runner — one addon, one process).
166
- *
167
- * The runner's Moleculer nodeID is `${parentNodeId}/${runnerId}`.
168
- */
169
- function resolveRunnerId(decl, addonId) {
170
- return decl.execution?.group ?? addonId;
171
- }
172
- /** Convenience accessor — the resolved placement (defaults to `hub-only`). */
173
- function resolveAddonPlacement(decl) {
174
- return resolveAddonExecution(decl).placement;
175
- }
176
- //#endregion
177
- //#region src/enums/event-category.ts
178
- var EventCategory = /* @__PURE__ */ function(EventCategory) {
179
- EventCategory["SystemBoot"] = "system.boot";
180
- EventCategory["SystemAddonsReady"] = "system.addons-ready";
181
- EventCategory["SystemRestarting"] = "system.restarting";
182
- /**
183
- * Fired exactly once after the hub finishes booting following a
184
- * restart that was triggered by `RestartCoordinator` (today: framework
185
- * live-update). Payload is the marker that was on disk at boot
186
- * (`PendingRestartMarkerPayload`); admin UI listens for it to display a
187
- * success toast describing what changed.
188
- *
189
- * Spec: docs/superpowers/specs/2026-05-14-framework-live-update-design.md
190
- */
191
- EventCategory["SystemRestartCompleted"] = "system.restart-completed";
192
- /**
193
- * Readiness transition for a capability provider. Every producer emits
194
- * this event on `onInitialize` completion, `onDestroy`, and
195
- * `$node.reconnect`; every consumer that needs to gate on a cross-process
196
- * cap subscribes via the kernel's readiness module (`awaitReady` /
197
- * `onReadyState`) instead of polling. Payload is
198
- * `SystemReadyStatePayload` — see event-bus.ts.
199
- */
200
- EventCategory["SystemReadyState"] = "system.ready-state";
201
- EventCategory["AddonStarted"] = "addon.started";
202
- EventCategory["AddonStopped"] = "addon.stopped";
203
- EventCategory["AddonRestarted"] = "addon.restarted";
204
- EventCategory["AddonUpdated"] = "addon.updated";
205
- EventCategory["AddonInstalled"] = "addon.installed";
206
- EventCategory["AddonUninstalled"] = "addon.uninstalled";
207
- EventCategory["AddonCrashed"] = "addon.crashed";
208
- EventCategory["AddonError"] = "addon.error";
209
- EventCategory["AddonPageReady"] = "addon.page-ready";
210
- EventCategory["AddonWidgetReady"] = "addon.widget-ready";
211
- /**
212
- * Addon failed to load (import or initialize). Emitted by the kernel's
213
- * AddonHealthMonitor only AFTER the boot grace period ends — failures
214
- * during the first 5 minutes are silently retried without alerting
215
- * (slow-starting addons must have time to come up). Post-grace, this
216
- * event is emitted exactly once per failure-streak; AlertCenter
217
- * consumes it to create a persistent operator-visible alert.
218
- *
219
- * Payload: `{ packageName, addonId?, error: { message, stack }, retryCount, nextRetryAt }`.
220
- */
221
- EventCategory["AddonLoadFailed"] = "addon.load-failed";
222
- /**
223
- * Addon recovered from a previous failure. Emitted when an addon
224
- * transitions from `failed` back to `healthy` (typically via the
225
- * monitor's auto-retry loop, or after manual `addons.retryLoad`).
226
- * AlertCenter dismisses the corresponding `AddonLoadFailed` alert
227
- * on this event.
228
- */
229
- EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
230
- /**
231
- * Monitor scheduled the next retry for a failed addon. Transient —
232
- * surfaced to the UI for live-updating the "next retry in Ns"
233
- * countdown on the Addons page row, NOT persisted as an alert.
234
- */
235
- EventCategory["AddonRetryScheduled"] = "addon.retry-scheduled";
236
- /**
237
- * Monitor is attempting to reload a failed addon NOW. UI uses this
238
- * to show a spinner during the retry attempt. Same transient nature
239
- * as AddonRetryScheduled.
240
- */
241
- EventCategory["AddonRetryAttempting"] = "addon.retry-attempting";
242
- EventCategory["DeviceRegistered"] = "device.registered";
243
- EventCategory["DeviceUnregistered"] = "device.unregistered";
244
- EventCategory["DeviceEnabled"] = "device.enabled";
245
- EventCategory["DeviceDisabled"] = "device.disabled";
246
- EventCategory["DeviceSettingsUpdated"] = "device.settings-updated";
247
- /**
248
- * Emitted when the set of native capability providers bound to a device
249
- * changes — e.g. an addon registers a new native cap via
250
- * `DeviceContext.registerNativeCap`, or all native bindings for a device
251
- * are cleared on removal. Hub consumers re-resolve device-proxy routes
252
- * when this fires.
253
- */
254
- EventCategory["DeviceBindingsChanged"] = "device.bindings-changed";
255
- /**
256
- * Emitted when the operator-organisational meta surface changes
257
- * (`name` / `location` / `disabled`). Payload: `{deviceId, field,
258
- * value}`. Live consumers (UI device list, alert center) react
259
- * without polling. Distinct from `DeviceSettingsUpdated` which
260
- * fires on hardware-config changes (host/port/credentials/etc).
261
- */
262
- EventCategory["DeviceMetaChanged"] = "device.meta-changed";
263
- /**
264
- * Emitted by `BaseDevice.updateSourceInfo()` after a successful patch
265
- * to the device's upstream-system identity / rendering envelope. The
266
- * full new SourceInfo travels in the payload so cross-process listeners
267
- * (UI, export adapters) don't need to re-read the meta blob.
268
- *
269
- * Payload: `{deviceId, sourceInfo}`.
270
- *
271
- * Distinct from `DeviceMetaChanged` which covers the operator-edited
272
- * surface (`name` / `location` / `disabled`). The two share the
273
- * persistence layer (both ride on `device-manager.setMetadata` for
274
- * SourceInfo, or `setName`/`setLocation`/`setDisabled` for the meta
275
- * fields) but consumers care about different subsets.
276
- */
277
- EventCategory["DeviceSourceInfoChanged"] = "device.source-info-changed";
278
- /**
279
- * Emitted by DeviceStreamWiringService after a successful
280
- * `stream-broker.registerDeviceStreams` call. Payload includes
281
- * deviceId — consumers look up the full registered device info via
282
- * `brokerManager.getRegisteredDevice(deviceId)`.
283
- *
284
- * Replaces the legacy `StreamRouterService.onDeviceRegistered` callback.
285
- */
286
- EventCategory["DeviceStreamsRegistered"] = "device.streams-registered";
287
- /**
288
- * Device-level "fully provisioned" signal — the EXPORT trigger. Emitted once
289
- * a device's persisted export-relevant shape (its `DeviceFeature` set) is
290
- * established or changes, carrying `{deviceId, fingerprint, generation}`.
291
- * Export adapters (Alexa / HAP) react to THIS instead of the chatty per-cap
292
- * `DeviceBindingsChanged`, turning a boot's incomplete→complete trickle into
293
- * one clean delta. `fingerprint` is `canonicalDeviceFingerprint(shape)`.
294
- *
295
- * Spec: docs/superpowers/specs/2026-06-01-alexa-hap-export-reconciler-design.md
296
- */
297
- EventCategory["DeviceProvisioned"] = "device.provisioned";
298
- /**
299
- * Fires once when a device's initial feature-probe completes successfully
300
- * (lastProbedAt 0→>0); exported shape is now stable. Telemetry (may be
301
- * dropped) — consumers MUST also gate on the device record's `probed` flag.
302
- */
303
- EventCategory["DeviceReady"] = "device.ready";
304
- EventCategory["IntegrationEnabled"] = "integration.enabled";
305
- EventCategory["IntegrationDisabled"] = "integration.disabled";
306
- EventCategory["IntegrationDeleted"] = "integration.deleted";
307
- /** Emitted when a broker connection's status flips (connected /
308
- * disconnected / error). Carries `{ brokerId, status, error? }`. */
309
- EventCategory["BrokerStatusChanged"] = "broker.status-changed";
310
- /** Emitted for every subscription-matched message routed by a
311
- * broker provider. Carries `{ brokerId, subscriptionId, key, payload }`.
312
- * Consumers filter by `brokerId` + `subscriptionId` in the handler. */
313
- EventCategory["BrokerMessage"] = "broker.message";
314
- EventCategory["ProviderStarted"] = "provider.started";
315
- EventCategory["ProviderStopped"] = "provider.stopped";
316
- EventCategory["ProcessCrashed"] = "process.crashed";
317
- EventCategory["ProcessRestartScheduled"] = "process.restart_scheduled";
318
- EventCategory["ProcessRestarted"] = "process.restarted";
319
- EventCategory["RecordingStarted"] = "recording.started";
320
- EventCategory["RecordingStopped"] = "recording.stopped";
321
- EventCategory["RecordingError"] = "recording.error";
322
- EventCategory["RecordingHealthDegraded"] = "recording.health.degraded";
323
- EventCategory["RecordingStorageCritical"] = "recording.storage.critical";
324
- EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
325
- EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
326
- EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
327
- EventCategory["DetectionEvent"] = "detection.event";
328
- EventCategory["SessionTrackNew"] = "session.track.new";
329
- EventCategory["SessionTrackExpired"] = "session.track.expired";
330
- EventCategory["BenchmarkProgress"] = "benchmark.progress";
331
- EventCategory["PlatformProbePhase"] = "platform-probe.phase";
332
- EventCategory["PipelineProgress"] = "pipeline.progress";
333
- /** Per-frame execution trace emitted by the pipeline executor for live observability. */
334
- EventCategory["PipelineTrace"] = "pipeline.trace";
335
- /**
336
- * Raw inference output emitted by `addon-pipeline-runner` after running the
337
- * detection pipeline on a frame. Carries the `FrameResult` (with
338
- * `detections[]`, `width`/`height`, timing debug) — never the frame
339
- * buffer itself. Hub-side consumers (analysis pipeline, class filters,
340
- * notifications) subscribe to this and re-emit `detection.result` after
341
- * post-processing.
342
- */
343
- EventCategory["PipelineInferenceResult"] = "pipeline.inference-result";
344
- /**
345
- * Camera lifecycle event emitted by `addon-pipeline-orchestrator` when it
346
- * assigns or unassigns a camera to/from an agent. Carries no frame data;
347
- * pure observability for UI dashboards and metrics consumers.
348
- */
349
- EventCategory["PipelineCameraAssigned"] = "pipeline.camera-assigned";
350
- EventCategory["PipelineCameraUnassigned"] = "pipeline.camera-unassigned";
351
- /**
352
- * Per-camera pipeline config was mutated by the orchestrator
353
- * (3-level settings change via `setAgentAddonDefaults` /
354
- * `setCameraStepToggle` / `setCameraPipelineForAgent` or a
355
- * pipeline-scoped `applyDeviceSettingsPatch`). Orchestrator
356
- * subscribes to its own emission to hot-reload the assigned runner
357
- * via `attachCamera` so the next frame executes against the new
358
- * engine/steps without waiting for a rebalance or the next
359
- * `DeviceStreamsRegistered` cycle.
360
- */
361
- EventCategory["PipelineCameraUpdated"] = "pipeline.camera-updated";
362
- /**
363
- * Periodic snapshot of per-node pipeline-runner load
364
- * (`RunnerLocalLoad`). Emitted ~1Hz by every runner so UI dashboards
365
- * subscribe instead of polling `pipelineRunner.getLocalLoad`.
366
- * `nodeId` carried in the payload + on `event.source.nodeId`.
367
- */
368
- EventCategory["PipelineRunnerLoadSnapshot"] = "pipeline.runner-load-snapshot";
369
- /**
370
- * Periodic snapshot of per-camera pipeline metrics (`CameraMetrics`
371
- * + `deviceId` + `nodeId`). Emitted ~1Hz by every runner for each
372
- * attached camera. UI subscribes to drive overlay phase / fps /
373
- * inference time without polling `getCameraMetrics`.
374
- */
375
- EventCategory["PipelineCameraMetricsSnapshot"] = "pipeline.camera-metrics-snapshot";
376
- /**
377
- * Periodic snapshot of stream-broker per-broker statistics (input
378
- * fps, decoded fps, bitrate, codec). Emitted ~1Hz by every
379
- * stream-broker process for each active broker so the UI can drive
380
- * the Stream / Cluster dashboards without polling
381
- * `streamBroker.listAllProfileSlots` and friends.
382
- */
383
- EventCategory["StreamBrokerMetricsSnapshot"] = "stream-broker.metrics-snapshot";
384
- /**
385
- * Cap event fired by `stream-broker` when a profile slot enters
386
- * "demanded" state — a cam stream has been assigned and at least one
387
- * consumer (RTSP restream, decoded subscriber, WebRTC session, …) is
388
- * present. Camera-provider addons (Reolink Baichuan push, …)
389
- * subscribe to this category to start their underlying transport
390
- * lazily. Payload: `{ deviceId, camStreamId, profile }`.
391
- */
392
- EventCategory["StreamBrokerOnCamStreamDemand"] = "stream-broker.onCamStreamDemand";
393
- /**
394
- * Cap event fired by `stream-broker` when the last consumer leaves a
395
- * previously-demanded cam stream. Providers tear down their
396
- * underlying transport on receipt. Payload: `{ deviceId, camStreamId }`.
397
- */
398
- EventCategory["StreamBrokerOnCamStreamIdle"] = "stream-broker.onCamStreamIdle";
399
- /**
400
- * Cap event fired by `stream-broker` when a broker fails to dial a
401
- * managed-loopback source (today: `pull-rfc4571`) and the publisher
402
- * needs to refresh the cached URL. The lib's TCP server idle-tears-down
403
- * on its own schedule, so a re-publish with a fresh `host:port` is the
404
- * only way to keep the broker dialable. Camera providers (Reolink
405
- * Baichuan native, …) subscribe and respond by re-running their publish
406
- * pipeline.
407
- * Payload: `{ deviceId, camStreamId, brokerId }`.
408
- */
409
- EventCategory["StreamBrokerOnRequestStreamSourceRefresh"] = "stream-broker.onRequestStreamSourceRefresh";
410
- /**
411
- * A camera provider changed a device's stream parameters (codec /
412
- * resolution / bitrate / a stream added or removed). A LOW-LATENCY NUDGE
413
- * for the stream-broker's catalog reconcile: it carries NO authoritative
414
- * data — the broker re-PULLS that device's `stream-catalog` cap on receipt
415
- * (the 30s reconcile poll is the backstop if this nudge is dropped). Emitted
416
- * by providers from `stream-params.setProfile`. Payload: `{ deviceId }`.
417
- */
418
- EventCategory["StreamParamsChanged"] = "stream-params.changed";
419
- /**
420
- * Generic per-device runtime-state change. Fired by `device-manager`
421
- * whenever a persisted slice in any cap's `runtimeState` shape
422
- * mutates. Payload: `{deviceId, capName, slice}`. Subscribers are
423
- * the `deviceState` cap router (cross-process listeners) and the
424
- * deviceProxy reactive bindings (`device.state.<capName>.value`).
425
- * Cap-specific events (`battery.onStatusChanged`, …) still fire
426
- * — they're authoritative for callers that want a typed payload
427
- * without filtering on `capName`.
428
- */
429
- EventCategory["DeviceStateChanged"] = "device.state-changed";
430
- /**
431
- * Cap event fired by every device that registers the `battery`
432
- * capability. Mirrors the cap definition's `onStatusChanged`. Carries
433
- * `{ deviceId, status: BatteryStatus }`. Subscribers (alert center,
434
- * snapshot wrapper, UI) react to charge/sleep transitions without
435
- * polling `batteryCapability.getStatus`.
436
- */
437
- EventCategory["BatteryOnStatusChanged"] = "battery.onStatusChanged";
438
- /**
439
- * Emitted by the battery cap provider WHEN `wakeForStream` enters the
440
- * "wake in progress" window — between the Baichuan wake-up issue and
441
- * the camera's first dialed-back RTP packet. The stream-broker
442
- * manager subscribes to flip the per-broker placeholder reason to
443
- * `'waking'` so viewers see a labelled "WAKING UP" tile instead of
444
- * the generic `'reconnecting'` frame. Carries `{ deviceId }`. The
445
- * complementary "wake complete" signal is the existing
446
- * `BatteryOnStatusChanged { sleeping: false }` event.
447
- */
448
- EventCategory["BatteryOnWakeStarted"] = "battery.onWakeStarted";
449
- /**
450
- * Cap event fired by every device that registers the `doorbell`
451
- * capability. Mirrors `doorbellCapability.events.onPressed`. Carries
452
- * `{ deviceId, timestamp }`. Operators consuming the UI subscribe
453
- * here to render transient ring toasts and a "Recent presses" row
454
- * on the device detail page.
455
- */
456
- EventCategory["DoorbellOnPressed"] = "doorbell.onPressed";
457
- /**
458
- * Cap event fired by every device that registers the `event-emitter`
459
- * capability. Mirrors `eventEmitterCapability.events.onEvent`. Carries
460
- * `{ deviceId, eventType, data, timestamp, seq }` — the device's EXACT
461
- * declared event verbatim (NO normalization). Subscribers (UI event
462
- * stream, advanced-notifier rules) react to fired events without
463
- * holding a cap reference.
464
- */
465
- EventCategory["EventEmitted"] = "event-emitter.event";
466
- /**
467
- * Periodic snapshot of the per-node detection-pipeline engine
468
- * registry (loaded engines, models resident, in-use cameras, idle
469
- * TTL). Emitted ~0.2Hz (every 5 s) by every detection-pipeline
470
- * process. The Engines tab subscribes to drive its inventory view
471
- * without polling `pipelineExecutor.listLoadedEngines`.
472
- */
473
- EventCategory["PipelineEngineMetricsSnapshot"] = "pipeline.engine-metrics-snapshot";
474
- /**
475
- * Per-node detection-engine runtime-provisioning transition. Emitted by
476
- * the detection-pipeline provider on every state change of its lazy
477
- * engine-provisioning machine (idle → installing → verifying → ready,
478
- * or → failed with a `nextRetryAt`). Payload is the
479
- * `EngineProvisioningState` snapshot; `event.source.nodeId` carries the
480
- * node. The Pipeline page subscribes to drive a live "installing
481
- * OpenVINO… / ready" indicator per node without polling
482
- * `pipelineExecutor.getEngineProvisioning`. Telemetry-grade (D8): the UI
483
- * also reads the cap snapshot on mount / reconnect. Phase 2.
484
- */
485
- EventCategory["PipelineEngineProvisioning"] = "pipeline.engine-provisioning";
486
- /**
487
- * Cluster topology snapshot. Carries the same payload returned by
488
- * `nodes.topology` (every reachable node + addons + processes).
489
- * Emitted by the hub on any agent / addon lifecycle change
490
- * (debounced) plus a periodic safety net. Replaces UI polling on
491
- * `nodes.topology` — admin-ui dashboards subscribe to drive the
492
- * cluster view directly from the event payload.
493
- */
494
- EventCategory["ClusterTopologySnapshot"] = "cluster.topology-snapshot";
495
- /**
496
- * Periodic per-node system metrics snapshot (CPU / memory / GPU /
497
- * disk / network). Emitted ~0.2 Hz by the metrics-provider addon
498
- * for each node. Drives the dashboard SystemStatus / ProcessResources
499
- * widgets without polling `metricsProvider.getCurrent`.
500
- */
501
- EventCategory["MetricsNodeResourcesSnapshot"] = "metrics.node-resources-snapshot";
502
- /**
503
- * Periodic per-node process-tree snapshot (camstack-related pids
504
- * with ghost / managed / root classification). Emitted ~0.2 Hz by
505
- * the metrics-provider addon. Drives the Cluster → Processes tab
506
- * without polling `metricsProvider.listNodeProcesses`.
507
- */
508
- EventCategory["MetricsNodeProcessesSnapshot"] = "metrics.node-processes-snapshot";
509
- /**
510
- * Capability binding change event emitted by `addon-pipeline-orchestrator`
511
- * when a user changes which addon implements a cap on a node. Subscribed
512
- * by every kernel process to update its local `preferredProviderRegistry`
513
- * so future capability lookups respect the new binding.
514
- */
515
- /**
516
- * A capability binding was changed for a node — addon X now provides
517
- * capability `cap` on node `nodeId`. Lives under the generic
518
- * `capability.*` namespace because capability bindings are a kernel-
519
- * level concept used by many addons, not strictly pipeline-scoped.
520
- */
521
- EventCategory["CapabilityBindingChanged"] = "capability.binding-changed";
522
- EventCategory["ModelDownloadProgress"] = "model.download.progress";
523
- EventCategory["AgentRegistered"] = "agent.registered";
524
- EventCategory["AgentUnregistered"] = "agent.unregistered";
525
- EventCategory["AgentOnline"] = "agent.online";
526
- EventCategory["AgentOffline"] = "agent.offline";
527
- /** Forked worker process (e.g. hub/pipeline) connected to the broker. */
528
- EventCategory["WorkerOnline"] = "worker.online";
529
- /** Forked worker process disconnected from the broker. */
530
- EventCategory["WorkerOffline"] = "worker.offline";
531
- EventCategory["AgentTaskDispatched"] = "agent.task.dispatched";
532
- EventCategory["AgentTaskAssigned"] = "agent.task.assigned";
533
- EventCategory["AgentTrpcConnected"] = "agent.trpc.connected";
534
- EventCategory["AgentWsConnected"] = "agent.ws.connected";
535
- EventCategory["AgentWsDisconnected"] = "agent.ws.disconnected";
536
- EventCategory["AgentBackupActivated"] = "agent.backup.activated";
537
- EventCategory["OrchestrationSettingsUpdated"] = "orchestration.settings-updated";
538
- /**
539
- * Per-agent hwaccel preference changed (user override set, cleared, or
540
- * re-probed). Observability event — decoders pull the current pref at
541
- * `createSession`, so running sessions keep their current backend until
542
- * they rotate naturally (camera add/remove, stream restart). Future
543
- * work can wire a listener in stream-broker that force-rotates live
544
- * sessions; for now this event powers logs + admin-UI toast feedback.
545
- */
546
- EventCategory["PipelineAgentHwaccelChanged"] = "pipeline.agent-hwaccel-changed";
547
- EventCategory["MotionAnalysis"] = "detection.motion-analysis";
548
- /** All raw motion zones from CCL before minArea filter — for UI debug overlay. */
549
- EventCategory["MotionZonesRaw"] = "detection.motion-zones-raw";
550
- /**
551
- * Per-camera motion phase transition (`watching ↔ active`) emitted
552
- * by the runner. Mirrors the `motion.onMotionChanged` cap event
553
- * surface — payload `MotionOnMotionChangedPayload` carries
554
- * `{deviceId, detected, timestamp, source, regions?}`. Subscribers
555
- * include addons that need to react to motion state without
556
- * polling the runtime-state mirror.
557
- */
558
- EventCategory["MotionOnMotionChanged"] = "motion.on-motion-changed";
559
- EventCategory["DetectionResult"] = "detection.result";
560
- EventCategory["DetectionRaw"] = "detection.raw";
561
- EventCategory["DetectionCameraNative"] = "detection.camera-native";
562
- /**
563
- * Canonical per-chunk live audio pipeline output. Payload is
564
- * `PipelineAudioInferenceResultPayload` carrying a full `AudioResult`
565
- * (level + detections + debug). Lives on the pipeline.* namespace
566
- * alongside `pipeline.inference-result` (video) for symmetry.
567
- */
568
- EventCategory["PipelineAudioInferenceResult"] = "pipeline.audio-inference-result";
569
- EventCategory["DetectionPhaseTransition"] = "detection.phase-transition";
570
- EventCategory["ProviderMotion"] = "provider.motion";
571
- EventCategory["ProviderDetection"] = "provider.detection";
572
- EventCategory["EnrichmentEmbeddingStored"] = "enrichment.embedding.stored";
573
- EventCategory["EnrichmentSceneStateChanged"] = "enrichment.scene.state-changed";
574
- EventCategory["EnrichmentActivitySummary"] = "enrichment.activity.summary";
575
- EventCategory["PipelineAnalyticsTrackStarted"] = "pipeline-analytics.track-started";
576
- EventCategory["PipelineAnalyticsTrackEnded"] = "pipeline-analytics.track-ended";
577
- EventCategory["PipelineAnalyticsDetectionEvent"] = "pipeline-analytics.detection-event";
578
- EventCategory["PipelineAnalyticsFrameTracked"] = "pipeline-analytics.frame-tracked";
579
- EventCategory["FrigateLiveEvent"] = "frigate.live-event";
580
- EventCategory["CameraStreamsProfileSlotsChanged"] = "camera-streams.onProfileSlotsChanged";
581
- /**
582
- * Stream-broker health watchdog. Per-broker (deviceId/profile) emission.
583
- * `stream.offline` fires after STREAM_STALE_TIMEOUT_MS without an encoded
584
- * packet on an active broker. `stream.online` fires on first packet
585
- * after a stale gap (or on initial first packet). Payload includes
586
- * the assigned camStreamId as `profileKey`.
587
- */
588
- EventCategory["StreamOnline"] = "stream.online";
589
- EventCategory["StreamOffline"] = "stream.offline";
590
- EventCategory["NetworkTunnelStarted"] = "network.tunnel.started";
591
- EventCategory["NetworkTunnelStopped"] = "network.tunnel.stopped";
592
- /** Fired by the `local-network` cap when the host's interface set
593
- * changes (new IP from DHCP, VPN connect, docker bridge added). */
594
- EventCategory["LocalNetworkChanged"] = "network.local.changed";
595
- /**
596
- * Fired by a `mesh-network` provider (Tailscale, …) when its
597
- * mesh-reachable host changes (join / leave / MagicDNS or 100.x IP
598
- * change). Lets `local-network` fold the mesh endpoint into its
599
- * connection-endpoint list without a cross-package import. Payload
600
- * carries the preferred host (`host: ''` = no longer reachable),
601
- * the hub port, and the scheme. Telemetry-grade (D8): consumers
602
- * pull-reconcile from the provider's `getStatus` on reconnect. */
603
- EventCategory["MeshNetworkChanged"] = "network.mesh.changed";
604
- EventCategory["BackupCompleted"] = "backup.completed";
605
- EventCategory["BackupRestored"] = "backup.restored";
606
- EventCategory["NotificationDispatched"] = "notification.dispatched";
607
- EventCategory["NotificationFailed"] = "notification.failed";
608
- EventCategory["DeviceUpdated"] = "device.updated";
609
- /**
610
- * Transport-level connectivity. Emitted by the device driver when
611
- * the underlying control socket actually connects / disconnects
612
- * (Baichuan TCP, ONVIF probe response, RTSP DESCRIBE, …) — NOT for
613
- * power-state transitions on a battery camera. For battery wake /
614
- * doze cycles see `DeviceAwake` / `DeviceSleeping`.
615
- */
616
- EventCategory["DeviceOnline"] = "device.online";
617
- /**
618
- * Stream-broker watchdog — emitted when no encoded packet has been
619
- * received for STREAM_STALE_TIMEOUT_MS on an active broker (rtsp or
620
- * push). Paired with DeviceOnline which fires on first packet after
621
- * a stale gap. Payload: DeviceStreamHealthPayload.
622
- */
623
- EventCategory["DeviceOffline"] = "device.offline";
624
- /**
625
- * Battery cam woke up — physical power-state transition reported
626
- * by the device firmware. Distinct from `DeviceOnline` so a UI panel
627
- * watching power state doesn't flap on every UDP socket reconnect.
628
- */
629
- EventCategory["DeviceAwake"] = "device.awake";
630
- /**
631
- * Battery cam went to sleep. See `DeviceAwake`.
632
- */
633
- EventCategory["DeviceSleeping"] = "device.sleeping";
634
- EventCategory["RetentionCleanup"] = "retention.cleanup";
635
- /**
636
- * Legacy bulk-update progress snapshot (payload `BulkUpdateState`). No longer
637
- * emitted — F3 removed the coordinator that produced it; "Update all" now runs
638
- * as one lifecycle engine job (`AddonsJobProgress`/`AddonsJobLog`). Retained
639
- * (with `BulkUpdateState`) only to avoid regenerating the event maps; removed
640
- * in F4 once live bulk progress is re-implemented over the engine events.
641
- */
642
- EventCategory["AddonsBulkUpdateProgress"] = "addons.bulk-update-progress";
643
- EventCategory["AddonsJobProgress"] = "addons.job-progress";
644
- EventCategory["AddonsJobLog"] = "addons.job-log";
645
- /**
646
- * A container's child visibility toggled (hidden/shown). Emitted by the
647
- * `accessories` cap when a child device is hidden or revealed.
648
- * Payload: `{ deviceId, childDeviceId, hidden }`.
649
- */
650
- EventCategory["AccessoriesChildVisibilityChanged"] = "accessories.onChildVisibilityChanged";
651
- /**
652
- * A container's child set changed (children added/removed/reordered).
653
- * Payload: `{ deviceId, childDeviceIds, hiddenChildIds }`.
654
- */
655
- EventCategory["AccessoriesChanged"] = "accessories.onAccessoriesChanged";
656
- return EventCategory;
657
- }({});
658
- //#endregion
659
- //#region src/interfaces/config-ui.ts
660
- /** Predefined tabs with standard label, icon, and sort order.
661
- *
662
- * Pipeline (renamed Orchestrator in the UI) holds the four
663
- * pipeline-orchestrator sections: General, Object Detection, Audio,
664
- * and Cluster Assignment. Motion stays its own tab.
665
- * `streaming` remains for older payloads that haven't been retagged. */
666
- var WELL_KNOWN_TABS = [
667
- {
668
- id: "overview",
669
- label: "Overview",
670
- icon: "layout-dashboard",
671
- order: -10
672
- },
673
- {
674
- id: "general",
675
- label: "General",
676
- icon: "settings",
677
- order: 0
678
- },
679
- {
680
- id: "image",
681
- label: "Image",
682
- icon: "image",
683
- order: 5
684
- },
685
- {
686
- id: "light",
687
- label: "Light",
688
- icon: "lightbulb",
689
- order: 7
690
- },
691
- {
692
- id: "motion",
693
- label: "Motion",
694
- icon: "activity",
695
- order: 10
696
- },
697
- {
698
- id: "audio",
699
- label: "Audio",
700
- icon: "mic",
701
- order: 12
702
- },
703
- {
704
- id: "alarms",
705
- label: "Alarms",
706
- icon: "bell-ring",
707
- order: 13
708
- },
709
- {
710
- id: "snapshot",
711
- label: "Snapshot",
712
- icon: "camera",
713
- order: 15
714
- },
715
- {
716
- id: "osd",
717
- label: "OSD",
718
- icon: "type",
719
- order: 18
720
- },
721
- {
722
- id: "stream-broker",
723
- label: "Stream Broker",
724
- icon: "radio",
725
- order: 20
726
- },
727
- {
728
- id: "streaming",
729
- label: "Streaming",
730
- icon: "video",
731
- order: 35
732
- },
733
- {
734
- id: "ptz",
735
- label: "PTZ",
736
- icon: "move",
737
- order: 40
738
- },
739
- {
740
- id: "consumables",
741
- label: "Consumables",
742
- icon: "recycle",
743
- order: 44
744
- },
745
- {
746
- id: "pipeline",
747
- label: "Detection Pipeline",
748
- icon: "cpu",
749
- order: 39
750
- },
751
- {
752
- id: "detection-pipeline",
753
- label: "Detection pipeline",
754
- icon: "cpu",
755
- order: 39
756
- },
757
- {
758
- id: "zones",
759
- label: "Detection",
760
- icon: "shapes",
761
- order: 38
762
- },
763
- {
764
- id: "analytics",
765
- label: "Analytics",
766
- icon: "activity",
767
- order: 37
768
- },
769
- {
770
- id: "live-stats",
771
- label: "Live Stats",
772
- icon: "activity",
773
- order: 39
774
- },
775
- {
776
- id: "recording",
777
- label: "Recording",
778
- icon: "circle-dot",
779
- order: 40
780
- },
781
- {
782
- id: "engine",
783
- label: "Inference Engine",
784
- icon: "zap",
785
- order: 41
786
- },
787
- {
788
- id: "scheduler",
789
- label: "Scheduler",
790
- icon: "list-checks",
791
- order: 42
792
- },
793
- {
794
- id: "decoder",
795
- label: "Decoder",
796
- icon: "film",
797
- order: 43
798
- },
799
- {
800
- id: "notifications",
801
- label: "Notifications",
802
- icon: "bell",
803
- order: 50
804
- },
805
- {
806
- id: "network",
807
- label: "Network",
808
- icon: "globe",
809
- order: 60
810
- },
811
- {
812
- id: "storage",
813
- label: "Storage",
814
- icon: "hard-drive",
815
- order: 70
816
- },
817
- {
818
- id: "advanced",
819
- label: "Advanced",
820
- icon: "wrench",
821
- order: 100
822
- }
823
- ];
824
- /** Lookup map for well-known tabs by ID. */
825
- var WELL_KNOWN_TAB_MAP = Object.fromEntries(WELL_KNOWN_TABS.map((t) => [t.id, t]));
826
- /**
827
- * Field types that never carry a value (separator, info, button). Used by
828
- * `hydrateSchema` to skip the `value` injection for structural-only fields.
829
- */
830
- function isValuelessField(field) {
831
- 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";
832
- }
833
- /**
834
- * Merge a `ConfigUISchema` with a raw `values` record into a
835
- * `ConfigUISchemaWithValues`. Used by every `get*Settings` backend endpoint
836
- * before returning to the admin UI. Groups are walked recursively.
837
- *
838
- * For each leaf field:
839
- * - structural fields (`separator`, `info`, `button`) pass through unchanged
840
- * - other fields get `value = values[key] ?? field.default ?? null`
841
- *
842
- * Unknown keys in `values` (keys not declared in the schema) are silently
843
- * ignored — they don't contribute to the output. Missing keys fall back to
844
- * the schema default or `null`. This mirrors the old backend behaviour where
845
- * `FormBuilder` rendered `values[key] ?? field.default`.
846
- */
847
- function hydrateSchema(schema, values) {
848
- return {
849
- ...schema.tabs ? { tabs: [...schema.tabs] } : {},
850
- sections: schema.sections.map((section) => ({
851
- ...section,
852
- fields: section.fields.map((field) => hydrateField(field, values))
853
- }))
854
- };
855
- }
856
- function hydrateField(field, values) {
857
- if (isValuelessField(field)) return field;
858
- if (field.type === "group") {
859
- const hydratedChildren = field.fields.map((child) => hydrateField(child, values));
860
- return {
861
- ...field,
862
- fields: hydratedChildren
863
- };
864
- }
865
- if (field.type === "sub-tabs") {
866
- const hydratedTabs = field.tabs.map((tab) => ({
867
- ...tab,
868
- fields: tab.fields.map((child) => hydrateField(child, values))
869
- }));
870
- return {
871
- ...field,
872
- tabs: hydratedTabs
873
- };
874
- }
875
- const key = field.key;
876
- const storedValue = Object.prototype.hasOwnProperty.call(values, key) ? values[key] : void 0;
877
- const defaultValue = field.default;
878
- if (field.multiple) {
879
- const stored = Array.isArray(storedValue) ? storedValue : storedValue !== void 0 && storedValue !== null ? [storedValue] : [];
880
- const itemFallback = field.multiple.itemDefault !== void 0 ? field.multiple.itemDefault : defaultValue !== void 0 ? defaultValue : typeOf(field) === "string" ? "" : null;
881
- const minCount = Math.max(field.multiple.min, stored.length);
882
- const items = [];
883
- for (let i = 0; i < minCount; i++) items.push(i < stored.length ? stored[i] : itemFallback);
884
- return {
885
- ...field,
886
- value: items
887
- };
888
- }
889
- const rawValue = storedValue !== void 0 ? storedValue : defaultValue !== void 0 ? defaultValue : null;
890
- if (field.type === "password") return {
891
- ...field,
892
- value: ""
893
- };
894
- const value = field.type === "textarea" && field.isJson && rawValue !== null && typeof rawValue === "object" ? JSON.stringify(rawValue, null, 2) : rawValue;
895
- return {
896
- ...field,
897
- value
898
- };
899
- }
900
- /**
901
- * Rough "value family" classifier used by `hydrateField`'s multiple
902
- * fallback to pick a sensible zero-value when no `itemDefault` / no
903
- * field `default` / no stored value is available.
904
- */
905
- function typeOf(field) {
906
- switch (field.type) {
907
- case "text":
908
- case "textarea":
909
- case "password":
910
- case "color":
911
- case "probe":
912
- case "timezone":
913
- case "datetime": return "string";
914
- case "number":
915
- case "slider": return "number";
916
- case "boolean": return "boolean";
917
- default: return "other";
918
- }
919
- }
920
- //#endregion
921
- //#region src/interfaces/event-bus.ts
922
- /**
923
- * Narrow a SystemEvent to a typed event by checking its category.
924
- * Returns `true` (and narrows the type) if the category matches.
925
- *
926
- * @example
927
- * ```typescript
928
- * eventBus.subscribe({ category: 'addon.started' }, (event) => {
929
- * if (isEvent(event, 'addon.started')) {
930
- * event.data.addonId // ✓ typed as string
931
- * }
932
- * })
933
- * ```
934
- */
935
- function isEvent(event, category) {
936
- return event.category === category;
937
- }
938
- /**
939
- * Create a typed event with less boilerplate.
940
- *
941
- * @example
942
- * ```typescript
943
- * eventBus.emit(createEvent('addon.started', { type: 'addon', id: 'pipeline' }, {
944
- * addonId: 'pipeline',
945
- * packageVersion: '0.1.8',
946
- * }))
947
- * ```
948
- */
949
- function createEvent(category, source, data) {
950
- return {
951
- id: typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2),
952
- timestamp: /* @__PURE__ */ new Date(),
953
- source,
954
- category,
955
- data
956
- };
957
- }
958
- /**
959
- * Emit a `system.ready-state` event for a capability. Caller supplies a
960
- * per-process `generation` string — stable across a single process
961
- * lifetime, changes on restart — which consumer-side registries use to
962
- * derive a monotonic `epoch` without requiring the emitter to
963
- * coordinate.
964
- */
965
- function emitReadiness(bus, params) {
966
- const ts = params.ts ?? Date.now();
967
- bus.emit(createEvent("system.ready-state", {
968
- type: "capability",
969
- id: params.capName,
970
- nodeId: params.sourceNodeId
971
- }, {
972
- capName: params.capName,
973
- scope: params.scope,
974
- state: params.state,
975
- generation: params.generation,
976
- sourceNodeId: params.sourceNodeId,
977
- ts
978
- }));
979
- }
980
- //#endregion
981
- //#region src/addon/durable-state.ts
982
- /**
983
- * Build a {@link DurableState} over a single store key. Transport-agnostic:
984
- * `read`/`write` are the addon-store or device-store accessors. The whole
985
- * validated value round-trips on every read/write — the schema is the
986
- * single source of truth for what is persisted, so no hand-listed field
987
- * set can drop a key on save.
988
- */
989
- function createDurableState(deps) {
990
- const get = async () => {
991
- const raw = (await deps.read())[deps.key];
992
- if (raw === void 0) return deps.fallback;
993
- const parsed = deps.schema.safeParse(raw);
994
- if (!parsed.success) {
995
- deps.onParseError?.(deps.key, parsed.error);
996
- return deps.fallback;
997
- }
998
- return parsed.data;
999
- };
1000
- const set = async (next) => {
1001
- const validated = deps.schema.parse(next);
1002
- await deps.write({ [deps.key]: validated });
1003
- };
1004
- const update = async (fn) => {
1005
- await set(fn(await get()));
1006
- };
1007
- return {
1008
- get,
1009
- set,
1010
- update
1011
- };
1012
- }
1013
- //#endregion
1014
- //#region src/addon/base-addon.ts
1015
- /**
1016
- * Base class for CamStack addons. Eliminates settings boilerplate:
1017
- *
1018
- * - Typed `config` property with automatic resolution from store + defaults
1019
- * - `getGlobalSettings()` / `updateGlobalSettings()` auto-implemented
1020
- * - `getAddonSettings()` / `updateAddonSettings()` auto-implemented
1021
- * - `getDeviceSettings()` / `updateDeviceSettings()` auto-implemented
1022
- * - `ctx` accessor for the AddonContext (no manual `this.ctxRef` storage)
1023
- * - `field()` helper that constrains field keys to TConfig keys
1024
- * - `schema()` helper that validates the full schema structure
1025
- *
1026
- * Subclasses override:
1027
- * - `onInitialize()` — addon-specific init logic, return ProviderRegistration[]
1028
- * - `onShutdown()` — cleanup
1029
- * - `globalSettingsSchema()` / `deviceSettingsSchema()` — UI schemas
1030
- *
1031
- * @example
1032
- * ```ts
1033
- * interface MyConfig {
1034
- * maxRetries: number
1035
- * endpoint: string
1036
- * }
1037
- *
1038
- * export default class MyAddon extends BaseAddon<MyConfig> {
1039
- * constructor() {
1040
- * super({ maxRetries: 3, endpoint: 'http://localhost' })
1041
- * }
1042
- *
1043
- * protected globalSettingsSchema() {
1044
- * return this.schema({
1045
- * sections: [{
1046
- * id: 'main', title: 'Settings',
1047
- * fields: [
1048
- * this.field({ type: 'number', key: 'maxRetries', label: 'Max Retries', default: 3 }),
1049
- * this.field({ type: 'text', key: 'endpoint', label: 'Endpoint' }),
1050
- * ],
1051
- * }],
1052
- * })
1053
- * }
1054
- *
1055
- * protected async onInitialize(): Promise<ProviderRegistration[]> {
1056
- * const client = new Client(this.config.endpoint, this.config.maxRetries)
1057
- * return [{ capability: 'my-feature', provider: client }]
1058
- * }
1059
- * }
1060
- * ```
1061
- */
1062
- var BaseAddon = class {
1063
- _ctx = null;
1064
- _config;
1065
- /**
1066
- * Per-process random id used as the `generation` stamp on every
1067
- * `system.ready-state` event this addon emits. Constant for the
1068
- * lifetime of this addon instance (== one process boot); consumer-
1069
- * side registries derive a monotonic `epoch` by watching for
1070
- * generation transitions.
1071
- */
1072
- _readinessGeneration = typeof crypto !== "undefined" && crypto.randomUUID ? crypto.randomUUID() : Math.random().toString(36).slice(2, 14);
1073
- /** Capability names this addon registered at init — used to emit matching `down` events on shutdown. */
1074
- _registeredCapNames = [];
1075
- /** Default config values. Provided via constructor. */
1076
- defaults;
1077
- constructor(defaults) {
1078
- this.defaults = defaults;
1079
- this._config = { ...defaults };
1080
- }
1081
- /**
1082
- * Override to opt out of the automatic `system.ready-state` emission.
1083
- * Returns `true` by default — addons that don't map cleanly to the
1084
- * readiness protocol (e.g. pure collection providers whose readiness
1085
- * is already reported per-device by upstream) can override to `false`
1086
- * and emit manually.
1087
- */
1088
- get autoEmitReadiness() {
1089
- return true;
1090
- }
1091
- /** The AddonContext, available after initialize(). */
1092
- get ctx() {
1093
- if (!this._ctx) throw new Error(`${this.constructor.name}: ctx accessed before initialize()`);
1094
- return this._ctx;
1095
- }
1096
- /**
1097
- * Non-throwing ctx accessor for code paths that can legitimately run
1098
- * before `initialize()` has resolved — most commonly cap handlers the
1099
- * hub invokes eagerly at page load (device-details aggregator pings
1100
- * every addon's `getDeviceSettingsContribution` as soon as the page
1101
- * mounts). Prefer `ctx` for the normal case; reach for this only in
1102
- * capability methods that may be queried before the addon is wired up.
1103
- */
1104
- get ctxIfReady() {
1105
- return this._ctx;
1106
- }
1107
- /** Current resolved config (defaults merged with persisted store values). */
1108
- get config() {
1109
- return this._config;
1110
- }
1111
- async initialize(context) {
1112
- this._ctx = context;
1113
- await this.resolveConfig();
1114
- const result = await this.onInitialize();
1115
- this.emitLifecycle(EventCategory.AddonStarted);
1116
- const normalized = normalizeAddonInitResult(result);
1117
- const providers = normalized && "providers" in normalized && normalized.providers ? normalized.providers : [];
1118
- this._registeredCapNames = providers.map((p) => p.capability.name);
1119
- return normalized;
1120
- }
1121
- /**
1122
- * Called by the isolated-process runner AFTER `broker.start()`.
1123
- * In-process addons never need this because the hub broker is already
1124
- * running when `initialize()` fires. For forked children the broker
1125
- * starts AFTER `initialize()`, so the readiness emit inside initialize()
1126
- * fires before the broker can broadcast it. This method re-emits the
1127
- * ready state once the transport is live.
1128
- */
1129
- postBrokerStart() {
1130
- if (this.autoEmitReadiness && this._registeredCapNames.length > 0) this.emitReadinessForProviders("ready");
1131
- }
1132
- /**
1133
- * Called by the isolated-process runner / agent bootstrap once the broker
1134
- * has started AND the hub node is connected — the moment `ctx.api.*` calls
1135
- * to hub-provided capabilities become safe. Wrap any bootstrap work that
1136
- * must query the hub (device restore, settings fetch, initial sync) in
1137
- * `onHubConnected()` rather than `onInitialize()` — during `initialize()`
1138
- * on a worker the broker is not yet connected and remote cap calls either
1139
- * time out or throw "Service not found".
1140
- *
1141
- * Default implementation is a no-op. Override in subclasses that need it.
1142
- * Never fires on hub-local in-process addons (the hub is its own node).
1143
- */
1144
- async onHubReachable() {}
1145
- async shutdown() {
1146
- this.emitLifecycle(EventCategory.AddonStopped);
1147
- if (this.autoEmitReadiness) this.emitReadinessForProviders("down");
1148
- await this.onShutdown();
1149
- for (const unsub of this._subscriptions) unsub();
1150
- this._subscriptions = [];
1151
- this._ctx = null;
1152
- }
1153
- /** Addon-specific cleanup. Override if needed. */
1154
- async onShutdown() {}
1155
- /**
1156
- * Called after config is resolved during updateGlobalSettings/updateAddonSettings.
1157
- * Override to react to config changes (e.g. restart a sampler, reconnect a service).
1158
- * Not called during initialize() — use onInitialize() for initial setup.
1159
- */
1160
- async onConfigChanged() {}
1161
- /**
1162
- * Create a ConfigField with `key` constrained to keys of TConfig.
1163
- * Provides autocomplete and compile-time validation.
1164
- */
1165
- field(field) {
1166
- return field;
1167
- }
1168
- /**
1169
- * Create a full ConfigUISchema with typed sections.
1170
- * Fields created via `this.field()` get key validation automatically.
1171
- */
1172
- schema(schema) {
1173
- return schema;
1174
- }
1175
- /** Override to provide global-level settings UI schema. */
1176
- globalSettingsSchema(_cap) {
1177
- return null;
1178
- }
1179
- /** Override to provide device-level settings UI schema. */
1180
- deviceSettingsSchema() {
1181
- return null;
1182
- }
1183
- async getGlobalSettings(overlay, cap) {
1184
- const schema = this.globalSettingsSchema(cap);
1185
- if (!schema) return { sections: [] };
1186
- const raw = await this._ctx?.settings?.readAddonStore() ?? {};
1187
- return hydrateSchema(schema, overlay ? {
1188
- ...raw,
1189
- ...overlay
1190
- } : raw);
1191
- }
1192
- async updateGlobalSettings(patch) {
1193
- await this._ctx?.settings?.writeAddonStore(patch);
1194
- await this.resolveConfig();
1195
- await this.onConfigChanged();
1196
- this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
1197
- this.maybeAutoRestart(patch, this.globalSettingsSchema());
1198
- }
1199
- /**
1200
- * If any field in `patch` is marked `requiresRestart` in `schema`,
1201
- * schedule an addon restart for the next tick. Deferred via
1202
- * `setImmediate` so the tRPC mutation that triggered the write has
1203
- * time to return its response before the addon is torn down and
1204
- * re-initialised by `AddonRegistryService.restartAddon`.
1205
- */
1206
- maybeAutoRestart(patch, schema) {
1207
- if (!schema) return;
1208
- const restartKeys = /* @__PURE__ */ new Set();
1209
- for (const section of schema.sections) for (const field of section.fields) {
1210
- if (field.type === "separator" || field.type === "info") continue;
1211
- if (field.requiresRestart) restartKeys.add(field.key);
1212
- }
1213
- if (restartKeys.size === 0) return;
1214
- if (!Object.keys(patch).some((k) => restartKeys.has(k))) return;
1215
- const ctx = this._ctx;
1216
- if (!ctx) return;
1217
- const addonId = ctx.id;
1218
- setImmediate(() => {
1219
- ctx.api.addons?.restartAddon?.mutate({ addonId }).then(() => {
1220
- ctx.logger.info("addon auto-restart triggered by restart-required setting change", { meta: { changedFields: Object.keys(patch).filter((k) => restartKeys.has(k)) } });
1221
- }).catch((err) => {
1222
- ctx.logger.error("addon auto-restart failed", { meta: { error: err instanceof Error ? err.message : String(err) } });
1223
- });
1224
- });
1225
- }
1226
- async getDeviceSettings(deviceId) {
1227
- const schema = this.deviceSettingsSchema();
1228
- if (!schema) return { sections: [] };
1229
- return hydrateSchema(schema, await this._ctx?.settings?.readDeviceStore(deviceId) ?? {});
1230
- }
1231
- async updateDeviceSettings(deviceId, patch) {
1232
- await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
1233
- }
1234
- _subscriptions = [];
1235
- /**
1236
- * Subscribe to an event bus category. The subscription is automatically
1237
- * unsubscribed on shutdown — no manual cleanup needed.
1238
- *
1239
- * @example
1240
- * ```ts
1241
- * this.subscribe({ category: EventCategory.DeviceRegistered }, (event) => {
1242
- * void this.handleDevice(event)
1243
- * })
1244
- * ```
1245
- */
1246
- /**
1247
- * Subscribe to `system.ready-state` events for one or more capabilities.
1248
- * Abstracts the boilerplate type-narrowing + nodeId extraction that every
1249
- * resilience subscription duplicates. Cleanup is automatic (registered on
1250
- * `_subscriptions` like a normal `subscribe` call).
1251
- */
1252
- watchCapability(capNames, handlers) {
1253
- const nameSet = new Set(Array.isArray(capNames) ? capNames : [capNames]);
1254
- this.subscribe({ category: "system.ready-state" }, (event) => {
1255
- const data = event.data;
1256
- if (typeof data.capName !== "string") return;
1257
- if (!nameSet.has(data.capName)) return;
1258
- if (data.scope?.type !== "node") return;
1259
- const nodeId = data.scope.nodeId;
1260
- if (typeof nodeId !== "string" || nodeId.length === 0) return;
1261
- const capName = data.capName;
1262
- if (data.state === "down") handlers.onDown?.(nodeId, capName);
1263
- else if (data.state === "ready") handlers.onReady?.(nodeId, capName);
1264
- });
1265
- }
1266
- subscribe(filter, handler) {
1267
- const unsub = this.ctx.eventBus.subscribe(filter, handler);
1268
- this._subscriptions.push(unsub);
1269
- }
1270
- emitLifecycle(category, data) {
1271
- try {
1272
- const ctx = this._ctx;
1273
- if (!ctx) return;
1274
- ctx.eventBus.emit({
1275
- id: `${ctx.id}-${Date.now()}`,
1276
- timestamp: /* @__PURE__ */ new Date(),
1277
- source: {
1278
- type: "addon",
1279
- id: ctx.id,
1280
- nodeId: ctx.kernel.localNodeId ?? "hub"
1281
- },
1282
- category,
1283
- data: {
1284
- addonId: ctx.id,
1285
- ...data
1286
- }
1287
- });
1288
- } catch {}
1289
- }
1290
- /**
1291
- * Emit a `system.ready-state` event for every capability this addon
1292
- * registered at init. Scope is `{type:'node', nodeId}` — readiness is
1293
- * tied to the node that hosts the provider. Consumers that care about
1294
- * per-device readiness can subscribe with `{type:'device', ...}` if
1295
- * the provider emits at finer granularity (collection addons may
1296
- * opt-out via `autoEmitReadiness` and emit manually).
1297
- */
1298
- emitReadinessForProviders(state) {
1299
- const ctx = this._ctx;
1300
- if (!ctx) return;
1301
- if (this._registeredCapNames.length === 0) return;
1302
- const rawNodeId = ctx.kernel?.localNodeId ?? "hub";
1303
- const nodeId = rawNodeId.includes("/") ? rawNodeId.split("/")[0] : rawNodeId;
1304
- for (const capName of this._registeredCapNames) try {
1305
- emitReadiness(ctx.eventBus, {
1306
- capName,
1307
- scope: {
1308
- type: "node",
1309
- nodeId
1310
- },
1311
- state,
1312
- generation: this._readinessGeneration,
1313
- sourceNodeId: nodeId
1314
- });
1315
- } catch {}
1316
- }
1317
- /**
1318
- * Resolve the shared models directory path via the storage capability.
1319
- * Falls back to `camstack-data/models` if storage is unavailable.
1320
- * Used by inference addons (detection-pipeline, audio-classifier, embedding-encoder).
1321
- */
1322
- async resolveModelsDir() {
1323
- return this.ctx.api.storage.resolve.query({
1324
- location: "models",
1325
- relativePath: ""
1326
- }).catch(() => "camstack-data/models");
1327
- }
1328
- /**
1329
- * Access the runtime capability registry for in-process provider lookups.
1330
- * Returns null if the registry is not available (e.g. on agents).
1331
- * Used by addons that consume other capabilities directly (snapshot, stream-broker, enrichment).
1332
- */
1333
- get capabilities() {
1334
- return this.ctx.capabilities ?? null;
1335
- }
1336
- /**
1337
- * Resolve config by merging defaults with persisted store values.
1338
- * Called automatically during initialize() and after every updateSettings().
1339
- *
1340
- * The merge is shallow: each key in `defaults` is checked against the store.
1341
- * Only keys present in defaults are read — the store can contain extra keys
1342
- * (e.g. from older versions) without polluting the typed config.
1343
- */
1344
- async resolveConfig() {
1345
- const stored = await this.readAddonStoreWithRetry();
1346
- const resolved = { ...this.defaults };
1347
- for (const key of Object.keys(this.defaults)) {
1348
- const storedValue = stored[key];
1349
- if (storedValue !== void 0 && storedValue !== null) {
1350
- const defaultType = typeof this.defaults[key];
1351
- if (typeof storedValue === defaultType) resolved[key] = storedValue;
1352
- }
1353
- }
1354
- this._config = resolved;
1355
- }
1356
- /**
1357
- * Typed durable handle over ONE key of this addon's store. The whole
1358
- * Zod-validated value round-trips on every read/write — no hand-listed
1359
- * fields, so a field can never be silently dropped on persist. Reads use
1360
- * the same retry budget as config resolution; a corrupt/legacy blob logs
1361
- * a warning and falls back rather than crashing boot.
1362
- */
1363
- state(key, schema, fallback) {
1364
- return createDurableState({
1365
- key,
1366
- schema,
1367
- fallback,
1368
- read: () => this.readAddonStoreWithRetry(),
1369
- write: async (patch) => {
1370
- await this._ctx?.settings?.writeAddonStore(patch);
1371
- },
1372
- onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
1373
- addonId: this._ctx?.id,
1374
- key: k,
1375
- error: String(e)
1376
- } })
1377
- });
1378
- }
1379
- /** Per-device variant of {@link state}, backed by the per-device store. */
1380
- deviceState(deviceId, key, schema, fallback) {
1381
- return createDurableState({
1382
- key,
1383
- schema,
1384
- fallback,
1385
- read: async () => await this._ctx?.settings?.readDeviceStore(deviceId) ?? {},
1386
- write: async (patch) => {
1387
- await this._ctx?.settings?.writeDeviceStore(deviceId, patch);
1388
- },
1389
- onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored device ${deviceId} "${k}" failed validation — using fallback`, { meta: {
1390
- addonId: this._ctx?.id,
1391
- deviceId,
1392
- key: k,
1393
- error: String(e)
1394
- } })
1395
- });
1396
- }
1397
- /**
1398
- * Wrap `ctx.settings.readAddonStore()` with a short retry budget so a
1399
- * transient settings-store outage (mid-restart of sqlite-settings, tsx-watch
1400
- * swap, or any race where the SqliteSettingsBackend is between shutdown and
1401
- * re-initialize) doesn't propagate up into `initialize()` and leave the
1402
- * addon permanently broken.
1403
- *
1404
- * Retry only the two known infra fingerprints. Anything else propagates so
1405
- * real bugs surface immediately. After the budget expires we fall back to
1406
- * `{}` (defaults) — the addon's first successful patch will rehydrate.
1407
- */
1408
- async readAddonStoreWithRetry() {
1409
- const settings = this._ctx?.settings;
1410
- if (!settings) return {};
1411
- const delaysMs = [
1412
- 150,
1413
- 350,
1414
- 600,
1415
- 900
1416
- ];
1417
- let lastErr;
1418
- for (let attempt = 0; attempt <= delaysMs.length; attempt++) try {
1419
- return await settings.readAddonStore() ?? {};
1420
- } catch (err) {
1421
- lastErr = err;
1422
- const msg = err instanceof Error ? err.message : String(err);
1423
- if (!(msg.includes("SqliteSettingsBackend not initialized") || msg.includes("provider not available"))) throw err;
1424
- if (attempt === delaysMs.length) break;
1425
- await new Promise((r) => setTimeout(r, delaysMs[attempt]));
1426
- }
1427
- this._ctx?.logger?.warn?.("readAddonStore: settings-store unavailable after retries — using defaults", { meta: { error: lastErr instanceof Error ? lastErr.message : String(lastErr) } });
1428
- return {};
1429
- }
1430
- };
1431
- /**
1432
- * Normalize an `ICamstackAddon.initialize()` return value into the
1433
- * `AddonInitResult` envelope. Arrays are wrapped into `{ providers }`;
1434
- * envelopes pass through; void stays void.
89
+ * Convenience accessor — the declared co-location group label, or
90
+ * `undefined` when the addon declares no group (→ its own dedicated
91
+ * runner). Callers that need the runner id should use `resolveRunnerId`.
92
+ */
93
+ function resolveAddonGroup(decl) {
94
+ return resolveAddonExecution(decl).group;
95
+ }
96
+ /**
97
+ * Resolve the runner id an addon belongs to. This is the single
98
+ * authority for the hub↔runner topology:
99
+ * - declared `group` present → the runner id is the group name
100
+ * (shared with every other addon declaring the same group);
101
+ * - no `group` declared → the runner id is the addon id itself
102
+ * (its own dedicated runner — one addon, one process).
1435
103
  *
1436
- * Exported so the kernel boot path and the backend addon registry can
1437
- * share the same normalizer instead of duplicating the shim. Addons that
1438
- * extend `BaseAddon` already emit the envelope, so this helper is only a
1439
- * safety net for direct `ICamstackAddon` implementations (mostly tests).
104
+ * The runner's Moleculer nodeID is `${parentNodeId}/${runnerId}`.
1440
105
  */
1441
- function normalizeAddonInitResult(result) {
1442
- if (result == null) return;
1443
- if (Array.isArray(result)) return { providers: result };
1444
- return result;
106
+ function resolveRunnerId(decl, addonId) {
107
+ return decl.execution?.group ?? addonId;
108
+ }
109
+ /** Convenience accessor — the resolved placement (defaults to `hub-only`). */
110
+ function resolveAddonPlacement(decl) {
111
+ return resolveAddonExecution(decl).placement;
1445
112
  }
1446
113
  //#endregion
1447
114
  //#region src/addon/build-addon-route-provider.ts
@@ -1655,465 +322,90 @@ var TIMEZONES = [
1655
322
  dst: US_DST
1656
323
  },
1657
324
  {
1658
- id: "America/Chicago",
1659
- label: "America/Chicago (UTC−6 / CDT)",
1660
- region: "Americas",
1661
- stdOffsetMinutes: -360,
1662
- dst: US_DST
1663
- },
1664
- {
1665
- id: "America/Denver",
1666
- label: "America/Denver (UTC−7 / MDT)",
1667
- region: "Americas",
1668
- stdOffsetMinutes: -420,
1669
- dst: US_DST
1670
- },
1671
- {
1672
- id: "America/Los_Angeles",
1673
- label: "America/Los Angeles (UTC−8 / PDT)",
1674
- region: "Americas",
1675
- stdOffsetMinutes: -480,
1676
- dst: US_DST
1677
- },
1678
- {
1679
- id: "America/Sao_Paulo",
1680
- label: "America/Sao Paulo (UTC−3)",
1681
- region: "Americas",
1682
- stdOffsetMinutes: -180,
1683
- dst: null
1684
- },
1685
- {
1686
- id: "Asia/Dubai",
1687
- label: "Asia/Dubai (UTC+4)",
1688
- region: "Asia",
1689
- stdOffsetMinutes: 240,
1690
- dst: null
1691
- },
1692
- {
1693
- id: "Asia/Kolkata",
1694
- label: "Asia/Kolkata (UTC+5:30)",
1695
- region: "Asia",
1696
- stdOffsetMinutes: 330,
1697
- dst: null
1698
- },
1699
- {
1700
- id: "Asia/Singapore",
1701
- label: "Asia/Singapore (UTC+8)",
1702
- region: "Asia",
1703
- stdOffsetMinutes: 480,
1704
- dst: null
1705
- },
1706
- {
1707
- id: "Asia/Shanghai",
1708
- label: "Asia/Shanghai (UTC+8)",
1709
- region: "Asia",
1710
- stdOffsetMinutes: 480,
1711
- dst: null
1712
- },
1713
- {
1714
- id: "Asia/Tokyo",
1715
- label: "Asia/Tokyo (UTC+9)",
1716
- region: "Asia",
1717
- stdOffsetMinutes: 540,
1718
- dst: null
1719
- },
1720
- {
1721
- id: "Australia/Sydney",
1722
- label: "Australia/Sydney (UTC+10 / AEDT)",
1723
- region: "Oceania",
1724
- stdOffsetMinutes: 600,
1725
- dst: {
1726
- offsetHours: 1,
1727
- startMonth: 10,
1728
- startWeekIndex: 1,
1729
- startWeekday: "Sunday",
1730
- startHour: 2,
1731
- endMonth: 4,
1732
- endWeekIndex: 1,
1733
- endWeekday: "Sunday",
1734
- endHour: 3
1735
- }
1736
- }
1737
- ];
1738
- /** Resolve an IANA id to its `Timezone`, or `undefined` if unknown. */
1739
- function findTimezone(id) {
1740
- return TIMEZONES.find((tz) => tz.id === id);
1741
- }
1742
- //#endregion
1743
- //#region src/capabilities/schemas/streaming-shared.ts
1744
- /** Shared Zod schemas used across streaming capabilities. */
1745
- var CamProfileSchema = z.enum([
1746
- "high",
1747
- "mid",
1748
- "low"
1749
- ]);
1750
- /** Canonical ordering. Hard-coded; never sorted. */
1751
- var CAM_PROFILE_ORDER = [
1752
- "high",
1753
- "mid",
1754
- "low"
1755
- ];
1756
- var CamStreamKindSchema = z.enum([
1757
- "pull-rtsp",
1758
- "pull-rtmp",
1759
- "pull-http",
1760
- "pull-rfc4571",
1761
- "push-annexb",
1762
- "derived"
1763
- ]);
1764
- var CamStreamResolutionSchema = z.object({
1765
- width: z.number().int().positive(),
1766
- height: z.number().int().positive()
1767
- });
1768
- var CameraStreamSchema = z.object({
1769
- /** Stable, provider-assigned id unique within the (deviceId) scope. */
1770
- camStreamId: z.string().min(1),
1771
- deviceId: z.number().int().nonnegative(),
1772
- kind: CamStreamKindSchema,
1773
- /** Required for pull-* kinds. Ignored for push-annexb. */
1774
- url: z.string().optional(),
1775
- codec: z.string().optional(),
1776
- resolution: CamStreamResolutionSchema.optional(),
1777
- fps: z.number().positive().optional(),
1778
- /** Human label surfaced in the Admin UI "Camera Stream" dropdown. */
1779
- label: z.string().optional(),
1780
- /**
1781
- * Device-level features the publisher advertised (e.g. `battery-operated`).
1782
- * The broker, snapshot orchestrator, and prebuffer manager all consult
1783
- * this list to derive policy — relaxed stall watchdog for battery
1784
- * cams, prebuffer off by default, longer snapshot rate-limit, etc.
1785
- *
1786
- * Single source of truth replacing per-stream flags like the
1787
- * historical `allowStall`: if the publisher knows the camera is
1788
- * battery-powered, every downstream service derives the right policy
1789
- * from this list.
1790
- */
1791
- deviceFeatures: z.array(z.string()).optional(),
1792
- /**
1793
- * Whether this stream participates in the broker's automatic profile
1794
- * assignment (`computeInitialAssignment`). Defaults to `true`. Publishers
1795
- * use `false` when they want a stream to be SELECTABLE in the UI but not
1796
- * picked by default — e.g. Reolink publishes its native Baichuan streams
1797
- * as `autoEligible: true` (the recommended path) and its RTSP / RTMP
1798
- * mirrors as `autoEligible: false` (still pickable per slot, just not
1799
- * the auto choice). Manual `assignProfile` calls remain valid for
1800
- * non-eligible streams.
1801
- */
1802
- autoEligible: z.boolean().optional(),
1803
- /**
1804
- * Transport-specific opaque metadata. The broker passes it through to
1805
- * the source reader without inspecting it. Currently used by
1806
- * `pull-rfc4571` streams to carry the upstream SDP (so the reader can
1807
- * route RTP packets to the right depacketizer without an in-band
1808
- * DESCRIBE phase). Other kinds typically leave it undefined.
1809
- */
1810
- metadata: z.record(z.string(), z.unknown()).optional()
1811
- });
1812
- var ProfileSlotStatusSchema = z.enum([
1813
- "unassigned",
1814
- "idle",
1815
- "connecting",
1816
- "streaming",
1817
- "error"
1818
- ]);
1819
- var ProfileSlotSchema = z.object({
1820
- deviceId: z.number().int().nonnegative(),
1821
- profile: CamProfileSchema,
1822
- /** Broker id the rest of the system addresses: `${deviceId}/${profile}`. */
1823
- brokerId: z.string(),
1824
- /** `null` when the profile is unassigned. */
1825
- sourceCamStreamId: z.string().nullable(),
1826
- status: ProfileSlotStatusSchema,
1827
- resolution: CamStreamResolutionSchema.optional(),
1828
- codec: z.string().optional(),
1829
- preBufferSec: z.number().nonnegative().optional(),
1830
- errorMessage: z.string().optional()
1831
- });
1832
- /** The canonical consumer-facing broker id for a device profile. */
1833
- function makeProfileBrokerId(deviceId, profile) {
1834
- return `${deviceId}/${profile}`;
1835
- }
1836
- /**
1837
- * The broker id keying a physical SOURCE stream: `${deviceId}/${camStreamId}`.
1838
- * This is the broker's internal source key (`brokerIdFor` in the manager) — a
1839
- * `camStreamId` like `native:main` — as opposed to the public profile alias
1840
- * {@link makeProfileBrokerId}. Used to resolve a source's restream endpoint
1841
- * (e.g. routing a transcode's ffmpeg input through the broker so it rides the
1842
- * single source dial instead of opening a second camera connection).
1843
- */
1844
- function makeSourceBrokerId(deviceId, camStreamId) {
1845
- return `${deviceId}/${camStreamId}`;
1846
- }
1847
- /**
1848
- * Inverse of {@link makeProfileBrokerId}. Returns `null` when the string is
1849
- * not a canonical `${deviceId}/${profile}` id — e.g. a broker-internal
1850
- * cam-stream id (`5/native:main`), a derived/adaptive ref, or a malformed
1851
- * value. So a caller can safely distinguish "addresses a public profile"
1852
- * from "addresses something broker-internal".
1853
- */
1854
- function parseProfileBrokerId(brokerId) {
1855
- const slash = brokerId.indexOf("/");
1856
- if (slash <= 0) return null;
1857
- const idPart = brokerId.slice(0, slash);
1858
- const profilePart = brokerId.slice(slash + 1);
1859
- if (!/^\d+$/.test(idPart)) return null;
1860
- const parsed = CamProfileSchema.safeParse(profilePart);
1861
- if (!parsed.success) return null;
1862
- return {
1863
- deviceId: Number(idPart),
1864
- profile: parsed.data
1865
- };
1866
- }
1867
- /**
1868
- * The profile slots worth consuming/recording: ASSIGNED only
1869
- * (`sourceCamStreamId != null`), DEDUPED by physical source so the same
1870
- * camera encoder is never subscribed/recorded twice, ordered high→mid→low.
1871
- * Optionally scoped to one `deviceId`. Pure; never mutates the input.
1872
- *
1873
- * Recording `main` and `mid` when both point at the same `sourceCamStreamId`
1874
- * would dial the camera's internal stream twice for no benefit — this is the
1875
- * dedup that prevents that.
1876
- */
1877
- function selectAssignedProfileSlots(slots, deviceId) {
1878
- 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));
1879
- const seenSources = /* @__PURE__ */ new Set();
1880
- const out = [];
1881
- for (const s of ordered) {
1882
- const src = s.sourceCamStreamId;
1883
- if (src === null) continue;
1884
- if (seenSources.has(src)) continue;
1885
- seenSources.add(src);
1886
- out.push(s);
1887
- }
1888
- return out;
1889
- }
1890
- /**
1891
- * Zod schema for StreamSourceEntry — the canonical stream descriptor
1892
- * exposed by ICameraDevice.getStreamSources() and consumed by the broker.
1893
- */
1894
- var StreamSourceEntrySchema = z.object({
1895
- id: z.string(),
1896
- label: z.string(),
1897
- protocol: z.enum([
1898
- "rtsp",
1899
- "rtmp",
1900
- "annexb",
1901
- "http-mjpeg",
1902
- "webrtc",
1903
- "custom"
1904
- ]),
1905
- url: z.string().optional(),
1906
- resolution: z.object({
1907
- width: z.number(),
1908
- height: z.number()
1909
- }).readonly().optional(),
1910
- fps: z.number().optional(),
1911
- bitrate: z.number().optional(),
1912
- codec: z.string().optional(),
1913
- profileHint: CamProfileSchema.optional()
1914
- });
1915
- var StreamSourceSchema = z.object({
1916
- type: z.string(),
1917
- url: z.string(),
1918
- videoCodec: z.string().optional(),
1919
- audioCodec: z.string().optional(),
1920
- metadata: z.record(z.string(), z.unknown()).readonly().optional()
1921
- });
1922
- var EncodedPacketSchema = z.object({
1923
- type: z.enum(["video", "audio"]),
1924
- data: z.instanceof(Uint8Array),
1925
- pts: z.number(),
1926
- dts: z.number(),
1927
- keyframe: z.boolean(),
1928
- codec: z.string()
1929
- });
1930
- var DecodedFrameSchema = z.object({
1931
- data: z.instanceof(Uint8Array),
1932
- width: z.number(),
1933
- height: z.number(),
1934
- format: z.enum([
1935
- "jpeg",
1936
- "rgb",
1937
- "bgr",
1938
- "yuv420",
1939
- "gray"
1940
- ]),
1941
- timestamp: z.number()
1942
- });
1943
- /**
1944
- * Wire schema for `FrameHandle` — a zero-pixel, serialisable reference to a
1945
- * frame in a shared-memory ring (Phase 5 / D9). Mirrors the `FrameHandle` TS
1946
- * interface in `interfaces/frame-handle.ts` field-for-field so a decoder cap
1947
- * method (`pullHandles`) can carry handles over tRPC / Moleculer.
1948
- *
1949
- * The `satisfies` assertion below pins this schema to the interface: if a
1950
- * field is added to / removed from `FrameHandle` without a matching schema
1951
- * edit, `z.infer<typeof FrameHandleSchema>` stops being assignable to
1952
- * `FrameHandle` and the build fails — no silent drift.
1953
- */
1954
- var FrameHandleSchema = z.object({
1955
- shmId: z.string(),
1956
- slot: z.number().int().nonnegative(),
1957
- seq: z.number().int().nonnegative(),
1958
- width: z.number().int().positive(),
1959
- height: z.number().int().positive(),
1960
- format: z.enum([
1961
- "jpeg",
1962
- "rgb",
1963
- "bgr",
1964
- "yuv420",
1965
- "gray"
1966
- ]),
1967
- pts: z.number(),
1968
- byteLength: z.number().int().nonnegative(),
1969
- nodeId: z.string(),
1970
- slotCount: z.number().int().positive()
1971
- });
1972
- /**
1973
- * Pixel format a frame-handle subscriber can request from the broker
1974
- * (Phase 5 / D9). The packed, raster subset of `FrameFormat` — the formats a
1975
- * `frameSink: 'shm'` decoder session can write into a `FrameRing` slot.
1976
- * `jpeg` is excluded: a variable-length stream is not a fixed-stride ring
1977
- * payload, and no decoded-frame consumer requests it over the shm plane.
1978
- */
1979
- var FrameHandleFormatSchema = z.enum([
1980
- "rgb",
1981
- "bgr",
1982
- "yuv420",
1983
- "gray"
1984
- ]);
1985
- /**
1986
- * Input for `stream-broker.subscribeFrames` (Phase 5 / D9). A consumer asks the
1987
- * broker for a `FrameHandle` stream of one `format`; the broker maintains one
1988
- * shared-memory ring per `(brokerId, format)` actually requested, so a `gray`
1989
- * subscriber (motion) and an `rgb` subscriber (detection) each read the format
1990
- * they asked for with no broker-side conversion.
1991
- */
1992
- var SubscribeFramesInputSchema = z.object({
1993
- brokerId: z.string(),
1994
- format: FrameHandleFormatSchema,
1995
- /**
1996
- * Optional reader-side cadence hint in frames per second. The broker does
1997
- * NOT throttle — latest-wins ring reads drop frames implicitly for a slow
1998
- * consumer. The value is echoed back in `SubscribeFramesResult.maxFps` so
1999
- * the consumer can pace its own `pullFrameHandles` polling.
2000
- */
2001
- maxFps: z.number().positive().optional(),
2002
- /** Short caller-identity tag (`motion`, `detection`, …) for diagnostics. */
2003
- tag: z.string().optional()
2004
- });
2005
- /**
2006
- * Result of `stream-broker.subscribeFrames`. The consumer then polls
2007
- * `pullFrameHandles({ subscriptionId, maxCount })` and feeds each returned
2008
- * `FrameHandle` to a `FrameRingReader`.
2009
- */
2010
- var SubscribeFramesResultSchema = z.object({
2011
- /** Opaque id the consumer passes to `pullFrameHandles` / `unsubscribeFrames`. */
2012
- subscriptionId: z.string(),
2013
- /** Reader-side cadence hint (frames/s) — echoes `SubscribeFramesInput.maxFps`. */
2014
- maxFps: z.number().nonnegative()
2015
- });
2016
- /**
2017
- * Wire schema for a decoded audio chunk (Phase 5 / D9). Mirrors the
2018
- * `DecodedAudioChunk` TS interface in `interfaces/stream-broker.ts`: PCM
2019
- * sample bytes plus the track parameters a consumer needs to interpret them.
2020
- *
2021
- * Audio chunks are tiny (a ~500ms AudioCodecSession window is a few KB of
2022
- * Float32 PCM), so unlike video frames they ship their bytes INLINE over
2023
- * tRPC / Moleculer — no shared-memory plane. `data` is typed `Uint8Array`
2024
- * (the wire-serialisable supertype of `Buffer`) to match `DecodedFrameSchema`
2025
- * / `EncodedPacketSchema`'s precedent; a `Buffer` is assignable to it.
2026
- */
2027
- var DecodedAudioChunkSchema = z.object({
2028
- data: z.instanceof(Uint8Array),
2029
- sampleRate: z.number().int().positive(),
2030
- channels: z.number().int().positive(),
2031
- timestamp: z.number()
2032
- });
2033
- /**
2034
- * Input for `stream-broker.subscribeAudioChunks` (Phase 5 / D9). The
2035
- * handle-free, ship-bytes-inline analogue of `subscribeFrames`: a consumer
2036
- * asks the broker for the decoded audio-chunk stream of one broker; the
2037
- * broker keeps a per-subscription bounded FIFO queue the consumer drains via
2038
- * `pullAudioChunks`.
2039
- */
2040
- var SubscribeAudioChunksInputSchema = z.object({
2041
- brokerId: z.string(),
2042
- /** Short caller-identity tag (`audio-analyzer`, …) for `listClients`. */
2043
- tag: z.string().optional()
2044
- });
2045
- /** Result of `stream-broker.subscribeAudioChunks`. */
2046
- var SubscribeAudioChunksResultSchema = z.object({
2047
- /** Opaque id passed to `pullAudioChunks` / `unsubscribeAudioChunks`. */
2048
- subscriptionId: z.string() });
2049
- var BrokerStatusSchema = z.enum([
2050
- "idle",
2051
- "connecting",
2052
- "streaming",
2053
- "error",
2054
- "stopped"
2055
- ]);
2056
- var BrokerStatsSchema = z.object({
2057
- status: BrokerStatusSchema,
2058
- inputFps: z.number(),
2059
- decodeFps: z.number(),
2060
- encodedSubscribers: z.number(),
2061
- decodedSubscribers: z.number(),
2062
- uptimeMs: z.number(),
2063
- bitrateKbps: z.number(),
2064
- idrIntervalMs: z.number(),
2065
- codec: z.string().optional(),
2066
- totalBytes: z.number(),
2067
- packetCount: z.number(),
2068
- rtspClients: z.number(),
2069
- pipeClients: z.number(),
2070
- preBufferSec: z.number(),
2071
- preBufferMs: z.number(),
2072
- preBufferPackets: z.number(),
2073
- /**
2074
- * Moleculer node id of the decoder provider currently servicing this
2075
- * stream's decoded subscribers. `null` until the deferred decoder is
2076
- * created (no decoded clients yet, or codec not detected). Surfaces the
2077
- * runtime decoder placement so the UI can show "Decoder: <agent>" without
2078
- * a separate cap call per broker.
2079
- */
2080
- decoderNodeId: z.string().nullable(),
2081
- /**
2082
- * Detected audio track parameters from the RTSP DESCRIBE / SDP. `null`
2083
- * when the stream has no audio track or the broker is in cold start.
2084
- * `supported = false` means the codec was detected but the local
2085
- * decoder pipeline cannot produce PCM chunks (e.g. AAC without the
2086
- * AAC pipeline wired). Surfaced in the UI device-overview so operators
2087
- * can pre-pick the audio-analysis model that matches the codec.
2088
- */
2089
- audio: z.object({
2090
- codec: z.string(),
2091
- sampleRate: z.number(),
2092
- channels: z.number(),
2093
- supported: z.boolean()
2094
- }).nullable().optional()
2095
- });
2096
- /**
2097
- * Exporter-facing "profile restream" entry. Returned by
2098
- * `cameraStreams.getProfileRtspEntries` — one per ASSIGNED profile slot
2099
- * (high/mid/low). `brokerId` is the PROFILE-keyed broker id
2100
- * (`${deviceId}/${profile}`, e.g. `15/high`); its `url` is a broker
2101
- * RTSP restream that aliases the profile's assigned source broker, so
2102
- * every consumer dialling a profile converges on the same single pull.
2103
- * `codec`/`resolution` describe the assigned SOURCE camStream (for the
2104
- * `auto` resolution-closest pick). Distinct from `RtspRestreamEntry`
2105
- * (raw per-camStream, live-view only).
2106
- */
2107
- var ProfileRtspEntrySchema = z.object({
2108
- profile: CamProfileSchema,
2109
- /** Profile-keyed broker id, format `${deviceId}/${profile}` (e.g. `"15/high"`). */
2110
- brokerId: z.string(),
2111
- url: z.string(),
2112
- mutedUrl: z.string(),
2113
- enabled: z.boolean(),
2114
- codec: z.string().optional(),
2115
- resolution: CamStreamResolutionSchema.optional()
2116
- });
325
+ id: "America/Chicago",
326
+ label: "America/Chicago (UTC−6 / CDT)",
327
+ region: "Americas",
328
+ stdOffsetMinutes: -360,
329
+ dst: US_DST
330
+ },
331
+ {
332
+ id: "America/Denver",
333
+ label: "America/Denver (UTC−7 / MDT)",
334
+ region: "Americas",
335
+ stdOffsetMinutes: -420,
336
+ dst: US_DST
337
+ },
338
+ {
339
+ id: "America/Los_Angeles",
340
+ label: "America/Los Angeles (UTC−8 / PDT)",
341
+ region: "Americas",
342
+ stdOffsetMinutes: -480,
343
+ dst: US_DST
344
+ },
345
+ {
346
+ id: "America/Sao_Paulo",
347
+ label: "America/Sao Paulo (UTC−3)",
348
+ region: "Americas",
349
+ stdOffsetMinutes: -180,
350
+ dst: null
351
+ },
352
+ {
353
+ id: "Asia/Dubai",
354
+ label: "Asia/Dubai (UTC+4)",
355
+ region: "Asia",
356
+ stdOffsetMinutes: 240,
357
+ dst: null
358
+ },
359
+ {
360
+ id: "Asia/Kolkata",
361
+ label: "Asia/Kolkata (UTC+5:30)",
362
+ region: "Asia",
363
+ stdOffsetMinutes: 330,
364
+ dst: null
365
+ },
366
+ {
367
+ id: "Asia/Singapore",
368
+ label: "Asia/Singapore (UTC+8)",
369
+ region: "Asia",
370
+ stdOffsetMinutes: 480,
371
+ dst: null
372
+ },
373
+ {
374
+ id: "Asia/Shanghai",
375
+ label: "Asia/Shanghai (UTC+8)",
376
+ region: "Asia",
377
+ stdOffsetMinutes: 480,
378
+ dst: null
379
+ },
380
+ {
381
+ id: "Asia/Tokyo",
382
+ label: "Asia/Tokyo (UTC+9)",
383
+ region: "Asia",
384
+ stdOffsetMinutes: 540,
385
+ dst: null
386
+ },
387
+ {
388
+ id: "Australia/Sydney",
389
+ label: "Australia/Sydney (UTC+10 / AEDT)",
390
+ region: "Oceania",
391
+ stdOffsetMinutes: 600,
392
+ dst: {
393
+ offsetHours: 1,
394
+ startMonth: 10,
395
+ startWeekIndex: 1,
396
+ startWeekday: "Sunday",
397
+ startHour: 2,
398
+ endMonth: 4,
399
+ endWeekIndex: 1,
400
+ endWeekday: "Sunday",
401
+ endHour: 3
402
+ }
403
+ }
404
+ ];
405
+ /** Resolve an IANA id to its `Timezone`, or `undefined` if unknown. */
406
+ function findTimezone(id) {
407
+ return TIMEZONES.find((tz) => tz.id === id);
408
+ }
2117
409
  //#endregion
2118
410
  //#region src/interfaces/recording-config.ts
2119
411
  /**
@@ -2306,375 +598,6 @@ function migrateConfigToBands(config) {
2306
598
  return schedules.map((schedule) => bandFromSchedule(schedule, bandMode, config));
2307
599
  }
2308
600
  //#endregion
2309
- //#region src/readiness/readiness-registry.ts
2310
- var ReadinessTimeoutError = class extends Error {
2311
- capName;
2312
- scope;
2313
- waitedMs;
2314
- constructor(capName, scope, waitedMs) {
2315
- super(`Timed out waiting for ${capName} (${scopeKey(scope)}) to become ready after ${waitedMs}ms`);
2316
- this.name = "ReadinessTimeoutError";
2317
- this.capName = capName;
2318
- this.scope = scope;
2319
- this.waitedMs = waitedMs;
2320
- }
2321
- };
2322
- /**
2323
- * Build a canonical string key for a `(capName, scope)` pair. Used as
2324
- * the snapshot map key and for log/debug output.
2325
- */
2326
- function readinessKey(capName, scope) {
2327
- return `${capName}|${scopeKey(scope)}`;
2328
- }
2329
- function scopeKey(scope) {
2330
- switch (scope.type) {
2331
- case "global": return "global";
2332
- case "node": return `node:${scope.nodeId}`;
2333
- case "device": return `device:${scope.deviceId}`;
2334
- }
2335
- }
2336
- function scopesEqual(a, b) {
2337
- if (a.type !== b.type) return false;
2338
- if (a.type === "global" || b.type === "global") return true;
2339
- if (a.type === "node" && b.type === "node") return a.nodeId === b.nodeId;
2340
- if (a.type === "device" && b.type === "device") return a.deviceId === b.deviceId;
2341
- return false;
2342
- }
2343
- var ReadinessRegistry = class {
2344
- bus;
2345
- sourceNodeId;
2346
- logger;
2347
- now;
2348
- generation;
2349
- snapshot = /* @__PURE__ */ new Map();
2350
- subscriptions = /* @__PURE__ */ new Set();
2351
- unsubscribeBus;
2352
- unsubscribeAgentOffline;
2353
- constructor(options) {
2354
- this.bus = options.eventBus;
2355
- this.sourceNodeId = options.sourceNodeId;
2356
- this.logger = options.logger;
2357
- this.now = options.now ?? (() => Date.now());
2358
- this.generation = options.generation ?? randomGeneration();
2359
- this.unsubscribeBus = this.bus.subscribe({ category: "system.ready-state" }, (event) => this.ingest(event.data));
2360
- this.unsubscribeAgentOffline = this.bus.subscribe({ category: "agent.offline" }, (event) => this.synthesizeDownForNode(event.data.agentId));
2361
- if (typeof this.bus.getRecent === "function") try {
2362
- const recent = this.bus.getRecent({ category: "system.ready-state" });
2363
- for (const event of recent) this.ingest(event.data);
2364
- } catch {}
2365
- }
2366
- /** Release the event-bus subscription. Idempotent. */
2367
- close() {
2368
- this.unsubscribeBus();
2369
- this.unsubscribeAgentOffline();
2370
- this.subscriptions.clear();
2371
- }
2372
- /** Current snapshot for a `(capName, scope)` pair, or `null` if never seen. */
2373
- get(capName, scope) {
2374
- return this.snapshot.get(readinessKey(capName, scope)) ?? null;
2375
- }
2376
- /**
2377
- * Serializable snapshot for cross-process transport. Returns an array
2378
- * of `ReadinessRecord` plain objects — safe for MsgPack / JSON
2379
- * transport. Used by the hub's `$readiness.getSnapshot` Moleculer
2380
- * action; consumers hydrate their local registry from the result.
2381
- */
2382
- getSnapshotForTransport() {
2383
- return Array.from(this.snapshot.values());
2384
- }
2385
- /**
2386
- * Hydrate the snapshot from an authoritative source. Entries already
2387
- * present locally are skipped — live deltas (received via the event
2388
- * bus subscription) always take precedence over the snapshot. For
2389
- * each newly hydrated entry, a one-shot transition is dispatched to
2390
- * matching subscriptions so pending `awaitReady` callers unblock
2391
- * without having to wait for a fresh event.
2392
- *
2393
- * Local `epoch` is reset to 1 per entry — consumer-side epoch is
2394
- * derived from observed generation transitions, so this mirrors the
2395
- * value that would have been assigned had the consumer observed the
2396
- * first `ready` event directly.
2397
- */
2398
- hydrate(records) {
2399
- const now = this.now();
2400
- for (const record of records) {
2401
- const key = readinessKey(record.capName, record.scope);
2402
- if (this.snapshot.has(key)) continue;
2403
- const hydrated = {
2404
- capName: record.capName,
2405
- scope: record.scope,
2406
- state: record.state,
2407
- generation: record.generation,
2408
- epoch: 1,
2409
- lastChange: now,
2410
- sourceNodeId: record.sourceNodeId
2411
- };
2412
- this.snapshot.set(key, hydrated);
2413
- if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → ${record.state} (hydrated, gen=${record.generation.slice(0, 6)})`);
2414
- const transition = {
2415
- capName: record.capName,
2416
- scope: record.scope,
2417
- state: record.state,
2418
- epoch: 1,
2419
- generation: record.generation,
2420
- sourceNodeId: "hydrated",
2421
- ts: now,
2422
- durationInPrevState: 0
2423
- };
2424
- for (const sub of this.subscriptions) {
2425
- if (sub.capName !== record.capName) continue;
2426
- if (!scopesEqual(sub.scope, record.scope)) continue;
2427
- try {
2428
- sub.handler(transition);
2429
- } catch (err) {
2430
- this.logger?.warn(`readiness hydrate handler threw for ${record.capName}: ${err.message ?? String(err)}`);
2431
- }
2432
- }
2433
- }
2434
- }
2435
- /** Shallow copy of the full snapshot — mainly for diagnostics/tests. */
2436
- getAll() {
2437
- return new Map(this.snapshot);
2438
- }
2439
- /**
2440
- * Emit a `ready` transition for a locally-owned capability. The
2441
- * payload carries this registry's `generation` so remote registries
2442
- * can derive their own `epoch`.
2443
- */
2444
- emitReady(capName, scope) {
2445
- this.emitTransition(capName, scope, "ready");
2446
- }
2447
- emitStarting(capName, scope) {
2448
- this.emitTransition(capName, scope, "starting");
2449
- }
2450
- emitDown(capName, scope) {
2451
- this.emitTransition(capName, scope, "down");
2452
- }
2453
- /**
2454
- * One-shot: resolve once the cap is `ready`. Reads the snapshot
2455
- * first — if already ready, resolves synchronously on the next tick.
2456
- *
2457
- * Default behaviour is **wait indefinitely** (timeoutMs = `Infinity`):
2458
- * the orchestrator and other callers never want to attach a camera
2459
- * with empty steps just because a cap took longer than 30s to come
2460
- * up. Pass an explicit finite `timeoutMs` to bound the wait, or an
2461
- * `AbortSignal` to cancel externally. `Infinity` is honoured natively
2462
- * — no `setTimeout` is registered (Node's setTimeout silently clamps
2463
- * values above ~24.8d, so we must skip the timer entirely).
2464
- */
2465
- awaitReady(capName, scope, opts = {}) {
2466
- const timeoutMs = opts.timeoutMs ?? Number.POSITIVE_INFINITY;
2467
- const start = this.now();
2468
- if (this.get(capName, scope)?.state === "ready") return Promise.resolve();
2469
- if (opts.signal?.aborted) return Promise.reject(opts.signal.reason ?? /* @__PURE__ */ new Error("aborted"));
2470
- return new Promise((resolve, reject) => {
2471
- let settled = false;
2472
- const unsubscribe = this.onReadyState(capName, scope, (t) => {
2473
- if (t.state !== "ready") return;
2474
- if (settled) return;
2475
- settled = true;
2476
- cleanup();
2477
- resolve();
2478
- });
2479
- const timer = !Number.isFinite(timeoutMs) ? null : setTimeout(() => {
2480
- if (settled) return;
2481
- settled = true;
2482
- cleanup();
2483
- reject(new ReadinessTimeoutError(capName, scope, this.now() - start));
2484
- }, timeoutMs);
2485
- const pendingLogTimer = setInterval(() => {
2486
- if (settled) return;
2487
- this.logger?.warn(`readiness: still awaiting ${capName} (${scopeKey(scope)}) after ${this.now() - start}ms`);
2488
- }, 3e4);
2489
- if (typeof pendingLogTimer.unref === "function") pendingLogTimer.unref();
2490
- const onAbort = () => {
2491
- if (settled) return;
2492
- settled = true;
2493
- cleanup();
2494
- reject(opts.signal?.reason ?? /* @__PURE__ */ new Error("aborted"));
2495
- };
2496
- opts.signal?.addEventListener("abort", onAbort, { once: true });
2497
- function cleanup() {
2498
- unsubscribe();
2499
- if (timer !== null) clearTimeout(timer);
2500
- clearInterval(pendingLogTimer);
2501
- opts.signal?.removeEventListener("abort", onAbort);
2502
- }
2503
- });
2504
- }
2505
- /**
2506
- * Observable: every transition (starting/ready/down) dispatches to
2507
- * `handler`. On subscription, if the snapshot has a current state,
2508
- * the handler fires ONCE asynchronously with that state as the
2509
- * initial transition (duration = 0) so late subscribers can pick up
2510
- * current state without racing the next transition.
2511
- *
2512
- * Returns an unsubscribe function.
2513
- */
2514
- onReadyState(capName, scope, handler) {
2515
- const sub = {
2516
- capName,
2517
- scope,
2518
- handler
2519
- };
2520
- this.subscriptions.add(sub);
2521
- const current = this.get(capName, scope);
2522
- if (current !== null) queueMicrotask(() => {
2523
- if (!this.subscriptions.has(sub)) return;
2524
- handler({
2525
- capName,
2526
- scope,
2527
- state: current.state,
2528
- epoch: current.epoch,
2529
- generation: current.generation,
2530
- sourceNodeId: this.sourceNodeId,
2531
- ts: current.lastChange,
2532
- durationInPrevState: 0
2533
- });
2534
- });
2535
- return () => {
2536
- this.subscriptions.delete(sub);
2537
- };
2538
- }
2539
- emitTransition(capName, scope, state) {
2540
- const ts = this.now();
2541
- this.bus.emit(createEvent("system.ready-state", {
2542
- type: "capability",
2543
- id: capName,
2544
- nodeId: this.sourceNodeId
2545
- }, {
2546
- capName,
2547
- scope,
2548
- state,
2549
- generation: this.generation,
2550
- sourceNodeId: this.sourceNodeId,
2551
- ts
2552
- }));
2553
- }
2554
- /**
2555
- * Update snapshot + dispatch to subscribers. Idempotent: same
2556
- * `generation + state` replay is a no-op for both snapshot and
2557
- * subscribers.
2558
- *
2559
- * Defensive against malformed payloads (legal reason: a mock bus's
2560
- * `getRecent` may ignore the category filter and replay unrelated
2561
- * events when we hydrate at construction time).
2562
- */
2563
- ingest(payload) {
2564
- if (typeof payload?.capName !== "string") return;
2565
- if (payload.state !== "ready" && payload.state !== "starting" && payload.state !== "down") return;
2566
- if (typeof payload.generation !== "string") return;
2567
- if (payload.scope?.type !== "global" && payload.scope?.type !== "node" && payload.scope?.type !== "device") return;
2568
- const key = readinessKey(payload.capName, payload.scope);
2569
- const prev = this.snapshot.get(key) ?? null;
2570
- const now = this.now();
2571
- let epoch;
2572
- if (prev === null) epoch = payload.state === "ready" ? 1 : 0;
2573
- else if (prev.generation !== payload.generation && payload.state === "ready") epoch = prev.epoch + 1;
2574
- else epoch = prev.epoch;
2575
- if (prev !== null && prev.generation === payload.generation && prev.state === payload.state) return;
2576
- const durationInPrevState = prev === null ? 0 : Math.max(0, now - prev.lastChange);
2577
- const next = {
2578
- capName: payload.capName,
2579
- scope: payload.scope,
2580
- state: payload.state,
2581
- generation: payload.generation,
2582
- epoch,
2583
- lastChange: now,
2584
- sourceNodeId: payload.sourceNodeId
2585
- };
2586
- this.snapshot.set(key, next);
2587
- const transition = {
2588
- capName: payload.capName,
2589
- scope: payload.scope,
2590
- state: payload.state,
2591
- epoch,
2592
- generation: payload.generation,
2593
- sourceNodeId: payload.sourceNodeId,
2594
- ts: payload.ts,
2595
- durationInPrevState
2596
- };
2597
- 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)`);
2598
- for (const sub of this.subscriptions) {
2599
- if (sub.capName !== payload.capName) continue;
2600
- if (!scopesEqual(sub.scope, payload.scope)) continue;
2601
- try {
2602
- sub.handler(transition);
2603
- } catch (err) {
2604
- this.logger?.warn(`readiness handler threw for ${payload.capName}: ${err.message ?? String(err)}`);
2605
- }
2606
- }
2607
- }
2608
- /**
2609
- * `agent.offline` demux. When an agent disconnects ungracefully,
2610
- * its BaseAddon's shutdown hook doesn't fire — consumers would
2611
- * otherwise keep waiting on a stale `ready`. Walk the snapshot and
2612
- * synthesize a `down` transition for every node-scoped record bound
2613
- * to the disconnected `nodeId`, skipping records already `down`.
2614
- *
2615
- * Synthesis is LOCAL: we don't re-emit on the bus. Every registry
2616
- * (one per process) receives the same `agent.offline` and reacts
2617
- * independently, which means no duplicated downs and no central
2618
- * supervisor required.
2619
- */
2620
- synthesizeDownForNode(offlineNodeId) {
2621
- const now = this.now();
2622
- for (const [key, record] of this.snapshot) {
2623
- if (record.state === "down") continue;
2624
- if (!(record.scope.type === "node" && record.scope.nodeId === offlineNodeId || record.scope.type === "device" && record.sourceNodeId === offlineNodeId)) continue;
2625
- const durationInPrevState = Math.max(0, now - record.lastChange);
2626
- const next = {
2627
- ...record,
2628
- state: "down",
2629
- lastChange: now
2630
- };
2631
- this.snapshot.set(key, next);
2632
- const transition = {
2633
- capName: record.capName,
2634
- scope: record.scope,
2635
- state: "down",
2636
- epoch: record.epoch,
2637
- generation: record.generation,
2638
- sourceNodeId: this.sourceNodeId,
2639
- ts: now,
2640
- durationInPrevState
2641
- };
2642
- if (this.logger) this.logger.debug(`readiness: ${record.capName} (${scopeKey(record.scope)}) → down (synthesized from agent.offline ${offlineNodeId})`);
2643
- for (const sub of this.subscriptions) {
2644
- if (sub.capName !== record.capName) continue;
2645
- if (!scopesEqual(sub.scope, record.scope)) continue;
2646
- try {
2647
- sub.handler(transition);
2648
- } catch (err) {
2649
- this.logger?.warn(`readiness handler threw during agent.offline demux for ${record.capName}: ${err.message ?? String(err)}`);
2650
- }
2651
- }
2652
- }
2653
- }
2654
- };
2655
- function randomGeneration() {
2656
- if (typeof crypto !== "undefined" && crypto.randomUUID) return crypto.randomUUID();
2657
- return Math.random().toString(36).slice(2, 14);
2658
- }
2659
- /**
2660
- * Given a list of `(capName, scope)` pairs that a just-disconnected
2661
- * node owned, emit a `down` transition for each. Callers (kernel
2662
- * supervisor, hub `$node.disconnected` handler) wire this up once they
2663
- * know which caps the node held.
2664
- */
2665
- function emitDownForOwnedCaps(registry, owned) {
2666
- for (const { capName, scope } of owned) registry.emitDown(capName, scope);
2667
- }
2668
- //#endregion
2669
- //#region src/interfaces/addon-data-plane.ts
2670
- /**
2671
- * Per-listener shared-secret header the hub injects on every reverse-proxied
2672
- * data-plane request and the addon's framework wrapper validates — so only the
2673
- * hub can reach the addon's `127.0.0.1` listener. Defined here (shared by the
2674
- * core proxy and the kernel listener) to keep both off a kernel↔core import.
2675
- */
2676
- var DATAPLANE_SECRET_HEADER = "x-camstack-dataplane-secret";
2677
- //#endregion
2678
601
  //#region src/interfaces/storage-location.ts
2679
602
  /**
2680
603
  * `StorageLocationType` — an addon-declared id that identifies the *kind* of
@@ -3212,66 +1135,6 @@ var RUNTIME_DEFAULTS = {
3212
1135
  "auth.tokenExpiry": "24h"
3213
1136
  };
3214
1137
  //#endregion
3215
- //#region src/utils/json-safe.ts
3216
- /**
3217
- * Type-safe JSON parsing helpers.
3218
- *
3219
- * `JSON.parse` is typed as `any` in lib.es5.d.ts, which triggers
3220
- * `no-unsafe-*` ESLint rules and destroys downstream inference. These
3221
- * wrappers return `unknown` — callers narrow structurally via type
3222
- * guards, `typeof` checks, or helpers like `asRecord`/`asString`.
3223
- */
3224
- /**
3225
- * Parse JSON and return it as `unknown` — the only entry point for untrusted JSON.
3226
- *
3227
- * The optional generic overload `parseJsonUnknown<T>(text)` returns `T` for
3228
- * call sites that know the shape at parse time (e.g. MQTT payloads with a
3229
- * known protocol schema). This is a **type-level bridge only** — no runtime
3230
- * validation is performed. Callers that need runtime validation should parse
3231
- * as `unknown` and narrow via Zod or structural guards.
3232
- */
3233
- function parseJsonUnknown(text) {
3234
- return JSON.parse(text);
3235
- }
3236
- /** Narrow an unknown value to a plain `Record<string, unknown>` or return null. */
3237
- function asJsonObject(value) {
3238
- if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
3239
- return { ...value };
3240
- }
3241
- /** Narrow an unknown value to a `readonly unknown[]` or return an empty array. */
3242
- function asJsonArray(value) {
3243
- return Array.isArray(value) ? value : [];
3244
- }
3245
- /** Safe string extraction from an unknown record field. */
3246
- function asString(value, fallback = "") {
3247
- return typeof value === "string" ? value : fallback;
3248
- }
3249
- /** Safe number extraction from an unknown record field. */
3250
- function asNumber(value, fallback = 0) {
3251
- return typeof value === "number" ? value : fallback;
3252
- }
3253
- /** Safe boolean extraction from an unknown record field. */
3254
- function asBoolean(value, fallback = false) {
3255
- return typeof value === "boolean" ? value : fallback;
3256
- }
3257
- /** Parse JSON + narrow to object in one step. */
3258
- function parseJsonObject(text) {
3259
- try {
3260
- return asJsonObject(parseJsonUnknown(text));
3261
- } catch {
3262
- return null;
3263
- }
3264
- }
3265
- /** Parse JSON + narrow to array in one step. */
3266
- function parseJsonArray(text) {
3267
- try {
3268
- const parsed = parseJsonUnknown(text);
3269
- return Array.isArray(parsed) ? parsed : null;
3270
- } catch {
3271
- return null;
3272
- }
3273
- }
3274
- //#endregion
3275
1138
  //#region src/utils/hf-url.ts
3276
1139
  function hfModelUrl(repo, path) {
3277
1140
  return `https://huggingface.co/${repo}/resolve/main/${path}`;
@@ -3518,18 +1381,6 @@ function requiresPython(runtime) {
3518
1381
  return runtime === "coreml" || runtime === "pytorch" || runtime === "openvino";
3519
1382
  }
3520
1383
  //#endregion
3521
- //#region src/utils/err-msg.ts
3522
- /**
3523
- import { errMsg } from '@camstack/types'
3524
- * Extract a human-readable message from an unknown error value.
3525
- * Replaces the ubiquitous `errMsg(err)` pattern.
3526
- */
3527
- function errMsg(err) {
3528
- if (err instanceof Error) return err.message;
3529
- if (typeof err === "string") return err;
3530
- return String(err);
3531
- }
3532
- //#endregion
3533
1384
  //#region src/utils/run-inference-step.ts
3534
1385
  /**
3535
1386
  * Execute an inference function with timing and error capture.
@@ -4344,290 +2195,6 @@ var DEVICE_TYPE_INFO = { ["camera"]: {
4344
2195
  icon: "camera"
4345
2196
  } };
4346
2197
  //#endregion
4347
- //#region src/device/device-type.ts
4348
- var DeviceType = /* @__PURE__ */ function(DeviceType) {
4349
- DeviceType["Camera"] = "camera";
4350
- DeviceType["Hub"] = "hub";
4351
- DeviceType["Light"] = "light";
4352
- DeviceType["Siren"] = "siren";
4353
- DeviceType["Switch"] = "switch";
4354
- DeviceType["Sensor"] = "sensor";
4355
- DeviceType["Thermostat"] = "thermostat";
4356
- DeviceType["Button"] = "button";
4357
- /** Generic stateless event emitter — carries a device's EXACT declared
4358
- * event vocabulary verbatim (no normalization). Installed with the
4359
- * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
4360
- * HA bus events (e.g. `zha_event`, generic). */
4361
- DeviceType["EventEmitter"] = "event-emitter";
4362
- /** Firmware/software update entity — current vs available version,
4363
- * updatable flag, update state, and an install action. Installed with
4364
- * the `update` cap. Sources: Homematic firmware-update channels (and
4365
- * reusable by other providers, e.g. HA `update.*` entities). */
4366
- DeviceType["Update"] = "update";
4367
- DeviceType["Generic"] = "generic";
4368
- /** Generic notification delivery target (HA `notify.<service>`, future
4369
- * Telegram / Discord / ntfy / SMTP, …). One device per delivery
4370
- * endpoint; the `notifier` cap defines the send surface. */
4371
- DeviceType["Notifier"] = "notifier";
4372
- /** Pre-recorded action sequence with optional parameters
4373
- * (HA `script.*`). Runnable via `script-runner` cap. */
4374
- DeviceType["Script"] = "script";
4375
- /** Automation rule (HA `automation.*`) — enable/disable + manual
4376
- * trigger surface exposed via `automation-control` cap. */
4377
- DeviceType["Automation"] = "automation";
4378
- /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
4379
- DeviceType["Lock"] = "lock";
4380
- /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
4381
- * `valve.*`). `cover` cap with sub-roles for variant. */
4382
- DeviceType["Cover"] = "cover";
4383
- /** Pipe / water / gas valve with open/close/stop and optional
4384
- * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
4385
- * modelled on the same open/closed lifecycle. */
4386
- DeviceType["Valve"] = "valve";
4387
- /** Humidifier / dehumidifier with on/off + target humidity + mode
4388
- * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
4389
- * modelled on the same target / mode lifecycle. */
4390
- DeviceType["Humidifier"] = "humidifier";
4391
- /** Water heater / boiler with target temperature + operation mode +
4392
- * away mode (HA `water_heater.*`). `water-heater` cap — a
4393
- * climate-family actuator. */
4394
- DeviceType["WaterHeater"] = "water-heater";
4395
- /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
4396
- DeviceType["Fan"] = "fan";
4397
- /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
4398
- * the camera surface — those use `Camera`. `media-player` cap. */
4399
- DeviceType["MediaPlayer"] = "media-player";
4400
- /** Security panel / alarm system (HA `alarm_control_panel.*`).
4401
- * `alarm-panel` cap. */
4402
- DeviceType["AlarmPanel"] = "alarm-panel";
4403
- /** Generic user-settable input (HA `number` / `input_number` / `select`
4404
- * / `input_select` / `text` / `input_text` / `input_datetime`).
4405
- * Sub-type via `DeviceRole`: NumericControl / SelectControl /
4406
- * TextControl / DateTimeControl. */
4407
- DeviceType["Control"] = "control";
4408
- /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
4409
- * `presence` cap. */
4410
- DeviceType["Presence"] = "presence";
4411
- /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
4412
- * `weather` cap. */
4413
- DeviceType["Weather"] = "weather";
4414
- /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
4415
- DeviceType["Vacuum"] = "vacuum";
4416
- /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
4417
- * `lawn-mower-control` cap. */
4418
- DeviceType["LawnMower"] = "lawn-mower";
4419
- /** Physical HA device group — parent container for entity-children
4420
- * adopted from a single HA device entry. Not renderable as a
4421
- * standalone device; exists only to anchor child entities. */
4422
- DeviceType["Container"] = "container";
4423
- /** Single still-image entity (HA `image.*`). Read-only display of an
4424
- * `entity_picture` signed URL the browser loads directly. `image` cap. */
4425
- DeviceType["Image"] = "image";
4426
- return DeviceType;
4427
- }({});
4428
- var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
4429
- DeviceFeature["BatteryOperated"] = "battery-operated";
4430
- DeviceFeature["Rebootable"] = "rebootable";
4431
- /**
4432
- * Device supports an on-demand re-sync of its derived spec with its
4433
- * upstream source — drives the generic Re-sync button. The owning
4434
- * provider implements the action via the `device-adoption.resync` cap.
4435
- */
4436
- DeviceFeature["Resyncable"] = "resyncable";
4437
- DeviceFeature["NativeSnapshot"] = "native-snapshot";
4438
- DeviceFeature["DoorbellButton"] = "doorbell-button";
4439
- DeviceFeature["TwoWayAudio"] = "two-way-audio";
4440
- DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
4441
- /**
4442
- * Camera supports the on-firmware autotrack subsystem (subject-
4443
- * following). Distinct from `PanTiltZoom` because not every PTZ
4444
- * camera ships autotrack — the admin UI uses this flag to gate
4445
- * the autotrack toggle / settings card without re-deriving from
4446
- * the cap registry. Mirrors `ptz-autotrack` cap registration:
4447
- * driver sets this feature when probe confirms the firmware
4448
- * surface, and registers the cap in the same code path.
4449
- */
4450
- DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
4451
- /**
4452
- * Accessory exposes a "trigger on motion" toggle — the parent camera's
4453
- * motion detection automatically activates this device. Mirrors
4454
- * `motion-trigger` cap registration: drivers set this feature in the
4455
- * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
4456
- *
4457
- * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
4458
- * fast scalar without binding fetch), notifier rules, and `listAll`
4459
- * filters that want "all devices with on-motion behaviour".
4460
- */
4461
- DeviceFeature["MotionTrigger"] = "motion-trigger";
4462
- /** Light supports rgb-triplet color via `color` cap. */
4463
- DeviceFeature["LightColorRgb"] = "light-color-rgb";
4464
- /** Light supports HSV color via `color` cap. */
4465
- DeviceFeature["LightColorHsv"] = "light-color-hsv";
4466
- /** Light supports color-temperature (mired) via `color` cap. */
4467
- DeviceFeature["LightColorMired"] = "light-color-mired";
4468
- /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
4469
- * targetHigh). Gates the range slider UI. */
4470
- DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
4471
- /** Thermostat exposes target humidity and/or current humidity
4472
- * readings. Gates the humidity controls. */
4473
- DeviceFeature["ClimateHumidity"] = "climate-humidity";
4474
- /** Thermostat exposes a fan-mode selector. */
4475
- DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
4476
- /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
4477
- DeviceFeature["ClimatePreset"] = "climate-preset";
4478
- /** Cover exposes intermediate position control (0..100). Gates the
4479
- * position slider UI. */
4480
- DeviceFeature["CoverPositionable"] = "cover-positionable";
4481
- /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
4482
- DeviceFeature["CoverTilt"] = "cover-tilt";
4483
- /** Valve exposes intermediate position control (0..100). Gates the
4484
- * position slider / drag surface UI. */
4485
- DeviceFeature["ValvePositionable"] = "valve-positionable";
4486
- /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
4487
- DeviceFeature["FanSpeed"] = "fan-speed";
4488
- /** Fan exposes a preset mode selector. */
4489
- DeviceFeature["FanPreset"] = "fan-preset";
4490
- /** Fan exposes blade direction (forward/reverse) — typical of
4491
- * ceiling fans. */
4492
- DeviceFeature["FanDirection"] = "fan-direction";
4493
- /** Fan exposes an oscillation toggle. */
4494
- DeviceFeature["FanOscillating"] = "fan-oscillating";
4495
- /** Lock requires a PIN code on lock/unlock. Gates the code-entry
4496
- * field on the UI lock-controls panel. */
4497
- DeviceFeature["LockPinRequired"] = "lock-pin-required";
4498
- /** Lock supports a latch-release ("open door") action distinct from
4499
- * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
4500
- * `supported_features`. Gates the Open Door button in the UI. */
4501
- DeviceFeature["LockOpen"] = "lock-open";
4502
- /** Media player exposes a seek-to-position surface. */
4503
- DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
4504
- /** Media player exposes a volume-level setter. */
4505
- DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
4506
- /** Media player exposes a mute toggle distinct from volume=0. */
4507
- DeviceFeature["MediaPlayerMute"] = "media-player-mute";
4508
- /** Media player exposes a shuffle toggle. */
4509
- DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
4510
- /** Media player exposes a repeat mode (off / all / one). */
4511
- DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
4512
- /** Media player exposes a source / input selector. */
4513
- DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
4514
- /** Media player exposes a play-arbitrary-media surface (URL / id). */
4515
- DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
4516
- /** Media player exposes next-track. */
4517
- DeviceFeature["MediaPlayerNext"] = "media-player-next";
4518
- /** Media player exposes previous-track. */
4519
- DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
4520
- /** Media player exposes stop distinct from pause. */
4521
- DeviceFeature["MediaPlayerStop"] = "media-player-stop";
4522
- /** Alarm panel requires a PIN code on arm/disarm. */
4523
- DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
4524
- /** Presence device carries GPS coordinates (lat/lng/accuracy) in
4525
- * addition to a textual location. */
4526
- DeviceFeature["PresenceGps"] = "presence-gps";
4527
- /** Notifier accepts an inline / URL image attachment. */
4528
- DeviceFeature["NotifierImage"] = "notifier-image";
4529
- /** Notifier accepts a priority hint (high/normal/low). */
4530
- DeviceFeature["NotifierPriority"] = "notifier-priority";
4531
- /** Notifier accepts a free-form `data` payload for platform-specific
4532
- * fields. */
4533
- DeviceFeature["NotifierData"] = "notifier-data";
4534
- /** Notifier supports interactive action buttons / callbacks. */
4535
- DeviceFeature["NotifierActions"] = "notifier-actions";
4536
- /** Notifier supports per-call recipient targeting (multi-user). */
4537
- DeviceFeature["NotifierRecipients"] = "notifier-recipients";
4538
- /** Script runner accepts a variables map on each run invocation. */
4539
- DeviceFeature["ScriptVariables"] = "script-variables";
4540
- /** Automation `trigger` accepts a skipCondition flag — fires the
4541
- * automation's actions while bypassing its condition block. */
4542
- DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
4543
- return DeviceFeature;
4544
- }({});
4545
- var ChargingStatus = /* @__PURE__ */ function(ChargingStatus) {
4546
- ChargingStatus["ChargingDC"] = "charging-dc";
4547
- ChargingStatus["ChargingSolar"] = "charging-solar";
4548
- ChargingStatus["NotCharging"] = "not-charging";
4549
- return ChargingStatus;
4550
- }({});
4551
- /**
4552
- * Semantic role a device plays within its parent. Populated by driver
4553
- * addons when creating accessory devices (Reolink siren/floodlight/
4554
- * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
4555
- * admin UI to pick icons, labels, and widgets — a `Switch` with
4556
- * `role: Floodlight` renders as a bulb with a brightness slider,
4557
- * whereas a `Switch` with `role: Siren` renders as a klaxon.
4558
- *
4559
- * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
4560
- * sqlite as a nullable TEXT column — old rows keep working unchanged.
4561
- */
4562
- var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
4563
- DeviceRole["Siren"] = "siren";
4564
- DeviceRole["Floodlight"] = "floodlight";
4565
- DeviceRole["Spotlight"] = "spotlight";
4566
- DeviceRole["PirSensor"] = "pir-sensor";
4567
- DeviceRole["Chime"] = "chime";
4568
- DeviceRole["Autotrack"] = "autotrack";
4569
- DeviceRole["Nightvision"] = "nightvision";
4570
- DeviceRole["PrivacyMask"] = "privacy-mask";
4571
- DeviceRole["Doorbell"] = "doorbell";
4572
- /** Virtual HA toggle (input_boolean.*) — distinguishable from a
4573
- * real Switch device for UI rendering / export adapters. */
4574
- DeviceRole["BinaryHelper"] = "binary-helper";
4575
- /** Generic motion / occupancy / moving event source. Distinct from
4576
- * the camera accessory PirSensor role: that one is a camera child;
4577
- * this is a standalone HA / 3rd-party motion sensor. */
4578
- DeviceRole["MotionSensor"] = "motion-sensor";
4579
- DeviceRole["ContactSensor"] = "contact-sensor";
4580
- DeviceRole["LeakSensor"] = "leak-sensor";
4581
- DeviceRole["SmokeSensor"] = "smoke-sensor";
4582
- DeviceRole["COSensor"] = "co-sensor";
4583
- DeviceRole["GasSensor"] = "gas-sensor";
4584
- DeviceRole["TamperSensor"] = "tamper-sensor";
4585
- DeviceRole["VibrationSensor"] = "vibration-sensor";
4586
- DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
4587
- DeviceRole["SoundSensor"] = "sound-sensor";
4588
- /** Fallback for `binary_sensor` without a known `device_class`. */
4589
- DeviceRole["BinarySensor"] = "binary-sensor";
4590
- DeviceRole["TemperatureSensor"] = "temperature-sensor";
4591
- DeviceRole["HumiditySensor"] = "humidity-sensor";
4592
- DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
4593
- DeviceRole["PressureSensor"] = "pressure-sensor";
4594
- DeviceRole["PowerSensor"] = "power-sensor";
4595
- DeviceRole["EnergySensor"] = "energy-sensor";
4596
- DeviceRole["VoltageSensor"] = "voltage-sensor";
4597
- DeviceRole["CurrentSensor"] = "current-sensor";
4598
- DeviceRole["AirQualitySensor"] = "air-quality-sensor";
4599
- /** Battery level (numeric % via `sensor` OR low-bool via
4600
- * `binary_sensor` — the cap distinguishes via the value type). */
4601
- DeviceRole["BatterySensor"] = "battery-sensor";
4602
- /** Fallback for `sensor` numeric without a known `device_class`. */
4603
- DeviceRole["NumericSensor"] = "numeric-sensor";
4604
- /** String / enum state (HA `sensor` with `state_class: enum` or
4605
- * `attributes.options`). */
4606
- DeviceRole["EnumSensor"] = "enum-sensor";
4607
- /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
4608
- * or `date`). The slice carries the raw ISO string verbatim (hosted on
4609
- * the `enum-sensor` cap); the UI renders it locale-formatted. */
4610
- DeviceRole["DateTimeSensor"] = "datetime-sensor";
4611
- /** Last-resort fallback when nothing else matches. */
4612
- DeviceRole["GenericSensor"] = "generic-sensor";
4613
- DeviceRole["NumericControl"] = "numeric-control";
4614
- DeviceRole["SelectControl"] = "select-control";
4615
- DeviceRole["TextControl"] = "text-control";
4616
- DeviceRole["DateTimeControl"] = "datetime-control";
4617
- /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
4618
- * rich features (image, priority, channel routing). */
4619
- DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
4620
- /** Chat / messaging service (HA `notify.telegram_*`,
4621
- * `notify.discord_*`, etc.). */
4622
- DeviceRole["MessagingNotifier"] = "messaging-notifier";
4623
- /** Email-based delivery (HA `notify.smtp`, etc.). */
4624
- DeviceRole["EmailNotifier"] = "email-notifier";
4625
- /** Fallback when the notifier service name doesn't match a known
4626
- * pattern. */
4627
- DeviceRole["GenericNotifier"] = "generic-notifier";
4628
- return DeviceRole;
4629
- }({});
4630
- //#endregion
4631
2198
  //#region src/device/accessory.ts
4632
2199
  /**
4633
2200
  * Accessory device helpers — shared across drivers.
@@ -5193,148 +2760,12 @@ var airQualitySensorCapability = {
5193
2760
  mode: "singleton",
5194
2761
  deviceTypes: [DeviceType.Sensor],
5195
2762
  methods: {},
5196
- status: {
5197
- schema: AirQualitySensorStatusSchema,
5198
- kind: "push"
5199
- },
5200
- runtimeState: AirQualitySensorStatusSchema
5201
- };
5202
- //#endregion
5203
- //#region src/capabilities/capability-definition.ts
5204
- /**
5205
- * Generic types for capability definitions.
5206
- *
5207
- * A capability is defined with Zod schemas for methods, events, and settings.
5208
- * TypeScript types are inferred via z.infer<> — zero duplication.
5209
- *
5210
- * Pattern:
5211
- * 1. Define Zod schemas for data, methods, settings
5212
- * 2. Export const capabilityDef = { ... } satisfies CapabilityDefinition
5213
- * 3. Export type IProvider = InferProvider<typeof capabilityDef>
5214
- * 4. Addon implements IProvider
5215
- * 5. Registry auto-mounts tRPC router from definition.methods
5216
- */
5217
- /**
5218
- * Output schema shared by the contribution + live methods.
5219
- *
5220
- * Mirrors the `ConfigUISchemaWithValues` shape (sections[] + optional
5221
- * tabs[]) without importing from `../interfaces/config-ui.js` — a
5222
- * concrete-but-lenient Zod object keeps tRPC output inference happy
5223
- * (using `z.unknown()` here collapses unrelated router branches to
5224
- * `unknown` when the generator re-inlines the huge AppRouter type).
5225
- *
5226
- * `.passthrough()` on sections/fields accepts whatever FormBuilder
5227
- * extensions the caller adds (showWhen, displayScale, …) without
5228
- * rebuilding every time a new field kind is introduced.
5229
- */
5230
- var ContributionSectionSchema = z.object({
5231
- id: z.string(),
5232
- title: z.string(),
5233
- description: z.string().optional(),
5234
- style: z.enum(["card", "accordion"]).optional(),
5235
- defaultCollapsed: z.boolean().optional(),
5236
- columns: z.union([
5237
- z.literal(1),
5238
- z.literal(2),
5239
- z.literal(3),
5240
- z.literal(4)
5241
- ]).optional(),
5242
- tab: z.string().optional(),
5243
- location: z.enum(["settings", "top-tab"]).optional(),
5244
- order: z.number().optional(),
5245
- fields: z.array(z.any())
5246
- });
5247
- var ContributionTabSchema = z.object({
5248
- id: z.string(),
5249
- label: z.string(),
5250
- icon: z.string(),
5251
- order: z.number().optional()
5252
- });
5253
- var ContributionOutputSchema = z.object({
5254
- tabs: z.array(ContributionTabSchema).optional(),
5255
- sections: z.array(ContributionSectionSchema)
5256
- }).nullable();
5257
- var DEVICE_SETTINGS_CONTRIBUTION_METHODS = {
5258
- getDeviceSettingsContribution: {
5259
- input: z.object({ deviceId: z.number() }),
5260
- output: ContributionOutputSchema,
5261
- kind: "query",
5262
- auth: "protected"
5263
- },
5264
- getDeviceLiveContribution: {
5265
- input: z.object({ deviceId: z.number() }),
5266
- output: ContributionOutputSchema,
5267
- kind: "query",
5268
- auth: "protected"
5269
- },
5270
- applyDeviceSettingsPatch: {
5271
- input: z.object({
5272
- deviceId: z.number(),
5273
- patch: z.record(z.string(), z.unknown())
5274
- }),
5275
- output: z.object({ success: z.literal(true) }),
5276
- kind: "mutation",
5277
- auth: "admin"
5278
- }
2763
+ status: {
2764
+ schema: AirQualitySensorStatusSchema,
2765
+ kind: "push"
2766
+ },
2767
+ runtimeState: AirQualitySensorStatusSchema
5279
2768
  };
5280
- /**
5281
- * Schema for the `getStatus` method auto-injected when a cap declares
5282
- * `status`. The runtime shape of the output is the cap's own
5283
- * `status.schema` — but at codegen time we need a concrete Zod to emit
5284
- * a typed tRPC route, so we keep the output as `z.unknown().nullable()`
5285
- * here and tighten it on the client side via the generated
5286
- * `CapStatusTypeMap` (see `scripts/generate-cap-status-types.ts`).
5287
- */
5288
- var DEVICE_STATUS_METHOD = { getStatus: {
5289
- input: z.object({ deviceId: z.number() }),
5290
- output: z.unknown().nullable(),
5291
- kind: "query",
5292
- auth: "protected"
5293
- } };
5294
- /**
5295
- * Expand a cap def's methods map with every auto-injected method:
5296
- * - `exposesDeviceSettings: true` → 3 contribution methods
5297
- * - `status: {...}` → `getStatus`
5298
- *
5299
- * Callers walk `expandCapMethods(def)` instead of `def.methods` when
5300
- * they need the effective runtime method surface (Moleculer actions,
5301
- * proxy shape, tRPC router entries). The cap def stays the single
5302
- * source of truth. Providers still declare their concrete `methods`
5303
- * block; the expansion happens at every consumption point, so there's
5304
- * no accidental divergence between the declared surface and what's
5305
- * actually mounted.
5306
- */
5307
- function expandCapMethods(def) {
5308
- let out = def.methods;
5309
- if (def.exposesDeviceSettings) out = {
5310
- ...DEVICE_SETTINGS_CONTRIBUTION_METHODS,
5311
- ...out
5312
- };
5313
- if (def.status) out = {
5314
- ...DEVICE_STATUS_METHOD,
5315
- ...out
5316
- };
5317
- return out;
5318
- }
5319
- /** Shorthand to define a method schema */
5320
- function method(input, output, options) {
5321
- return {
5322
- input,
5323
- output,
5324
- kind: options?.kind ?? "query",
5325
- auth: options?.auth ?? "protected",
5326
- ...options?.access !== void 0 ? { access: options.access } : {},
5327
- timeoutMs: options?.timeoutMs
5328
- };
5329
- }
5330
- /** Shorthand to define an event schema */
5331
- function event(data) {
5332
- return { data };
5333
- }
5334
- /** Type guard: does this cap declare the D14 device-config archetype? */
5335
- function isDeviceConfigCap(def) {
5336
- return def?.deviceConfig !== void 0;
5337
- }
5338
2769
  //#endregion
5339
2770
  //#region src/capabilities/alarm-panel.cap.ts
5340
2771
  /**
@@ -12248,767 +9679,6 @@ function enumerateSchemaFields(schema, prefix = "") {
12248
9679
  return out;
12249
9680
  }
12250
9681
  //#endregion
12251
- //#region src/device/device-state-handle.ts
12252
- var DEVICE_STATE_EVENT_CATEGORY = "device.state-changed";
12253
- /**
12254
- * Lazy tRPC source — the default behavior. Maintains a per-key local
12255
- * cache (`{deviceId}:{capName}` → last slice), one shared
12256
- * `live.onEvent` bridge that fans out into the listener map, and
12257
- * `refresh` round-trips through `deviceState.getCapSlice`.
12258
- *
12259
- * The bridge is opened on first `watch()` and closed when the last
12260
- * watcher unsubscribes — keeps idle handles cheap.
12261
- */
12262
- function createLazyTrpcSource(api) {
12263
- const cache = /* @__PURE__ */ new Map();
12264
- const listeners = /* @__PURE__ */ new Map();
12265
- let bridge = null;
12266
- const keyOf = (deviceId, capName) => `${deviceId}:${capName}`;
12267
- const ensureBridge = () => {
12268
- if (bridge) return;
12269
- if (!api.live?.onEvent) return;
12270
- bridge = api.live.onEvent.subscribe({ category: DEVICE_STATE_EVENT_CATEGORY }, { onData: (evt) => {
12271
- const data = evt.data;
12272
- if (!data || typeof data.deviceId !== "number" || typeof data.capName !== "string") return;
12273
- const k = keyOf(data.deviceId, data.capName);
12274
- cache.set(k, data.slice);
12275
- const set = listeners.get(k);
12276
- if (!set) return;
12277
- for (const cb of set) try {
12278
- cb(data.slice);
12279
- } catch {}
12280
- } });
12281
- };
12282
- const closeBridgeIfIdle = () => {
12283
- if (!bridge) return;
12284
- if (listeners.size > 0) return;
12285
- bridge.unsubscribe();
12286
- bridge = null;
12287
- };
12288
- return {
12289
- read(deviceId, capName) {
12290
- return cache.get(keyOf(deviceId, capName));
12291
- },
12292
- async refresh(deviceId, capName) {
12293
- const slice = await api.deviceState.getCapSlice.query({
12294
- deviceId,
12295
- capName
12296
- });
12297
- const k = keyOf(deviceId, capName);
12298
- cache.set(k, slice ?? void 0);
12299
- const set = listeners.get(k);
12300
- if (set) for (const cb of set) try {
12301
- cb(slice ?? void 0);
12302
- } catch {}
12303
- },
12304
- watch(deviceId, capName, cb) {
12305
- const k = keyOf(deviceId, capName);
12306
- let set = listeners.get(k);
12307
- if (!set) {
12308
- set = /* @__PURE__ */ new Set();
12309
- listeners.set(k, set);
12310
- }
12311
- set.add(cb);
12312
- ensureBridge();
12313
- return () => {
12314
- set.delete(cb);
12315
- if (set.size === 0) listeners.delete(k);
12316
- closeBridgeIfIdle();
12317
- };
12318
- },
12319
- async write(deviceId, capName, slice) {
12320
- await api.deviceState.setCapSlice.mutate({
12321
- deviceId,
12322
- capName,
12323
- slice
12324
- });
12325
- }
12326
- };
12327
- }
12328
- /**
12329
- * Mirror source — reads from a shared map populated by a
12330
- * `SystemManager` warm-boot + push event handler. Refresh is a no-op
12331
- * (the SystemManager owns the update loop). `watch()` registers in a
12332
- * shared listener set — the SystemManager calls these from its single
12333
- * `device.state-changed` subscription.
12334
- *
12335
- * The mirror map and listener map are passed in by reference so the
12336
- * SystemManager can mutate both as events arrive.
12337
- */
12338
- function createMirrorSource(mirror, listeners, api) {
12339
- const keyOf = (deviceId, capName) => `${deviceId}:${capName}`;
12340
- return {
12341
- read(deviceId, capName) {
12342
- return mirror.get(deviceId)?.get(capName);
12343
- },
12344
- async refresh() {},
12345
- watch(deviceId, capName, cb) {
12346
- const k = keyOf(deviceId, capName);
12347
- let set = listeners.get(k);
12348
- if (!set) {
12349
- set = /* @__PURE__ */ new Set();
12350
- listeners.set(k, set);
12351
- }
12352
- set.add(cb);
12353
- return () => {
12354
- set.delete(cb);
12355
- if (set.size === 0) listeners.delete(k);
12356
- };
12357
- },
12358
- async write(deviceId, capName, slice) {
12359
- if (!api) throw new Error("createMirrorSource: write requires an api reference — pass it as the third argument when constructing the source");
12360
- await api.deviceState.setCapSlice.mutate({
12361
- deviceId,
12362
- capName,
12363
- slice
12364
- });
12365
- }
12366
- };
12367
- }
12368
- /**
12369
- * Build a `SliceHandle<T>` bound to `(deviceId, capName)`. Caller
12370
- * provides the type parameter — codegen passes
12371
- * `InferRuntimeState<typeof <cap>>` so consumers see the cap's typed
12372
- * shape:
12373
- *
12374
- * const dev = createDeviceProxy(api, binding)
12375
- * dev.state.battery.value // BatteryStatus | undefined
12376
- * dev.state.battery.value?.percentage
12377
- *
12378
- * Two-arg form (deprecated path, kept for spec compatibility):
12379
- * passes the api directly, builds a per-handle lazy source. New
12380
- * code should pre-build a single source and reuse it across handles
12381
- * via the explicit `source` form.
12382
- */
12383
- function createSliceHandle(source, deviceId, capName) {
12384
- const src = isSource(source) ? source : createLazyTrpcSource(source);
12385
- return {
12386
- get value() {
12387
- return src.read(deviceId, capName);
12388
- },
12389
- refresh() {
12390
- return src.refresh(deviceId, capName);
12391
- },
12392
- subscribe(cb) {
12393
- const unwatch = src.watch(deviceId, capName, (slice) => {
12394
- try {
12395
- cb(slice);
12396
- } catch {}
12397
- });
12398
- try {
12399
- cb(src.read(deviceId, capName));
12400
- } catch {}
12401
- if (src.read(deviceId, capName) === void 0) src.refresh(deviceId, capName).catch(() => void 0);
12402
- return unwatch;
12403
- },
12404
- async set(slice) {
12405
- await src.write(deviceId, capName, slice);
12406
- },
12407
- async patch(partial) {
12408
- const next = {
12409
- ...src.read(deviceId, capName),
12410
- ...partial
12411
- };
12412
- await src.write(deviceId, capName, next);
12413
- }
12414
- };
12415
- }
12416
- function isSource(x) {
12417
- return typeof x.read === "function" && typeof x.watch === "function";
12418
- }
12419
- //#endregion
12420
- //#region src/generated/device-proxy.ts
12421
- /**
12422
- * Build a DeviceProxy that pre-binds deviceId + nodeId on every method call
12423
- * and dispatches through the existing cap-router tRPC procedures.
12424
- *
12425
- * Optional `opts.stateSource` lets a SystemManager pass a shared mirror
12426
- * source so every device proxy reads from the same in-memory map and
12427
- * shares the warm-boot/push-event update loop. When omitted, a per-proxy
12428
- * lazy tRPC source is created — the path used by `ctx.fetchDevice` and
12429
- * `BackendClient.fetchDevice` for one-off reads.
12430
- *
12431
- * The returned proxy's `binding` field is set to the input `binding`
12432
- * by default — callers that want to expose a different value (e.g. the
12433
- * SDK's `System` patches it from a shared cache) can overwrite the
12434
- * field on the returned object.
12435
- */
12436
- function createDeviceProxy(api, binding, opts) {
12437
- const typedApi = api;
12438
- const stateSource = opts?.stateSource ?? createLazyTrpcSource(api);
12439
- /** Merge `{deviceId, nodeId?}` into the caller-supplied input. */
12440
- function mergeInput(input, nodeId) {
12441
- const base = typeof input === "object" && input !== null ? input : {};
12442
- return nodeId !== void 0 ? {
12443
- ...base,
12444
- deviceId: binding.deviceId,
12445
- nodeId
12446
- } : {
12447
- ...base,
12448
- deviceId: binding.deviceId
12449
- };
12450
- }
12451
- /**
12452
- * Invoke `api.<capProp>.<method>.{query|mutate|subscribe}(merged)`.
12453
- * Returns `any` so the caller-side per-method declaration assigns
12454
- * cleanly into the strongly-typed `InferDeviceProxyCap<...>` shape on
12455
- * the `DeviceProxy` interface — the proxy's contract is enforced by
12456
- * the interface, not the dispatch helper.
12457
- */
12458
- function callLeaf(capProp, method, kind, merged, push) {
12459
- const leaf = typedApi[capProp]?.[method];
12460
- if (!leaf) throw new Error(`DeviceProxy: api has no '${capProp}.${method}'`);
12461
- const fn = leaf;
12462
- if (kind === "mutation") return fn.mutate(merged);
12463
- if (kind === "subscription") return fn.subscribe(merged, push);
12464
- return fn.query(merged);
12465
- }
12466
- /**
12467
- * Device-scoped cap dispatch. Looks up the binding entry for `capName`
12468
- * to pin the call to the worker that owns the per-device provider; when
12469
- * no entry exists (cluster-wide singletons like `zones` /
12470
- * `zone-rules` / `audio-metrics` that don't register per-device
12471
- * natives), nodeId is omitted and the cap-router's `resolveProvider`
12472
- * falls through to the local provider — a Moleculer bridge proxy when
12473
- * the actual singleton lives in a worker.
12474
- */
12475
- function dispatch(capName, capProp, method, kind, input, push) {
12476
- return callLeaf(capProp, method, kind, mergeInput(input, binding.entries.find((e) => e.capName === capName)?.providerNodeId), push);
12477
- }
12478
- /**
12479
- * System-cap dispatch. No binding gate — system caps are cluster-wide
12480
- * singletons; the nodeId is left absent so caps that load-balance (e.g.
12481
- * `pipeline-runner`) can resolve their own target node.
12482
- */
12483
- function dispatchSystem(capProp, method, kind, input, push) {
12484
- return callLeaf(capProp, method, kind, mergeInput(input, void 0), push);
12485
- }
12486
- return {
12487
- deviceId: binding.deviceId,
12488
- binding,
12489
- state: {
12490
- airQualitySensor: createSliceHandle(stateSource, binding.deviceId, "air-quality-sensor"),
12491
- alarmPanel: createSliceHandle(stateSource, binding.deviceId, "alarm-panel"),
12492
- ambientLightSensor: createSliceHandle(stateSource, binding.deviceId, "ambient-light-sensor"),
12493
- audioMetrics: createSliceHandle(stateSource, binding.deviceId, "audio-metrics"),
12494
- automationControl: createSliceHandle(stateSource, binding.deviceId, "automation-control"),
12495
- battery: createSliceHandle(stateSource, binding.deviceId, "battery"),
12496
- binary: createSliceHandle(stateSource, binding.deviceId, "binary"),
12497
- brightness: createSliceHandle(stateSource, binding.deviceId, "brightness"),
12498
- cameraStreams: createSliceHandle(stateSource, binding.deviceId, "camera-streams"),
12499
- carbonMonoxide: createSliceHandle(stateSource, binding.deviceId, "carbon-monoxide"),
12500
- climateControl: createSliceHandle(stateSource, binding.deviceId, "climate-control"),
12501
- color: createSliceHandle(stateSource, binding.deviceId, "color"),
12502
- connectivity: createSliceHandle(stateSource, binding.deviceId, "connectivity"),
12503
- consumables: createSliceHandle(stateSource, binding.deviceId, "consumables"),
12504
- contact: createSliceHandle(stateSource, binding.deviceId, "contact"),
12505
- control: createSliceHandle(stateSource, binding.deviceId, "control"),
12506
- cover: createSliceHandle(stateSource, binding.deviceId, "cover"),
12507
- deviceDiscovery: createSliceHandle(stateSource, binding.deviceId, "device-discovery"),
12508
- deviceStatus: createSliceHandle(stateSource, binding.deviceId, "device-status"),
12509
- doorbell: createSliceHandle(stateSource, binding.deviceId, "doorbell"),
12510
- enumSensor: createSliceHandle(stateSource, binding.deviceId, "enum-sensor"),
12511
- eventEmitter: createSliceHandle(stateSource, binding.deviceId, "event-emitter"),
12512
- fanControl: createSliceHandle(stateSource, binding.deviceId, "fan-control"),
12513
- featureProbe: createSliceHandle(stateSource, binding.deviceId, "feature-probe"),
12514
- flood: createSliceHandle(stateSource, binding.deviceId, "flood"),
12515
- gas: createSliceHandle(stateSource, binding.deviceId, "gas"),
12516
- humidifier: createSliceHandle(stateSource, binding.deviceId, "humidifier"),
12517
- humiditySensor: createSliceHandle(stateSource, binding.deviceId, "humidity-sensor"),
12518
- image: createSliceHandle(stateSource, binding.deviceId, "image"),
12519
- lawnMowerControl: createSliceHandle(stateSource, binding.deviceId, "lawn-mower-control"),
12520
- lockControl: createSliceHandle(stateSource, binding.deviceId, "lock-control"),
12521
- mediaPlayer: createSliceHandle(stateSource, binding.deviceId, "media-player"),
12522
- motion: createSliceHandle(stateSource, binding.deviceId, "motion"),
12523
- motionTrigger: createSliceHandle(stateSource, binding.deviceId, "motion-trigger"),
12524
- motionZones: createSliceHandle(stateSource, binding.deviceId, "motion-zones"),
12525
- nativeObjectDetection: createSliceHandle(stateSource, binding.deviceId, "native-object-detection"),
12526
- notifier: createSliceHandle(stateSource, binding.deviceId, "notifier"),
12527
- numericSensor: createSliceHandle(stateSource, binding.deviceId, "numeric-sensor"),
12528
- powerMeter: createSliceHandle(stateSource, binding.deviceId, "power-meter"),
12529
- presence: createSliceHandle(stateSource, binding.deviceId, "presence"),
12530
- pressureSensor: createSliceHandle(stateSource, binding.deviceId, "pressure-sensor"),
12531
- privacyMask: createSliceHandle(stateSource, binding.deviceId, "privacy-mask"),
12532
- ptzAutotrack: createSliceHandle(stateSource, binding.deviceId, "ptz-autotrack"),
12533
- scriptRunner: createSliceHandle(stateSource, binding.deviceId, "script-runner"),
12534
- smoke: createSliceHandle(stateSource, binding.deviceId, "smoke"),
12535
- streamParams: createSliceHandle(stateSource, binding.deviceId, "stream-params"),
12536
- switch: createSliceHandle(stateSource, binding.deviceId, "switch"),
12537
- tamper: createSliceHandle(stateSource, binding.deviceId, "tamper"),
12538
- temperatureSensor: createSliceHandle(stateSource, binding.deviceId, "temperature-sensor"),
12539
- update: createSliceHandle(stateSource, binding.deviceId, "update"),
12540
- vacuumControl: createSliceHandle(stateSource, binding.deviceId, "vacuum-control"),
12541
- valve: createSliceHandle(stateSource, binding.deviceId, "valve"),
12542
- vibration: createSliceHandle(stateSource, binding.deviceId, "vibration"),
12543
- waterHeater: createSliceHandle(stateSource, binding.deviceId, "water-heater"),
12544
- weather: createSliceHandle(stateSource, binding.deviceId, "weather"),
12545
- zoneAnalytics: createSliceHandle(stateSource, binding.deviceId, "zone-analytics"),
12546
- zoneRules: createSliceHandle(stateSource, binding.deviceId, "zone-rules"),
12547
- zones: createSliceHandle(stateSource, binding.deviceId, "zones")
12548
- },
12549
- accessories: {
12550
- setChildHidden: (input) => dispatch("accessories", "accessories", "setChildHidden", "mutation", input),
12551
- getStatus: (input) => dispatch("accessories", "accessories", "getStatus", "query", input)
12552
- },
12553
- airQualitySensor: { getStatus: (input) => dispatch("air-quality-sensor", "airQualitySensor", "getStatus", "query", input) },
12554
- alarmPanel: {
12555
- arm: (input) => dispatch("alarm-panel", "alarmPanel", "arm", "mutation", input),
12556
- disarm: (input) => dispatch("alarm-panel", "alarmPanel", "disarm", "mutation", input),
12557
- trigger: (input) => dispatch("alarm-panel", "alarmPanel", "trigger", "mutation", input),
12558
- getStatus: (input) => dispatch("alarm-panel", "alarmPanel", "getStatus", "query", input)
12559
- },
12560
- ambientLightSensor: { getStatus: (input) => dispatch("ambient-light-sensor", "ambientLightSensor", "getStatus", "query", input) },
12561
- audioAnalysis: {
12562
- resolveDeviceSettings: (input) => dispatch("audio-analysis", "audioAnalysis", "resolveDeviceSettings", "query", input),
12563
- getDeviceSettingsContribution: (input) => dispatch("audio-analysis", "audioAnalysis", "getDeviceSettingsContribution", "query", input),
12564
- getDeviceLiveContribution: (input) => dispatch("audio-analysis", "audioAnalysis", "getDeviceLiveContribution", "query", input),
12565
- applyDeviceSettingsPatch: (input) => dispatch("audio-analysis", "audioAnalysis", "applyDeviceSettingsPatch", "mutation", input)
12566
- },
12567
- audioMetrics: {
12568
- getCurrentSnapshot: (input) => dispatch("audio-metrics", "audioMetrics", "getCurrentSnapshot", "query", input),
12569
- getHistory: (input) => dispatch("audio-metrics", "audioMetrics", "getHistory", "query", input)
12570
- },
12571
- automationControl: {
12572
- enable: (input) => dispatch("automation-control", "automationControl", "enable", "mutation", input),
12573
- disable: (input) => dispatch("automation-control", "automationControl", "disable", "mutation", input),
12574
- trigger: (input) => dispatch("automation-control", "automationControl", "trigger", "mutation", input),
12575
- getStatus: (input) => dispatch("automation-control", "automationControl", "getStatus", "query", input)
12576
- },
12577
- battery: {
12578
- wakeForStream: (input) => dispatch("battery", "battery", "wakeForStream", "mutation", input),
12579
- getStatus: (input) => dispatch("battery", "battery", "getStatus", "query", input)
12580
- },
12581
- binary: { getStatus: (input) => dispatch("binary", "binary", "getStatus", "query", input) },
12582
- brightness: {
12583
- setBrightness: (input) => dispatch("brightness", "brightness", "setBrightness", "mutation", input),
12584
- getStatus: (input) => dispatch("brightness", "brightness", "getStatus", "query", input)
12585
- },
12586
- button: { press: (input) => dispatch("button", "button", "press", "mutation", input) },
12587
- cameraCredentials: {
12588
- getCredentials: (input) => dispatch("camera-credentials", "cameraCredentials", "getCredentials", "query", input),
12589
- getStatus: (input) => dispatch("camera-credentials", "cameraCredentials", "getStatus", "query", input)
12590
- },
12591
- cameraStreams: {
12592
- getCameraStreams: (input) => dispatch("camera-streams", "cameraStreams", "getCameraStreams", "query", input),
12593
- getBrokerStreams: (input) => dispatch("camera-streams", "cameraStreams", "getBrokerStreams", "query", input),
12594
- getRtspEntries: (input) => dispatch("camera-streams", "cameraStreams", "getRtspEntries", "query", input),
12595
- getProfileRtspEntries: (input) => dispatch("camera-streams", "cameraStreams", "getProfileRtspEntries", "query", input),
12596
- pickStream: (input) => dispatch("camera-streams", "cameraStreams", "pickStream", "query", input)
12597
- },
12598
- carbonMonoxide: { getStatus: (input) => dispatch("carbon-monoxide", "carbonMonoxide", "getStatus", "query", input) },
12599
- climateControl: {
12600
- setMode: (input) => dispatch("climate-control", "climateControl", "setMode", "mutation", input),
12601
- setFanMode: (input) => dispatch("climate-control", "climateControl", "setFanMode", "mutation", input),
12602
- setPreset: (input) => dispatch("climate-control", "climateControl", "setPreset", "mutation", input),
12603
- setTarget: (input) => dispatch("climate-control", "climateControl", "setTarget", "mutation", input),
12604
- setTargetRange: (input) => dispatch("climate-control", "climateControl", "setTargetRange", "mutation", input),
12605
- setTargetHumidity: (input) => dispatch("climate-control", "climateControl", "setTargetHumidity", "mutation", input),
12606
- getStatus: (input) => dispatch("climate-control", "climateControl", "getStatus", "query", input)
12607
- },
12608
- color: {
12609
- setColor: (input) => dispatch("color", "color", "setColor", "mutation", input),
12610
- getStatus: (input) => dispatch("color", "color", "getStatus", "query", input)
12611
- },
12612
- connectivity: { getStatus: (input) => dispatch("connectivity", "connectivity", "getStatus", "query", input) },
12613
- consumables: {
12614
- reset: (input) => dispatch("consumables", "consumables", "reset", "mutation", input),
12615
- getStatus: (input) => dispatch("consumables", "consumables", "getStatus", "query", input)
12616
- },
12617
- contact: { getStatus: (input) => dispatch("contact", "contact", "getStatus", "query", input) },
12618
- control: {
12619
- setValue: (input) => dispatch("control", "control", "setValue", "mutation", input),
12620
- getStatus: (input) => dispatch("control", "control", "getStatus", "query", input)
12621
- },
12622
- cover: {
12623
- open: (input) => dispatch("cover", "cover", "open", "mutation", input),
12624
- close: (input) => dispatch("cover", "cover", "close", "mutation", input),
12625
- stop: (input) => dispatch("cover", "cover", "stop", "mutation", input),
12626
- setPosition: (input) => dispatch("cover", "cover", "setPosition", "mutation", input),
12627
- setTiltPosition: (input) => dispatch("cover", "cover", "setTiltPosition", "mutation", input),
12628
- getStatus: (input) => dispatch("cover", "cover", "getStatus", "query", input)
12629
- },
12630
- detectionPipeline: {
12631
- getDeviceSettingsContribution: (input) => dispatch("detection-pipeline", "detectionPipeline", "getDeviceSettingsContribution", "query", input),
12632
- getDeviceLiveContribution: (input) => dispatch("detection-pipeline", "detectionPipeline", "getDeviceLiveContribution", "query", input),
12633
- applyDeviceSettingsPatch: (input) => dispatch("detection-pipeline", "detectionPipeline", "applyDeviceSettingsPatch", "mutation", input)
12634
- },
12635
- deviceDiscovery: {
12636
- listDiscovered: (input) => dispatch("device-discovery", "deviceDiscovery", "listDiscovered", "query", input),
12637
- refreshDiscovery: (input) => dispatch("device-discovery", "deviceDiscovery", "refreshDiscovery", "mutation", input),
12638
- adoptDevice: (input) => dispatch("device-discovery", "deviceDiscovery", "adoptDevice", "mutation", input),
12639
- releaseDevice: (input) => dispatch("device-discovery", "deviceDiscovery", "releaseDevice", "mutation", input),
12640
- getStatus: (input) => dispatch("device-discovery", "deviceDiscovery", "getStatus", "query", input)
12641
- },
12642
- deviceOps: {
12643
- getStreamSources: (input) => dispatch("device-ops", "deviceOps", "getStreamSources", "query", input),
12644
- getConfigEntries: (input) => dispatch("device-ops", "deviceOps", "getConfigEntries", "query", input),
12645
- setConfig: (input) => dispatch("device-ops", "deviceOps", "setConfig", "mutation", input),
12646
- runAction: (input) => dispatch("device-ops", "deviceOps", "runAction", "mutation", input),
12647
- removeDevice: (input) => dispatch("device-ops", "deviceOps", "removeDevice", "mutation", input),
12648
- getSettingsSchema: (input) => dispatch("device-ops", "deviceOps", "getSettingsSchema", "query", input),
12649
- getRawState: (input) => dispatch("device-ops", "deviceOps", "getRawState", "query", input)
12650
- },
12651
- deviceStatus: { getStatus: (input) => dispatch("device-status", "deviceStatus", "getStatus", "query", input) },
12652
- doorbell: { getStatus: (input) => dispatch("doorbell", "doorbell", "getStatus", "query", input) },
12653
- enumSensor: { getStatus: (input) => dispatch("enum-sensor", "enumSensor", "getStatus", "query", input) },
12654
- eventEmitter: { getStatus: (input) => dispatch("event-emitter", "eventEmitter", "getStatus", "query", input) },
12655
- events: {
12656
- getEvents: (input) => dispatch("events", "events", "getEvents", "query", input),
12657
- getEventThumbnail: (input) => dispatch("events", "events", "getEventThumbnail", "query", input),
12658
- getEventClipUrl: (input) => dispatch("events", "events", "getEventClipUrl", "query", input)
12659
- },
12660
- fanControl: {
12661
- setPercentage: (input) => dispatch("fan-control", "fanControl", "setPercentage", "mutation", input),
12662
- setPreset: (input) => dispatch("fan-control", "fanControl", "setPreset", "mutation", input),
12663
- setDirection: (input) => dispatch("fan-control", "fanControl", "setDirection", "mutation", input),
12664
- setOscillating: (input) => dispatch("fan-control", "fanControl", "setOscillating", "mutation", input),
12665
- getStatus: (input) => dispatch("fan-control", "fanControl", "getStatus", "query", input)
12666
- },
12667
- featureProbe: { getStatus: (input) => dispatch("feature-probe", "featureProbe", "getStatus", "query", input) },
12668
- flood: { getStatus: (input) => dispatch("flood", "flood", "getStatus", "query", input) },
12669
- gas: { getStatus: (input) => dispatch("gas", "gas", "getStatus", "query", input) },
12670
- humidifier: {
12671
- setOn: (input) => dispatch("humidifier", "humidifier", "setOn", "mutation", input),
12672
- setTargetHumidity: (input) => dispatch("humidifier", "humidifier", "setTargetHumidity", "mutation", input),
12673
- setMode: (input) => dispatch("humidifier", "humidifier", "setMode", "mutation", input),
12674
- getStatus: (input) => dispatch("humidifier", "humidifier", "getStatus", "query", input)
12675
- },
12676
- humiditySensor: { getStatus: (input) => dispatch("humidity-sensor", "humiditySensor", "getStatus", "query", input) },
12677
- image: { getStatus: (input) => dispatch("image", "image", "getStatus", "query", input) },
12678
- intercom: {
12679
- startSession: (input) => dispatch("intercom", "intercom", "startSession", "mutation", input),
12680
- handleAnswer: (input) => dispatch("intercom", "intercom", "handleAnswer", "mutation", input),
12681
- stopSession: (input) => dispatch("intercom", "intercom", "stopSession", "mutation", input),
12682
- startTalkSession: (input) => dispatch("intercom", "intercom", "startTalkSession", "mutation", input),
12683
- pushTalkAudio: (input) => dispatch("intercom", "intercom", "pushTalkAudio", "mutation", input),
12684
- endTalkSession: (input) => dispatch("intercom", "intercom", "endTalkSession", "mutation", input),
12685
- getStatus: (input) => dispatch("intercom", "intercom", "getStatus", "query", input)
12686
- },
12687
- lawnMowerControl: {
12688
- startMowing: (input) => dispatch("lawn-mower-control", "lawnMowerControl", "startMowing", "mutation", input),
12689
- pause: (input) => dispatch("lawn-mower-control", "lawnMowerControl", "pause", "mutation", input),
12690
- dock: (input) => dispatch("lawn-mower-control", "lawnMowerControl", "dock", "mutation", input),
12691
- getStatus: (input) => dispatch("lawn-mower-control", "lawnMowerControl", "getStatus", "query", input)
12692
- },
12693
- lockControl: {
12694
- lock: (input) => dispatch("lock-control", "lockControl", "lock", "mutation", input),
12695
- unlock: (input) => dispatch("lock-control", "lockControl", "unlock", "mutation", input),
12696
- open: (input) => dispatch("lock-control", "lockControl", "open", "mutation", input),
12697
- getStatus: (input) => dispatch("lock-control", "lockControl", "getStatus", "query", input)
12698
- },
12699
- mediaPlayer: {
12700
- play: (input) => dispatch("media-player", "mediaPlayer", "play", "mutation", input),
12701
- pause: (input) => dispatch("media-player", "mediaPlayer", "pause", "mutation", input),
12702
- stop: (input) => dispatch("media-player", "mediaPlayer", "stop", "mutation", input),
12703
- next: (input) => dispatch("media-player", "mediaPlayer", "next", "mutation", input),
12704
- previous: (input) => dispatch("media-player", "mediaPlayer", "previous", "mutation", input),
12705
- seek: (input) => dispatch("media-player", "mediaPlayer", "seek", "mutation", input),
12706
- setVolume: (input) => dispatch("media-player", "mediaPlayer", "setVolume", "mutation", input),
12707
- setMute: (input) => dispatch("media-player", "mediaPlayer", "setMute", "mutation", input),
12708
- setShuffle: (input) => dispatch("media-player", "mediaPlayer", "setShuffle", "mutation", input),
12709
- setRepeat: (input) => dispatch("media-player", "mediaPlayer", "setRepeat", "mutation", input),
12710
- selectSource: (input) => dispatch("media-player", "mediaPlayer", "selectSource", "mutation", input),
12711
- playMedia: (input) => dispatch("media-player", "mediaPlayer", "playMedia", "mutation", input),
12712
- getStatus: (input) => dispatch("media-player", "mediaPlayer", "getStatus", "query", input)
12713
- },
12714
- motion: {
12715
- isDetected: (input) => dispatch("motion", "motion", "isDetected", "query", input),
12716
- getStatus: (input) => dispatch("motion", "motion", "getStatus", "query", input)
12717
- },
12718
- motionDetection: {
12719
- analyze: (input) => dispatch("motion-detection", "motionDetection", "analyze", "mutation", input),
12720
- removeCamera: (input) => dispatch("motion-detection", "motionDetection", "removeCamera", "mutation", input),
12721
- reset: (input) => dispatch("motion-detection", "motionDetection", "reset", "mutation", input),
12722
- getDeviceSettingsContribution: (input) => dispatch("motion-detection", "motionDetection", "getDeviceSettingsContribution", "query", input),
12723
- getDeviceLiveContribution: (input) => dispatch("motion-detection", "motionDetection", "getDeviceLiveContribution", "query", input),
12724
- applyDeviceSettingsPatch: (input) => dispatch("motion-detection", "motionDetection", "applyDeviceSettingsPatch", "mutation", input)
12725
- },
12726
- motionTrigger: {
12727
- setMotionTrigger: (input) => dispatch("motion-trigger", "motionTrigger", "setMotionTrigger", "mutation", input),
12728
- getStatus: (input) => dispatch("motion-trigger", "motionTrigger", "getStatus", "query", input)
12729
- },
12730
- motionZones: {
12731
- getOptions: (input) => dispatch("motion-zones", "motionZones", "getOptions", "query", input),
12732
- setZone: (input) => dispatch("motion-zones", "motionZones", "setZone", "mutation", input),
12733
- getStatus: (input) => dispatch("motion-zones", "motionZones", "getStatus", "query", input)
12734
- },
12735
- nativeObjectDetection: {
12736
- setEnabled: (input) => dispatch("native-object-detection", "nativeObjectDetection", "setEnabled", "mutation", input),
12737
- getStatus: (input) => dispatch("native-object-detection", "nativeObjectDetection", "getStatus", "query", input)
12738
- },
12739
- notifier: {
12740
- send: (input) => dispatch("notifier", "notifier", "send", "mutation", input),
12741
- cancel: (input) => dispatch("notifier", "notifier", "cancel", "mutation", input),
12742
- getStatus: (input) => dispatch("notifier", "notifier", "getStatus", "query", input)
12743
- },
12744
- numericSensor: { getStatus: (input) => dispatch("numeric-sensor", "numericSensor", "getStatus", "query", input) },
12745
- osd: {
12746
- setOverlay: (input) => dispatch("osd", "osd", "setOverlay", "mutation", input),
12747
- getStatus: (input) => dispatch("osd", "osd", "getStatus", "query", input)
12748
- },
12749
- pipelineAnalytics: {
12750
- getActiveTracks: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getActiveTracks", "query", input),
12751
- getTrack: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrack", "query", input),
12752
- listTracks: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "listTracks", "query", input),
12753
- clearTracks: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "clearTracks", "mutation", input),
12754
- getMotionEvents: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getMotionEvents", "query", input),
12755
- getObjectEvents: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getObjectEvents", "query", input),
12756
- getAudioEvents: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getAudioEvents", "query", input),
12757
- getEventDensity: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getEventDensity", "query", input),
12758
- pruneEventsBefore: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "pruneEventsBefore", "mutation", input),
12759
- getEventMedia: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getEventMedia", "query", input),
12760
- getTrackMedia: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getTrackMedia", "query", input),
12761
- getDeviceSettingsContribution: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getDeviceSettingsContribution", "query", input),
12762
- getDeviceLiveContribution: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "getDeviceLiveContribution", "query", input),
12763
- applyDeviceSettingsPatch: (input) => dispatch("pipeline-analytics", "pipelineAnalytics", "applyDeviceSettingsPatch", "mutation", input)
12764
- },
12765
- powerMeter: { getStatus: (input) => dispatch("power-meter", "powerMeter", "getStatus", "query", input) },
12766
- presence: { getStatus: (input) => dispatch("presence", "presence", "getStatus", "query", input) },
12767
- pressureSensor: { getStatus: (input) => dispatch("pressure-sensor", "pressureSensor", "getStatus", "query", input) },
12768
- privacyMask: {
12769
- getOptions: (input) => dispatch("privacy-mask", "privacyMask", "getOptions", "query", input),
12770
- setMask: (input) => dispatch("privacy-mask", "privacyMask", "setMask", "mutation", input),
12771
- getStatus: (input) => dispatch("privacy-mask", "privacyMask", "getStatus", "query", input)
12772
- },
12773
- ptz: {
12774
- move: (input) => dispatch("ptz", "ptz", "move", "mutation", input),
12775
- continuousMove: (input) => dispatch("ptz", "ptz", "continuousMove", "mutation", input),
12776
- stop: (input) => dispatch("ptz", "ptz", "stop", "mutation", input),
12777
- getPresets: (input) => dispatch("ptz", "ptz", "getPresets", "query", input),
12778
- goToPreset: (input) => dispatch("ptz", "ptz", "goToPreset", "mutation", input),
12779
- savePreset: (input) => dispatch("ptz", "ptz", "savePreset", "mutation", input),
12780
- deletePreset: (input) => dispatch("ptz", "ptz", "deletePreset", "mutation", input),
12781
- getOptions: (input) => dispatch("ptz", "ptz", "getOptions", "query", input),
12782
- goHome: (input) => dispatch("ptz", "ptz", "goHome", "mutation", input),
12783
- getPosition: (input) => dispatch("ptz", "ptz", "getPosition", "query", input),
12784
- setAutofocus: (input) => dispatch("ptz", "ptz", "setAutofocus", "mutation", input),
12785
- getStatus: (input) => dispatch("ptz", "ptz", "getStatus", "query", input)
12786
- },
12787
- ptzAutotrack: {
12788
- getStatus: (input) => dispatch("ptz-autotrack", "ptzAutotrack", "getStatus", "query", input),
12789
- setEnabled: (input) => dispatch("ptz-autotrack", "ptzAutotrack", "setEnabled", "mutation", input),
12790
- getSettings: (input) => dispatch("ptz-autotrack", "ptzAutotrack", "getSettings", "query", input),
12791
- setSettings: (input) => dispatch("ptz-autotrack", "ptzAutotrack", "setSettings", "mutation", input)
12792
- },
12793
- reboot: { reboot: (input) => dispatch("reboot", "reboot", "reboot", "mutation", input) },
12794
- scriptRunner: {
12795
- run: (input) => dispatch("script-runner", "scriptRunner", "run", "mutation", input),
12796
- stop: (input) => dispatch("script-runner", "scriptRunner", "stop", "mutation", input),
12797
- getStatus: (input) => dispatch("script-runner", "scriptRunner", "getStatus", "query", input)
12798
- },
12799
- smoke: { getStatus: (input) => dispatch("smoke", "smoke", "getStatus", "query", input) },
12800
- snapshot: {
12801
- getSnapshot: (input) => dispatch("snapshot", "snapshot", "getSnapshot", "query", input),
12802
- invalidateCache: (input) => dispatch("snapshot", "snapshot", "invalidateCache", "mutation", input),
12803
- getStatus: (input) => dispatch("snapshot", "snapshot", "getStatus", "query", input),
12804
- getDeviceSettingsContribution: (input) => dispatch("snapshot", "snapshot", "getDeviceSettingsContribution", "query", input),
12805
- getDeviceLiveContribution: (input) => dispatch("snapshot", "snapshot", "getDeviceLiveContribution", "query", input),
12806
- applyDeviceSettingsPatch: (input) => dispatch("snapshot", "snapshot", "applyDeviceSettingsPatch", "mutation", input)
12807
- },
12808
- streamCatalog: { getCatalog: (input) => dispatch("stream-catalog", "streamCatalog", "getCatalog", "query", input) },
12809
- streamParams: {
12810
- getOptions: (input) => dispatch("stream-params", "streamParams", "getOptions", "query", input),
12811
- setProfile: (input) => dispatch("stream-params", "streamParams", "setProfile", "mutation", input),
12812
- getConfigSchema: (input) => dispatch("stream-params", "streamParams", "getConfigSchema", "query", input),
12813
- getStatus: (input) => dispatch("stream-params", "streamParams", "getStatus", "query", input)
12814
- },
12815
- switch: {
12816
- setState: (input) => dispatch("switch", "switch", "setState", "mutation", input),
12817
- getStatus: (input) => dispatch("switch", "switch", "getStatus", "query", input)
12818
- },
12819
- tamper: { getStatus: (input) => dispatch("tamper", "tamper", "getStatus", "query", input) },
12820
- temperatureSensor: { getStatus: (input) => dispatch("temperature-sensor", "temperatureSensor", "getStatus", "query", input) },
12821
- update: {
12822
- installUpdate: (input) => dispatch("update", "update", "installUpdate", "mutation", input),
12823
- getStatus: (input) => dispatch("update", "update", "getStatus", "query", input)
12824
- },
12825
- vacuumControl: {
12826
- start: (input) => dispatch("vacuum-control", "vacuumControl", "start", "mutation", input),
12827
- pause: (input) => dispatch("vacuum-control", "vacuumControl", "pause", "mutation", input),
12828
- stop: (input) => dispatch("vacuum-control", "vacuumControl", "stop", "mutation", input),
12829
- returnToBase: (input) => dispatch("vacuum-control", "vacuumControl", "returnToBase", "mutation", input),
12830
- locate: (input) => dispatch("vacuum-control", "vacuumControl", "locate", "mutation", input),
12831
- setFanSpeed: (input) => dispatch("vacuum-control", "vacuumControl", "setFanSpeed", "mutation", input),
12832
- getStatus: (input) => dispatch("vacuum-control", "vacuumControl", "getStatus", "query", input)
12833
- },
12834
- valve: {
12835
- open: (input) => dispatch("valve", "valve", "open", "mutation", input),
12836
- close: (input) => dispatch("valve", "valve", "close", "mutation", input),
12837
- stop: (input) => dispatch("valve", "valve", "stop", "mutation", input),
12838
- setPosition: (input) => dispatch("valve", "valve", "setPosition", "mutation", input),
12839
- getStatus: (input) => dispatch("valve", "valve", "getStatus", "query", input)
12840
- },
12841
- vibration: { getStatus: (input) => dispatch("vibration", "vibration", "getStatus", "query", input) },
12842
- videoclips: {
12843
- listClips: (input) => dispatch("videoclips", "videoclips", "listClips", "query", input),
12844
- getClipPlayback: (input) => dispatch("videoclips", "videoclips", "getClipPlayback", "query", input)
12845
- },
12846
- waterHeater: {
12847
- setTargetTemp: (input) => dispatch("water-heater", "waterHeater", "setTargetTemp", "mutation", input),
12848
- setOperationMode: (input) => dispatch("water-heater", "waterHeater", "setOperationMode", "mutation", input),
12849
- setAway: (input) => dispatch("water-heater", "waterHeater", "setAway", "mutation", input),
12850
- getStatus: (input) => dispatch("water-heater", "waterHeater", "getStatus", "query", input)
12851
- },
12852
- weather: { getStatus: (input) => dispatch("weather", "weather", "getStatus", "query", input) },
12853
- webrtcSession: {
12854
- listStreams: (input) => dispatch("webrtc-session", "webrtcSession", "listStreams", "query", input),
12855
- createSession: (input) => dispatch("webrtc-session", "webrtcSession", "createSession", "mutation", input),
12856
- handleOffer: (input) => dispatch("webrtc-session", "webrtcSession", "handleOffer", "mutation", input),
12857
- handleAnswer: (input) => dispatch("webrtc-session", "webrtcSession", "handleAnswer", "mutation", input),
12858
- addIceCandidate: (input) => dispatch("webrtc-session", "webrtcSession", "addIceCandidate", "mutation", input),
12859
- getIceCandidates: (input) => dispatch("webrtc-session", "webrtcSession", "getIceCandidates", "query", input),
12860
- closeSession: (input) => dispatch("webrtc-session", "webrtcSession", "closeSession", "mutation", input),
12861
- hasAdaptiveBitrate: (input) => dispatch("webrtc-session", "webrtcSession", "hasAdaptiveBitrate", "query", input),
12862
- getSessionState: (input) => dispatch("webrtc-session", "webrtcSession", "getSessionState", "query", input)
12863
- },
12864
- zoneAnalytics: {
12865
- getCurrentSnapshot: (input) => dispatch("zone-analytics", "zoneAnalytics", "getCurrentSnapshot", "query", input),
12866
- getZoneHistory: (input) => dispatch("zone-analytics", "zoneAnalytics", "getZoneHistory", "query", input),
12867
- getCameraHistory: (input) => dispatch("zone-analytics", "zoneAnalytics", "getCameraHistory", "query", input),
12868
- getUnzonedHistory: (input) => dispatch("zone-analytics", "zoneAnalytics", "getUnzonedHistory", "query", input)
12869
- },
12870
- zoneRules: {
12871
- listRules: (input) => dispatch("zone-rules", "zoneRules", "listRules", "query", input),
12872
- setRules: (input) => dispatch("zone-rules", "zoneRules", "setRules", "mutation", input)
12873
- },
12874
- zones: {
12875
- listZones: (input) => dispatch("zones", "zones", "listZones", "query", input),
12876
- addZone: (input) => dispatch("zones", "zones", "addZone", "mutation", input),
12877
- removeZone: (input) => dispatch("zones", "zones", "removeZone", "mutation", input),
12878
- updateZone: (input) => dispatch("zones", "zones", "updateZone", "mutation", input)
12879
- },
12880
- addonSettings: {
12881
- getDeviceSettings: (input) => dispatchSystem("addonSettings", "getDeviceSettings", "query", input),
12882
- updateDeviceSettings: (input) => dispatchSystem("addonSettings", "updateDeviceSettings", "mutation", input)
12883
- },
12884
- cameraPipelineConfig: {
12885
- getDeviceSettingsContribution: (input) => dispatchSystem("cameraPipelineConfig", "getDeviceSettingsContribution", "query", input),
12886
- getDeviceLiveContribution: (input) => dispatchSystem("cameraPipelineConfig", "getDeviceLiveContribution", "query", input),
12887
- applyDeviceSettingsPatch: (input) => dispatchSystem("cameraPipelineConfig", "applyDeviceSettingsPatch", "mutation", input)
12888
- },
12889
- deviceAdoption: { getStatus: (input) => dispatchSystem("deviceAdoption", "getStatus", "query", input) },
12890
- deviceExport: {
12891
- getDeviceSettingsContribution: (input) => dispatchSystem("deviceExport", "getDeviceSettingsContribution", "query", input),
12892
- getDeviceLiveContribution: (input) => dispatchSystem("deviceExport", "getDeviceLiveContribution", "query", input),
12893
- applyDeviceSettingsPatch: (input) => dispatchSystem("deviceExport", "applyDeviceSettingsPatch", "mutation", input)
12894
- },
12895
- deviceManager: {
12896
- loadConfig: (input) => dispatchSystem("deviceManager", "loadConfig", "query", input),
12897
- loadRuntimeState: (input) => dispatchSystem("deviceManager", "loadRuntimeState", "query", input),
12898
- loadMeta: (input) => dispatchSystem("deviceManager", "loadMeta", "query", input),
12899
- setName: (input) => dispatchSystem("deviceManager", "setName", "mutation", input),
12900
- setLocation: (input) => dispatchSystem("deviceManager", "setLocation", "mutation", input),
12901
- setType: (input) => dispatchSystem("deviceManager", "setType", "mutation", input),
12902
- setIntegrationId: (input) => dispatchSystem("deviceManager", "setIntegrationId", "mutation", input),
12903
- setLinkDeviceId: (input) => dispatchSystem("deviceManager", "setLinkDeviceId", "mutation", input),
12904
- setPrimaryChildEntityId: (input) => dispatchSystem("deviceManager", "setPrimaryChildEntityId", "mutation", input),
12905
- setChildLayout: (input) => dispatchSystem("deviceManager", "setChildLayout", "mutation", input),
12906
- setDeviceLinks: (input) => dispatchSystem("deviceManager", "setDeviceLinks", "mutation", input),
12907
- getWireableFields: (input) => dispatchSystem("deviceManager", "getWireableFields", "query", input),
12908
- setRole: (input) => dispatchSystem("deviceManager", "setRole", "mutation", input),
12909
- applyInitialMeta: (input) => dispatchSystem("deviceManager", "applyInitialMeta", "mutation", input),
12910
- setMetadata: (input) => dispatchSystem("deviceManager", "setMetadata", "mutation", input),
12911
- setDisabled: (input) => dispatchSystem("deviceManager", "setDisabled", "mutation", input),
12912
- getDevice: (input) => dispatchSystem("deviceManager", "getDevice", "query", input),
12913
- getStreamSources: (input) => dispatchSystem("deviceManager", "getStreamSources", "query", input),
12914
- getConfigSchema: (input) => dispatchSystem("deviceManager", "getConfigSchema", "query", input),
12915
- getSettingsSchema: (input) => dispatchSystem("deviceManager", "getSettingsSchema", "query", input),
12916
- updateConfig: (input) => dispatchSystem("deviceManager", "updateConfig", "mutation", input),
12917
- enable: (input) => dispatchSystem("deviceManager", "enable", "mutation", input),
12918
- disable: (input) => dispatchSystem("deviceManager", "disable", "mutation", input),
12919
- remove: (input) => dispatchSystem("deviceManager", "remove", "mutation", input),
12920
- getStreamProfileMap: (input) => dispatchSystem("deviceManager", "getStreamProfileMap", "query", input),
12921
- setStreamProfileMap: (input) => dispatchSystem("deviceManager", "setStreamProfileMap", "mutation", input),
12922
- probeStreams: (input) => dispatchSystem("deviceManager", "probeStreams", "mutation", input),
12923
- getBindings: (input) => dispatchSystem("deviceManager", "getBindings", "query", input),
12924
- getAllBindings: (input) => dispatchSystem("deviceManager", "getAllBindings", "query", input),
12925
- setWrapperActive: (input) => dispatchSystem("deviceManager", "setWrapperActive", "mutation", input),
12926
- getDeviceSettingsAggregate: (input) => dispatchSystem("deviceManager", "getDeviceSettingsAggregate", "query", input),
12927
- getDeviceLiveInfoAggregate: (input) => dispatchSystem("deviceManager", "getDeviceLiveInfoAggregate", "query", input),
12928
- getDeviceAggregate: (input) => dispatchSystem("deviceManager", "getDeviceAggregate", "query", input),
12929
- runDeviceAction: (input) => dispatchSystem("deviceManager", "runDeviceAction", "mutation", input),
12930
- updateDeviceField: (input) => dispatchSystem("deviceManager", "updateDeviceField", "mutation", input),
12931
- updateDeviceFieldsBatch: (input) => dispatchSystem("deviceManager", "updateDeviceFieldsBatch", "mutation", input),
12932
- testField: (input) => dispatchSystem("deviceManager", "testField", "mutation", input),
12933
- getDeviceStatusAggregate: (input) => dispatchSystem("deviceManager", "getDeviceStatusAggregate", "query", input)
12934
- },
12935
- deviceState: {
12936
- getSnapshot: (input) => dispatchSystem("deviceState", "getSnapshot", "query", input),
12937
- getCapSlice: (input) => dispatchSystem("deviceState", "getCapSlice", "query", input),
12938
- setCapSlice: (input) => dispatchSystem("deviceState", "setCapSlice", "mutation", input)
12939
- },
12940
- faceGallery: { getFaceByTrack: (input) => dispatchSystem("faceGallery", "getFaceByTrack", "query", input) },
12941
- networkQuality: {
12942
- getDeviceStats: (input) => dispatchSystem("networkQuality", "getDeviceStats", "query", input),
12943
- reportClientStats: (input) => dispatchSystem("networkQuality", "reportClientStats", "mutation", input)
12944
- },
12945
- pipelineExecutor: {
12946
- runPipeline: (input) => dispatchSystem("pipelineExecutor", "runPipeline", "mutation", input),
12947
- runPipelineBatch: (input) => dispatchSystem("pipelineExecutor", "runPipelineBatch", "mutation", input)
12948
- },
12949
- pipelineOrchestrator: {
12950
- assignPipeline: (input) => dispatchSystem("pipelineOrchestrator", "assignPipeline", "mutation", input),
12951
- unassignPipeline: (input) => dispatchSystem("pipelineOrchestrator", "unassignPipeline", "mutation", input),
12952
- getPipelineAssignment: (input) => dispatchSystem("pipelineOrchestrator", "getPipelineAssignment", "query", input),
12953
- getCameraMetrics: (input) => dispatchSystem("pipelineOrchestrator", "getCameraMetrics", "query", input),
12954
- assignDecoder: (input) => dispatchSystem("pipelineOrchestrator", "assignDecoder", "mutation", input),
12955
- unassignDecoder: (input) => dispatchSystem("pipelineOrchestrator", "unassignDecoder", "mutation", input),
12956
- assignAudio: (input) => dispatchSystem("pipelineOrchestrator", "assignAudio", "mutation", input),
12957
- unassignAudio: (input) => dispatchSystem("pipelineOrchestrator", "unassignAudio", "mutation", input),
12958
- getAudioAssignment: (input) => dispatchSystem("pipelineOrchestrator", "getAudioAssignment", "query", input),
12959
- getAudioAssignments: (input) => dispatchSystem("pipelineOrchestrator", "getAudioAssignments", "query", input),
12960
- getDecoderAssignment: (input) => dispatchSystem("pipelineOrchestrator", "getDecoderAssignment", "query", input),
12961
- getCameraSettings: (input) => dispatchSystem("pipelineOrchestrator", "getCameraSettings", "query", input),
12962
- setCameraStepToggle: (input) => dispatchSystem("pipelineOrchestrator", "setCameraStepToggle", "mutation", input),
12963
- getCameraStepOverrides: (input) => dispatchSystem("pipelineOrchestrator", "getCameraStepOverrides", "query", input),
12964
- setCameraStepOverride: (input) => dispatchSystem("pipelineOrchestrator", "setCameraStepOverride", "mutation", input),
12965
- setCameraPipelineForAgent: (input) => dispatchSystem("pipelineOrchestrator", "setCameraPipelineForAgent", "mutation", input),
12966
- resolvePipeline: (input) => dispatchSystem("pipelineOrchestrator", "resolvePipeline", "query", input),
12967
- getCameraStatus: (input) => dispatchSystem("pipelineOrchestrator", "getCameraStatus", "query", input),
12968
- getDeviceSettingsContribution: (input) => dispatchSystem("pipelineOrchestrator", "getDeviceSettingsContribution", "query", input),
12969
- getDeviceLiveContribution: (input) => dispatchSystem("pipelineOrchestrator", "getDeviceLiveContribution", "query", input),
12970
- applyDeviceSettingsPatch: (input) => dispatchSystem("pipelineOrchestrator", "applyDeviceSettingsPatch", "mutation", input)
12971
- },
12972
- pipelineRunner: {
12973
- detachCamera: (input) => dispatchSystem("pipelineRunner", "detachCamera", "mutation", input),
12974
- getCameraMetrics: (input) => dispatchSystem("pipelineRunner", "getCameraMetrics", "query", input)
12975
- },
12976
- plateGallery: {
12977
- listPlates: (input) => dispatchSystem("plateGallery", "listPlates", "query", input),
12978
- getPlateByTrack: (input) => dispatchSystem("plateGallery", "getPlateByTrack", "query", input)
12979
- },
12980
- recording: {
12981
- getAvailability: (input) => dispatchSystem("recording", "getAvailability", "query", input),
12982
- getDaysWithRecordings: (input) => dispatchSystem("recording", "getDaysWithRecordings", "query", input),
12983
- getPlaybackManifest: (input) => dispatchSystem("recording", "getPlaybackManifest", "query", input),
12984
- getDeviceConfig: (input) => dispatchSystem("recording", "getDeviceConfig", "query", input),
12985
- locateSegment: (input) => dispatchSystem("recording", "locateSegment", "query", input),
12986
- readSegmentBytes: (input) => dispatchSystem("recording", "readSegmentBytes", "query", input),
12987
- setDeviceConfig: (input) => dispatchSystem("recording", "setDeviceConfig", "mutation", input),
12988
- rescanStorage: (input) => dispatchSystem("recording", "rescanStorage", "mutation", input),
12989
- pruneFootage: (input) => dispatchSystem("recording", "pruneFootage", "mutation", input),
12990
- getStatus: (input) => dispatchSystem("recording", "getStatus", "query", input),
12991
- getDeviceSettingsContribution: (input) => dispatchSystem("recording", "getDeviceSettingsContribution", "query", input),
12992
- getDeviceLiveContribution: (input) => dispatchSystem("recording", "getDeviceLiveContribution", "query", input),
12993
- applyDeviceSettingsPatch: (input) => dispatchSystem("recording", "applyDeviceSettingsPatch", "mutation", input)
12994
- },
12995
- snapshotProvider: {
12996
- supportsDevice: (input) => dispatchSystem("snapshotProvider", "supportsDevice", "query", input),
12997
- getSnapshot: (input) => dispatchSystem("snapshotProvider", "getSnapshot", "query", input)
12998
- },
12999
- streamBroker: {
13000
- publishCameraStream: (input) => dispatchSystem("streamBroker", "publishCameraStream", "mutation", input),
13001
- retractCameraStream: (input) => dispatchSystem("streamBroker", "retractCameraStream", "mutation", input),
13002
- assignProfile: (input) => dispatchSystem("streamBroker", "assignProfile", "mutation", input),
13003
- unassignProfile: (input) => dispatchSystem("streamBroker", "unassignProfile", "mutation", input),
13004
- restartProfile: (input) => dispatchSystem("streamBroker", "restartProfile", "mutation", input),
13005
- getDeviceSettingsContribution: (input) => dispatchSystem("streamBroker", "getDeviceSettingsContribution", "query", input),
13006
- getDeviceLiveContribution: (input) => dispatchSystem("streamBroker", "getDeviceLiveContribution", "query", input),
13007
- applyDeviceSettingsPatch: (input) => dispatchSystem("streamBroker", "applyDeviceSettingsPatch", "mutation", input)
13008
- }
13009
- };
13010
- }
13011
- //#endregion
13012
9682
  //#region src/utils/zone-rule-eval.ts
13013
9683
  /**
13014
9684
  * Evaluate `rules` against `items`. Returns the items partitioned
@@ -13332,7 +10002,8 @@ function createSystemProxy(api) {
13332
10002
  listAddonInstances: (input) => dispatch("metricsProvider", "listAddonInstances", "query", input),
13333
10003
  getAddonStats: (input) => dispatch("metricsProvider", "getAddonStats", "query", input),
13334
10004
  listNodeProcesses: (input) => dispatch("metricsProvider", "listNodeProcesses", "query", input),
13335
- killProcess: (input) => dispatch("metricsProvider", "killProcess", "mutation", input)
10005
+ killProcess: (input) => dispatch("metricsProvider", "killProcess", "mutation", input),
10006
+ dumpHeapSnapshot: (input) => dispatch("metricsProvider", "dumpHeapSnapshot", "mutation", input)
13336
10007
  },
13337
10008
  mqttBroker: {
13338
10009
  listBrokers: (input) => dispatch("mqttBroker", "listBrokers", "query", input),
@@ -15096,20 +11767,6 @@ var logDestinationCapability = {
15096
11767
  }
15097
11768
  };
15098
11769
  //#endregion
15099
- //#region src/capabilities/admin-ui.cap.ts
15100
- var StaticDirOutputSchema = z.object({ staticDir: z.string() });
15101
- var VersionOutputSchema = z.object({ version: z.string() });
15102
- var adminUiCapability = {
15103
- name: "admin-ui",
15104
- scope: "system",
15105
- mode: "singleton",
15106
- internal: true,
15107
- methods: {
15108
- getStaticDir: method(z.void(), StaticDirOutputSchema),
15109
- getVersion: method(z.void(), VersionOutputSchema)
15110
- }
15111
- };
15112
- //#endregion
15113
11770
  //#region src/schemas/auth-records.ts
15114
11771
  /**
15115
11772
  * Zod schemas for persisted record types.
@@ -19468,7 +16125,20 @@ var DiskSpaceInfoSchema = z.object({
19468
16125
  var PidResourceStatsSchema = z.object({
19469
16126
  pid: z.number(),
19470
16127
  cpu: z.number(),
19471
- memory: z.number()
16128
+ memory: z.number(),
16129
+ /**
16130
+ * Private (anonymous) resident bytes — the per-process V8 heap + native
16131
+ * allocations NOT shared with other processes (Linux RssAnon). This is the
16132
+ * "real" per-runner cost; summing it across runners is meaningful, unlike
16133
+ * `memory` (RSS), which double-counts the shared mmap'd framework code.
16134
+ * Undefined where /proc is unavailable (e.g. macOS).
16135
+ */
16136
+ privateBytes: z.number().optional(),
16137
+ /**
16138
+ * Shared file-backed resident bytes (Linux RssFile) — mmap'd framework/lib
16139
+ * code shared copy-on-write across runners. Undefined on macOS.
16140
+ */
16141
+ sharedBytes: z.number().optional()
19472
16142
  });
19473
16143
  var AddonInstanceSchema = z.object({
19474
16144
  addonId: z.string(),
@@ -19517,6 +16187,17 @@ var KillProcessResultSchema = z.object({
19517
16187
  reason: z.string().optional(),
19518
16188
  signal: z.enum(["SIGTERM", "SIGKILL"]).optional()
19519
16189
  });
16190
+ var DumpHeapSnapshotInputSchema = z.object({
16191
+ /** The addon whose runner should dump a heap snapshot. */
16192
+ addonId: z.string() });
16193
+ var DumpHeapSnapshotResultSchema = z.object({
16194
+ success: z.boolean(),
16195
+ /** Path of the written .heapsnapshot inside the runner's container/host. */
16196
+ path: z.string().optional(),
16197
+ /** Process pid that was signalled. */
16198
+ pid: z.number().optional(),
16199
+ reason: z.string().optional()
16200
+ });
19520
16201
  var SystemMetricsSchema = z.object({
19521
16202
  cpuPercent: z.number(),
19522
16203
  memoryPercent: z.number(),
@@ -19577,6 +16258,17 @@ var metricsProviderCapability = {
19577
16258
  killProcess: method(KillProcessInputSchema, KillProcessResultSchema, {
19578
16259
  kind: "mutation",
19579
16260
  auth: "admin"
16261
+ }),
16262
+ /**
16263
+ * Tell the addon's forked runner to write a V8 heap snapshot to disk (via
16264
+ * SIGUSR2 — the runner's diagnostic handler). Also logs its
16265
+ * `process.memoryUsage()` + heap-space breakdown. Refuses pids not in the
16266
+ * live `listNodeProcesses()` snapshot. Use for deep per-addon memory
16267
+ * attribution; copy the returned path off the node to analyze.
16268
+ */
16269
+ dumpHeapSnapshot: method(DumpHeapSnapshotInputSchema, DumpHeapSnapshotResultSchema, {
16270
+ kind: "mutation",
16271
+ auth: "admin"
19580
16272
  })
19581
16273
  }
19582
16274
  };
@@ -20090,124 +16782,6 @@ var recordingCapability = {
20090
16782
  }
20091
16783
  };
20092
16784
  //#endregion
20093
- //#region src/capabilities/device-ops.cap.ts
20094
- /**
20095
- * device-ops — device-scoped cap that unifies the per-IDevice operations
20096
- * previously routed through the `.device-ops` Moleculer bridge service.
20097
- *
20098
- * Each worker that hosts live `IDevice` instances auto-registers a native
20099
- * provider for this cap (per device) backed by its local
20100
- * `DeviceRegistry`. Hub-side callers reach it transparently through
20101
- * `ctx.fetchDevice(id).deviceOps.*` — the DeviceProxy injects
20102
- * `deviceId` + `nodeId` and dispatches through the standard cap-router,
20103
- * so there's no parallel bridge path anymore.
20104
- *
20105
- * The surface is intentionally small — every method corresponds to a
20106
- * single action on the live `IDevice` (or `ICameraDevice` for
20107
- * `getStreamSources`). Richer orchestration (enable/disable with
20108
- * integration plumbing, bulk updates) stays in the `device-manager` cap;
20109
- * `device-ops` is the per-device primitive the device-manager routes to.
20110
- */
20111
- var StreamSourceEntrySchema$1 = z.object({
20112
- id: z.string(),
20113
- label: z.string(),
20114
- protocol: z.enum([
20115
- "rtsp",
20116
- "rtmp",
20117
- "annexb",
20118
- "http-mjpeg",
20119
- "webrtc",
20120
- "custom"
20121
- ]),
20122
- url: z.string().optional(),
20123
- resolution: z.object({
20124
- width: z.number(),
20125
- height: z.number()
20126
- }).optional(),
20127
- fps: z.number().optional(),
20128
- bitrate: z.number().optional(),
20129
- codec: z.string().optional(),
20130
- profileHint: CamProfileSchema.optional(),
20131
- sdp: z.string().optional()
20132
- });
20133
- var ConfigEntrySchema$1 = z.object({
20134
- key: z.string(),
20135
- value: z.unknown()
20136
- });
20137
- var RawStateResultSchema = z.object({
20138
- /** Originating provider id, e.g. 'homeassistant' | 'reolink' | 'hikvision'. */
20139
- source: z.string(),
20140
- /** Opaque, DISPLAY-SAFE upstream blob (no secrets/PII). */
20141
- data: z.record(z.string(), z.unknown())
20142
- });
20143
- var deviceOpsCapability = {
20144
- name: "device-ops",
20145
- scope: "device",
20146
- deviceNative: true,
20147
- mode: "singleton",
20148
- methods: {
20149
- /**
20150
- * Return stream sources for camera-like devices. Non-camera devices
20151
- * return an empty array (the bridge did the same; preserved for compat).
20152
- */
20153
- getStreamSources: method(z.object({ deviceId: z.number() }), z.array(StreamSourceEntrySchema$1)),
20154
- /**
20155
- * Return the device's config entries (key + current value). Used by
20156
- * the device-manager aggregator when reading the driver's schema+values.
20157
- */
20158
- getConfigEntries: method(z.object({ deviceId: z.number() }), z.array(ConfigEntrySchema$1)),
20159
- /**
20160
- * Bulk-apply a config patch via `IDevice.config.setAll`. Covers the
20161
- * updateConfig / setStreamProfileMap / enable-as-config paths from
20162
- * the old bridge.
20163
- */
20164
- setConfig: method(z.object({
20165
- deviceId: z.number(),
20166
- values: z.record(z.string(), z.unknown())
20167
- }), z.void(), { kind: "mutation" }),
20168
- /**
20169
- * Invoke a device custom action on a forked/remote device (the
20170
- * cross-process transport for `IDevice.runDeviceAction`). Mirrors
20171
- * `setConfig` — the device-manager calls this when the device is not
20172
- * hub-local.
20173
- */
20174
- runAction: method(z.object({
20175
- deviceId: z.number(),
20176
- action: z.string().min(1),
20177
- input: z.unknown()
20178
- }), z.unknown(), { kind: "mutation" }),
20179
- /**
20180
- * Invoke `IDevice.removeDevice()` so the driver can release resources
20181
- * (close sockets, stop background tasks, …). The device-manager still
20182
- * performs its own persistence cleanup before/after this call.
20183
- */
20184
- removeDevice: method(z.object({ deviceId: z.number() }), z.void(), { kind: "mutation" }),
20185
- /**
20186
- * Build the ConfigUISchema (FormBuilder input shape) from the device's
20187
- * Zod config schema. Runs on the worker that owns the IDevice so the
20188
- * Zod types stay local (they're function references, not
20189
- * serializable). Returns a fully JSON-serializable schema with
20190
- * sections/fields the admin UI renders directly.
20191
- *
20192
- * Needed because the hub-side `device-manager.getSettingsSchema`
20193
- * couldn't reach forked-worker devices — it had no registry entry
20194
- * and no cross-process lookup, so the UI silently rendered an empty
20195
- * settings panel for every worker-owned device.
20196
- *
20197
- * Returns `null` when the device isn't found on this worker.
20198
- */
20199
- getSettingsSchema: method(z.object({ deviceId: z.number() }), z.unknown().nullable()),
20200
- /**
20201
- * Opt-in: return the device's RAW upstream state (the provider's
20202
- * cached values) as a display-safe `{ source, data }` blob. Returns
20203
- * `null` when the device exposes no raw state — the State panel hides
20204
- * its Raw toggle in that case. One-shot (read the provider's existing
20205
- * cache; no upstream round-trip).
20206
- */
20207
- getRawState: method(z.object({ deviceId: z.number() }), RawStateResultSchema.nullable(), { auth: "protected" })
20208
- }
20209
- };
20210
- //#endregion
20211
16785
  //#region src/capabilities/camera-credentials.cap.ts
20212
16786
  /**
20213
16787
  * camera-credentials — device-scoped cap exposing the camera's network
@@ -25791,6 +22365,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
25791
22365
  addonId: null,
25792
22366
  access: "view"
25793
22367
  },
22368
+ "metricsProvider.dumpHeapSnapshot": {
22369
+ capName: "metrics-provider",
22370
+ capScope: "system",
22371
+ addonId: null,
22372
+ access: "create"
22373
+ },
25794
22374
  "metricsProvider.getAddonStats": {
25795
22375
  capName: "metrics-provider",
25796
22376
  capScope: "system",
@@ -28619,59 +25199,6 @@ function cellsToRects(cells, gridWidth, gridHeight, maxRegions) {
28619
25199
  }));
28620
25200
  }
28621
25201
  //#endregion
28622
- //#region src/utils/sleep.ts
28623
- /**
28624
- * Promise-based timer helpers — used everywhere the codebase needs to
28625
- * wait, back off, or schedule a retry. Before these helpers landed, each
28626
- * call site re-implemented `new Promise(r => setTimeout(r, ms))` inline,
28627
- * with subtle variations (some swallowing cancellation, some not). Two
28628
- * shapes cover every observed use case:
28629
- *
28630
- * - {@link sleep} for a plain, uncancellable wait — the default choice.
28631
- * - {@link sleepCancellable} for a wait that wakes early when an
28632
- * abort signal trips, used by long-running pollers whose teardown
28633
- * must stop a pending backoff promptly.
28634
- */
28635
- /**
28636
- * Resolve after `ms` milliseconds. Never rejects, never cancels. The
28637
- * sleep cannot be interrupted; for a wakeable variant use
28638
- * {@link sleepCancellable}.
28639
- *
28640
- * `ms <= 0` resolves on the next microtask via `setTimeout(0)`, which
28641
- * still gives the event loop a chance to drain — useful for breaking
28642
- * up tight async loops without changing call-site semantics.
28643
- */
28644
- function sleep(ms) {
28645
- return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
28646
- }
28647
- /**
28648
- * Resolve after `ms` milliseconds OR when `signal.aborted` flips,
28649
- * whichever fires first. The returned promise never rejects — aborted
28650
- * sleeps resolve normally so callers can model "wait or wake" without
28651
- * try/catch noise.
28652
- *
28653
- * await sleepCancellable(backoffMs, lifecycle.abortSignal)
28654
- * if (lifecycle.aborted) return
28655
- *
28656
- * If `signal` is already aborted at call time the helper resolves on
28657
- * the next microtask.
28658
- */
28659
- function sleepCancellable(ms, signal) {
28660
- if (signal.aborted) return Promise.resolve();
28661
- return new Promise((resolve) => {
28662
- const timer = setTimeout(() => {
28663
- signal.removeEventListener("abort", onAbort);
28664
- resolve();
28665
- }, Math.max(0, ms));
28666
- const onAbort = () => {
28667
- clearTimeout(timer);
28668
- signal.removeEventListener("abort", onAbort);
28669
- resolve();
28670
- };
28671
- signal.addEventListener("abort", onAbort, { once: true });
28672
- });
28673
- }
28674
- //#endregion
28675
25202
  //#region src/helpers/bind-addon-actions.ts
28676
25203
  /**
28677
25204
  * Bind an addon's custom-action catalog to its tRPC surface, returning a