@camstack/addon-import-alexa 0.2.195 → 0.2.197

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.
package/dist/addon.js CHANGED
@@ -48,1194 +48,6 @@ node_fs = __toESM(node_fs, 1);
48
48
  let node_child_process = require("node:child_process");
49
49
  require("node:zlib");
50
50
  let node_url = require("node:url");
51
- //#region ../types/dist/event-category-78tcBemo.mjs
52
- var EventCategory = /* @__PURE__ */ function(EventCategory) {
53
- EventCategory["SystemBoot"] = "system.boot";
54
- EventCategory["SystemAddonsReady"] = "system.addons-ready";
55
- EventCategory["SystemRestarting"] = "system.restarting";
56
- /**
57
- * Fired exactly once after the hub finishes booting following a
58
- * restart that was triggered by `RestartCoordinator` (today: framework
59
- * live-update). Payload is the marker that was on disk at boot
60
- * (`PendingRestartMarkerPayload`); admin UI listens for it to display a
61
- * success toast describing what changed.
62
- *
63
- * Spec: docs/superpowers/specs/2026-05-14-framework-live-update-design.md
64
- */
65
- EventCategory["SystemRestartCompleted"] = "system.restart-completed";
66
- /**
67
- * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
68
- * Emitted only when the material on disk actually changed, so an
69
- * operator who trusted the old certificate by hand is told rather than
70
- * discovering it as a browser error. Payload `TlsCertChangedPayload`.
71
- *
72
- * Rule: docs/decisions/adr-0227-*.md
73
- */
74
- EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
75
- /**
76
- * A newer addon or server-root package version was found by the
77
- * authoritative registry check. Emitted once when any observed
78
- * `latestVersion` changes (or a package/node first appears behind);
79
- * the payload carries the full currently-available list. Repeated
80
- * polling of the same latests is silent.
81
- */
82
- EventCategory["UpdateAvailable"] = "update.available";
83
- /**
84
- * Readiness transition for a capability provider. Every producer emits
85
- * this event on `onInitialize` completion, `onDestroy`, and
86
- * `$node.reconnect`; every consumer that needs to gate on a cross-process
87
- * cap subscribes via the kernel's readiness module (`awaitReady` /
88
- * `onReadyState`) instead of polling. Payload is
89
- * `SystemReadyStatePayload` — see event-bus.ts.
90
- */
91
- EventCategory["SystemReadyState"] = "system.ready-state";
92
- EventCategory["AddonStarted"] = "addon.started";
93
- EventCategory["AddonStopped"] = "addon.stopped";
94
- EventCategory["AddonRestarted"] = "addon.restarted";
95
- EventCategory["AddonUpdated"] = "addon.updated";
96
- EventCategory["AddonInstalled"] = "addon.installed";
97
- EventCategory["AddonUninstalled"] = "addon.uninstalled";
98
- EventCategory["AddonCrashed"] = "addon.crashed";
99
- /**
100
- * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
101
- *
102
- * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
103
- * respawned"); this is the one that never resolves itself. Emitted exactly
104
- * once per trip, by `process-service.ts`, carrying the node, the runner, the
105
- * addons it hosted and the crash count that tripped the breaker.
106
- *
107
- * It exists because a runner marked terminally `failed` used to be SILENT:
108
- * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
109
- * errors, the breaker stopped respawning it (correctly), `/health` kept
110
- * returning 200, and fleet-wide recording was gone for 3h15 before the
111
- * operator's phone told him. The Notification Center's system-event intake
112
- * consumes this category and maps it to the `addon-crash-loop` kind.
113
- */
114
- EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
115
- /**
116
- * A runner whose addon refused to start with a typed reason (D877) - parked
117
- * in `refused`, never counted by the crash breaker, retried on operator
118
- * action. One event per refusal.
119
- */
120
- EventCategory["AddonRunnerRefused"] = "addon.runner-refused";
121
- /**
122
- * A runner settled on a state (D877): `running` (READY), `crashed`, `failed` or
123
- * `refused`. In-process telemetry for the boot check (re-evaluate while any
124
- * runner is still pending) and the alert centre (close a recovered runner).
125
- */
126
- EventCategory["ProcessStateChanged"] = "process.state-changed";
127
- /**
128
- * The node's one-shot runner boot check (D877): planned runners against the
129
- * process service, `degraded` when any is failed, refused or missing. The
130
- * alert centre turns it into one critical alert.
131
- */
132
- EventCategory["SystemRunnerCheck"] = "system.runner-check";
133
- /**
134
- * An alert EPISODE opened or closed, emitted by the alert centre for alerts
135
- * of severity `critical` or `error` (`AlertEpisodePayload`). The alert
136
- * centre sends no notification of its own: Notification Center turns this
137
- * into the `alert-critical` / `alert-error` / `alert-critical-resolved`
138
- * system-event kinds, and reconciles from the alerts store because a bus
139
- * event may be dropped (D8, D11).
140
- */
141
- EventCategory["AlertEpisode"] = "alert.episode";
142
- EventCategory["AddonError"] = "addon.error";
143
- EventCategory["AddonPageReady"] = "addon.page-ready";
144
- EventCategory["AddonWidgetReady"] = "addon.widget-ready";
145
- /**
146
- * Addon failed to load (import or initialize). Emitted by the kernel's
147
- * AddonHealthMonitor only AFTER the boot grace period ends — failures
148
- * during the first 5 minutes are silently retried without alerting
149
- * (slow-starting addons must have time to come up). Post-grace, this
150
- * event is emitted exactly once per failure-streak; AlertCenter
151
- * consumes it to create a persistent operator-visible alert.
152
- *
153
- * Payload: `{ packageName, addonId?, error: { message, stack }, retryCount, nextRetryAt }`.
154
- */
155
- EventCategory["AddonLoadFailed"] = "addon.load-failed";
156
- /**
157
- * A keychain value was stored or removed (D787). Carries the ref, its
158
- * owner and the new version — NEVER the value. A process holding the ref
159
- * drops its cached copy and the owning `BaseAddon` re-resolves its config.
160
- * Telemetry (D8): a process that misses it still revalidates by version.
161
- */
162
- EventCategory["KeychainChanged"] = "keychain.changed";
163
- /**
164
- * Addon recovered from a previous failure. Emitted when an addon
165
- * transitions from `failed` back to `healthy` (typically via the
166
- * monitor's auto-retry loop, or after manual `addons.retryLoad`).
167
- * AlertCenter dismisses the corresponding `AddonLoadFailed` alert
168
- * on this event.
169
- */
170
- EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
171
- /**
172
- * An addon is RUNNING although its installed package is below what the
173
- * running server requires (`camstack.requires.addons` of the server, or the
174
- * addon's own `requires.server`). Raised once per streak by the unsupported-
175
- * addon tracker, on the hub and on agents; AlertCenter turns it into one
176
- * operator alert per node and package. The addon is never held back for it.
177
- *
178
- * Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
179
- */
180
- EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
181
- /**
182
- * The package of an {@link AddonUnsupportedVersion} alert reached a supported
183
- * version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
184
- *
185
- * Payload: `{ packageName, addonId, nodeId }`.
186
- */
187
- EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
188
- /**
189
- * Monitor scheduled the next retry for a failed addon. Transient —
190
- * surfaced to the UI for live-updating the "next retry in Ns"
191
- * countdown on the Addons page row, NOT persisted as an alert.
192
- */
193
- EventCategory["AddonRetryScheduled"] = "addon.retry-scheduled";
194
- /**
195
- * A liveness invariant of this node is false — it has no devices, every
196
- * camera is offline, or a camera that was recording has stopped producing
197
- * segments. Emitted by the `liveness-monitor` builtin, once per fault (the
198
- * `findingId` is stable across ticks), only after its boot grace period.
199
- * AlertCenter raises a persistent operator-visible alert.
200
- *
201
- * It exists because on 2026-08-03 the hub ran four hours with zero devices
202
- * and zero recordings while every surface stayed quiet.
203
- *
204
- * Payload: `{ findingId, severity, title, message, deviceId? }`.
205
- */
206
- EventCategory["SystemLivenessFailed"] = "system.liveness-failed";
207
- /**
208
- * A previously reported liveness fault is true again. AlertCenter dismisses
209
- * the matching `SystemLivenessFailed` alert.
210
- *
211
- * Payload: `{ findingId }`.
212
- */
213
- EventCategory["SystemLivenessRecovered"] = "system.liveness-recovered";
214
- /**
215
- * Monitor is attempting to reload a failed addon NOW. UI uses this
216
- * to show a spinner during the retry attempt. Same transient nature
217
- * as AddonRetryScheduled.
218
- */
219
- EventCategory["AddonRetryAttempting"] = "addon.retry-attempting";
220
- EventCategory["DeviceRegistered"] = "device.registered";
221
- EventCategory["DeviceUnregistered"] = "device.unregistered";
222
- EventCategory["DeviceEnabled"] = "device.enabled";
223
- EventCategory["DeviceDisabled"] = "device.disabled";
224
- EventCategory["DeviceSettingsUpdated"] = "device.settings-updated";
225
- /**
226
- * Emitted when the set of native capability providers bound to a device
227
- * changes — e.g. an addon registers a new native cap via
228
- * `DeviceContext.registerNativeCap`, or all native bindings for a device
229
- * are cleared on removal. Hub consumers re-resolve device-proxy routes
230
- * when this fires.
231
- */
232
- EventCategory["DeviceBindingsChanged"] = "device.bindings-changed";
233
- /**
234
- * Emitted when the operator-organisational meta surface changes
235
- * (`name` / `location` / `disabled`). Payload: `{deviceId, field,
236
- * value}`. Live consumers (UI device list, alert center) react
237
- * without polling. Distinct from `DeviceSettingsUpdated` which
238
- * fires on hardware-config changes (host/port/credentials/etc).
239
- */
240
- EventCategory["DeviceMetaChanged"] = "device.meta-changed";
241
- /**
242
- * Emitted by `BaseDevice.updateSourceInfo()` after a successful patch
243
- * to the device's upstream-system identity / rendering envelope. The
244
- * full new SourceInfo travels in the payload so cross-process listeners
245
- * (UI, export adapters) don't need to re-read the meta blob.
246
- *
247
- * Payload: `{deviceId, sourceInfo}`.
248
- *
249
- * Distinct from `DeviceMetaChanged` which covers the operator-edited
250
- * surface (`name` / `location` / `disabled`). The two share the
251
- * persistence layer (both ride on `device-manager.setMetadata` for
252
- * SourceInfo, or `setName`/`setLocation`/`setDisabled` for the meta
253
- * fields) but consumers care about different subsets.
254
- */
255
- EventCategory["DeviceSourceInfoChanged"] = "device.source-info-changed";
256
- /**
257
- * Emitted by DeviceStreamWiringService after a successful
258
- * `stream-broker.registerDeviceStreams` call. Payload includes
259
- * deviceId — consumers look up the full registered device info via
260
- * `brokerManager.getRegisteredDevice(deviceId)`.
261
- *
262
- * Replaces the legacy `StreamRouterService.onDeviceRegistered` callback.
263
- */
264
- EventCategory["DeviceStreamsRegistered"] = "device.streams-registered";
265
- /**
266
- * Device-level "fully provisioned" signal — the EXPORT trigger. Emitted once
267
- * a device's persisted export-relevant shape (its `DeviceFeature` set) is
268
- * established or changes, carrying `{deviceId, fingerprint, generation}`.
269
- * Export adapters (Alexa / HAP) react to THIS instead of the chatty per-cap
270
- * `DeviceBindingsChanged`, turning a boot's incomplete→complete trickle into
271
- * one clean delta. `fingerprint` is `canonicalDeviceFingerprint(shape)`.
272
- *
273
- * Spec: docs/superpowers/specs/2026-06-01-alexa-hap-export-reconciler-design.md
274
- */
275
- EventCategory["DeviceProvisioned"] = "device.provisioned";
276
- /**
277
- * Fires once when a device's initial feature-probe completes successfully
278
- * (lastProbedAt 0→>0); exported shape is now stable. Telemetry (may be
279
- * dropped) — consumers MUST also gate on the device record's `probed` flag.
280
- */
281
- EventCategory["DeviceReady"] = "device.ready";
282
- EventCategory["IntegrationEnabled"] = "integration.enabled";
283
- EventCategory["IntegrationDisabled"] = "integration.disabled";
284
- EventCategory["IntegrationDeleted"] = "integration.deleted";
285
- /** Emitted when a broker connection's status flips (connected /
286
- * disconnected / error). Carries `{ brokerId, status, error? }`. */
287
- EventCategory["BrokerStatusChanged"] = "broker.status-changed";
288
- /** Emitted for every subscription-matched message routed by a
289
- * broker provider. Carries `{ brokerId, subscriptionId, key, payload }`.
290
- * Consumers filter by `brokerId` + `subscriptionId` in the handler. */
291
- EventCategory["BrokerMessage"] = "broker.message";
292
- EventCategory["ProviderStarted"] = "provider.started";
293
- EventCategory["ProviderStopped"] = "provider.stopped";
294
- EventCategory["ProcessCrashed"] = "process.crashed";
295
- EventCategory["ProcessRestartScheduled"] = "process.restart_scheduled";
296
- EventCategory["ProcessRestarted"] = "process.restarted";
297
- /**
298
- * The SET of storage locations changed — one was created, edited, enabled,
299
- * disabled or deleted through the location instances or the forced-delete action.
300
- *
301
- * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
302
- * it must also converge on its own periodic path, because a dropped event
303
- * must not leave a node writing to yesterday's disk set forever. It exists
304
- * because there was NO signal at all — an operator who added a second
305
- * recordings disk in the admin UI got nothing, and the recorder kept its
306
- * resolved locations until something else happened to re-resolve them
307
- * (D387). Payload `StorageLocationsChangedPayload`.
308
- */
309
- EventCategory["StorageLocationsChanged"] = "storage.locations-changed";
310
- EventCategory["RecordingStarted"] = "recording.started";
311
- EventCategory["RecordingStopped"] = "recording.stopped";
312
- EventCategory["RecordingError"] = "recording.error";
313
- EventCategory["RecordingHealthDegraded"] = "recording.health.degraded";
314
- /** Recording enabled but no segment finalized for the NC stall window. */
315
- EventCategory["RecordingHealthStalled"] = "recording.health.stalled";
316
- /** Recovery from {@link RecordingHealthStalled}. */
317
- EventCategory["RecordingHealthOk"] = "recording.health.ok";
318
- EventCategory["RecordingStorageCritical"] = "recording.storage.critical";
319
- /** Free space on a storage location crossed the warn bar. */
320
- EventCategory["StorageLocationLow"] = "storage.location.low";
321
- /** Free space at/under the critical bar, or capacity probe failed (`null`). */
322
- EventCategory["StorageLocationCritical"] = "storage.location.critical";
323
- /** Recovery from low/critical on a storage location. */
324
- EventCategory["StorageLocationOk"] = "storage.location.ok";
325
- EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
326
- EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
327
- EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
328
- /** Export render progress (0–100). Telemetry (D8): a lost tick is a stale
329
- * progress bar the client reconciles via `recordingExport.getExport`. */
330
- EventCategory["RecordingExportProgress"] = "recording.export.progress";
331
- EventCategory["RecordingExportCompleted"] = "recording.export.completed";
332
- EventCategory["RecordingExportFailed"] = "recording.export.failed";
333
- EventCategory["DetectionEvent"] = "detection.event";
334
- EventCategory["SessionTrackNew"] = "session.track.new";
335
- EventCategory["SessionTrackExpired"] = "session.track.expired";
336
- EventCategory["BenchmarkProgress"] = "benchmark.progress";
337
- EventCategory["PlatformProbePhase"] = "platform-probe.phase";
338
- EventCategory["PipelineProgress"] = "pipeline.progress";
339
- /** Per-frame execution trace emitted by the pipeline executor for live observability. */
340
- EventCategory["PipelineTrace"] = "pipeline.trace";
341
- /**
342
- * Raw inference output emitted by `addon-pipeline-runner` after running the
343
- * detection pipeline on a frame. Carries the `FrameResult` (with
344
- * `detections[]`, `width`/`height`, timing debug) — never the frame
345
- * buffer itself. Hub-side consumers (analysis pipeline, class filters,
346
- * notifications) subscribe to this and re-emit `detection.result` after
347
- * post-processing.
348
- */
349
- EventCategory["PipelineInferenceResult"] = "pipeline.inference-result";
350
- /**
351
- * Camera lifecycle event emitted by `addon-pipeline-orchestrator` when it
352
- * assigns or unassigns a camera to/from an agent. Carries no frame data;
353
- * pure observability for UI dashboards and metrics consumers.
354
- */
355
- EventCategory["PipelineCameraAssigned"] = "pipeline.camera-assigned";
356
- EventCategory["PipelineCameraUnassigned"] = "pipeline.camera-unassigned";
357
- /**
358
- * A node the orchestrator would otherwise place cameras on has NO usable
359
- * inference device: the operator enabled one or more accelerators there and
360
- * the live probe reports every one of them unavailable. Emitted once per
361
- * TRANSITION into that state (never per dispatch), and the node is dropped
362
- * from the placement candidate set for as long as it holds.
363
- *
364
- * This exists because the state was previously invisible: agent-a
365
- * absorbed 283k inference errors in a day while still being handed cameras,
366
- * and nothing in the system said so.
367
- *
368
- * A node with no accelerators configured at all is NOT this — its devices
369
- * are `disabled`, not `unavailable`, and the runner's default CPU pool
370
- * serves it exactly as before.
371
- */
372
- EventCategory["PipelineNodeInferenceUnavailable"] = "pipeline.node-inference-unavailable";
373
- /**
374
- * A camera has an OPEN detection session and has produced no detection at
375
- * all for longer than the blind threshold — the camera is being decoded and
376
- * inferred and is returning nothing. Emitted once per transition into blind,
377
- * per camera.
378
- *
379
- * The failure it reports: a 1h43 detection blackout on the entrance camera
380
- * that nobody noticed, because "a camera that detects nothing" and "a quiet
381
- * camera" produce byte-identical silence.
382
- */
383
- EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
384
- /**
385
- * A camera's motion source has fired constantly with no subject for an hour
386
- * (motion duty or session count over the bar, zero detections) — it is not
387
- * blind, its motion zones or sensitivity want tuning (D907). Paired with
388
- * {@link PipelineMotionCalm}.
389
- */
390
- EventCategory["PipelineMotionNoisy"] = "pipeline.motion-noisy";
391
- /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
392
- EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
393
- /**
394
- * A camera's package LLM confirm has failed closed three times in a row
395
- * (D931, 2026-10-26). One edge per camera until
396
- * {@link PipelinePackageLlmAnswered}.
397
- */
398
- EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
399
- /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
400
- EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
401
- /**
402
- * Per-camera pipeline config was mutated by the orchestrator
403
- * (a settings change via `setAgentInferenceDevices` /
404
- * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
405
- * subscribes to its own emission to hot-reload the assigned runner
406
- * via `attachCamera` so the next frame executes against the new
407
- * engine/steps without waiting for a rebalance or the next
408
- * `DeviceStreamsRegistered` cycle.
409
- */
410
- EventCategory["PipelineCameraUpdated"] = "pipeline.camera-updated";
411
- /**
412
- * The cluster camera-source OWNER changed (`clusterRoles.ingestNode`).
413
- * Emitted by addon-pipeline-orchestrator whenever it (re)derives node
414
- * capabilities — at boot, on agent online/offline, and on an ingest-node
415
- * flip. Carries the resolved `ownerNodeId`. The stream-broker consumes it to
416
- * keep its ingest-owner-gate decision current WITHOUT a per-`ensureBroker`
417
- * cross-process `getIngestOwner` query (push the authority's decision instead
418
- * of polling it on the hot path). Idempotent state — re-emitted on every
419
- * topology change, so a dropped event self-heals on the next one (plus the
420
- * broker's long backstop reconcile query).
421
- */
422
- EventCategory["PipelineIngestOwnerChanged"] = "pipeline.ingest-owner-changed";
423
- /**
424
- * Periodic snapshot of per-node pipeline-runner load
425
- * (`RunnerLocalLoad`). Emitted ~1Hz by every runner so UI dashboards
426
- * subscribe instead of polling `pipelineRunner.getLocalLoad`.
427
- * `nodeId` carried in the payload + on `event.source.nodeId`.
428
- */
429
- EventCategory["PipelineRunnerLoadSnapshot"] = "pipeline.runner-load-snapshot";
430
- /**
431
- * Periodic snapshot of per-camera pipeline metrics (`CameraMetrics`
432
- * + `deviceId` + `nodeId`). Emitted ~1Hz by every runner for each
433
- * attached camera. UI subscribes to drive overlay phase / fps /
434
- * inference time without polling `getCameraMetrics`.
435
- */
436
- EventCategory["PipelineCameraMetricsSnapshot"] = "pipeline.camera-metrics-snapshot";
437
- /**
438
- * Periodic snapshot of stream-broker per-broker statistics (input
439
- * fps, decoded fps, bitrate, codec). Emitted ~1Hz by every
440
- * stream-broker process for each active broker so the UI can drive
441
- * the Stream / Cluster dashboards without polling
442
- * `streamBroker.listAllProfileSlots` and friends.
443
- */
444
- EventCategory["StreamBrokerMetricsSnapshot"] = "stream-broker.metrics-snapshot";
445
- /**
446
- * Cap event fired by `stream-broker` when a profile slot enters
447
- * "demanded" state — a cam stream has been assigned and at least one
448
- * consumer (RTSP restream, decoded subscriber, WebRTC session, …) is
449
- * present. Camera-provider addons (Reolink Baichuan push, …)
450
- * subscribe to this category to start their underlying transport
451
- * lazily. Payload: `{ deviceId, camStreamId, profile }`.
452
- */
453
- EventCategory["StreamBrokerOnCamStreamDemand"] = "stream-broker.onCamStreamDemand";
454
- /**
455
- * Cap event fired by `stream-broker` when the last consumer leaves a
456
- * previously-demanded cam stream. Providers tear down their
457
- * underlying transport on receipt. Payload: `{ deviceId, camStreamId }`.
458
- */
459
- EventCategory["StreamBrokerOnCamStreamIdle"] = "stream-broker.onCamStreamIdle";
460
- /**
461
- * Cap event fired by `stream-broker` when a broker fails to dial a
462
- * managed-loopback source (today: `pull-rfc4571`) and the publisher
463
- * needs to refresh the cached URL. The lib's TCP server idle-tears-down
464
- * on its own schedule, so a re-publish with a fresh `host:port` is the
465
- * only way to keep the broker dialable. Camera providers (Reolink
466
- * Baichuan native, …) subscribe and respond by re-running their publish
467
- * pipeline.
468
- * Payload: `{ deviceId, camStreamId, brokerId }`.
469
- */
470
- EventCategory["StreamBrokerOnRequestStreamSourceRefresh"] = "stream-broker.onRequestStreamSourceRefresh";
471
- /**
472
- * A camera provider changed a device's stream parameters (codec /
473
- * resolution / bitrate / a stream added or removed). A LOW-LATENCY NUDGE
474
- * for the stream-broker's catalog reconcile: it carries NO authoritative
475
- * data — the broker re-PULLS that device's `stream-catalog` cap on receipt
476
- * (the 30s reconcile poll is the backstop if this nudge is dropped). Emitted
477
- * by providers from `stream-params.setProfile`. Payload: `{ deviceId }`.
478
- */
479
- EventCategory["StreamParamsChanged"] = "stream-params.changed";
480
- /**
481
- * Generic per-device runtime-state change. Fired by `device-manager`
482
- * whenever a persisted slice in any cap's `runtimeState` shape
483
- * mutates. Payload: `{deviceId, capName, slice}`. Subscribers are
484
- * the `deviceState` cap router (cross-process listeners) and the
485
- * deviceProxy reactive bindings (`device.state.<capName>.value`).
486
- * Cap-specific events (`battery.onStatusChanged`, …) still fire
487
- * — they're authoritative for callers that want a typed payload
488
- * without filtering on `capName`.
489
- */
490
- EventCategory["DeviceStateChanged"] = "device.state-changed";
491
- /**
492
- * The NATIVE shadow of a cap a composition has claimed fields of moved
493
- * (D663). Payload: `{ deviceId, capName, native }` — what the provider
494
- * wrote, BEFORE the owned fields are overlaid. Owner-only (D224): it is for
495
- * the composer's native self-reads, and the public event routers refuse it
496
- * (`isOwnerOnlyEventCategory`) so no consumer surface ever shows a second
497
- * truth beside the merged `DeviceStateChanged.slice`. Emitted only under a
498
- * `replace` claim, one per native write of that cap.
499
- */
500
- EventCategory["DeviceNativeShadowChanged"] = "device.native-shadow-changed";
501
- /**
502
- * Frame occupancy for a camera CHANGED — a tracked object was gained or
503
- * lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
504
- *
505
- * Emitted only on a change, so a steady scene is silent. It exists so a
506
- * client can stop polling `zoneAnalytics.getCurrentSnapshot`: that was the
507
- * one live badge with no push signal at all, and it cost a request every
508
- * four seconds per visible camera.
509
- *
510
- * Like every event it is telemetry and may be dropped ([D8]) — a consumer
511
- * keeps a slow reconcile rather than trusting it alone.
512
- */
513
- EventCategory["ZoneAnalyticsOccupancyChanged"] = "zone-analytics.occupancy-changed";
514
- /**
515
- * Cap event fired by every device that registers the `battery`
516
- * capability. Mirrors the cap definition's `onStatusChanged`. Carries
517
- * `{ deviceId, status: BatteryStatus }`. Subscribers (alert center,
518
- * snapshot wrapper, UI) react to charge/sleep transitions without
519
- * polling `batteryCapability.getStatus`.
520
- */
521
- EventCategory["BatteryOnStatusChanged"] = "battery.onStatusChanged";
522
- /**
523
- * Cap event fired by every device that registers the `network-link`
524
- * capability. Mirrors the cap definition's `onStatusChanged`. Carries
525
- * `{ deviceId, status: NetworkLinkStatus }` — a link switch or a signal
526
- * reading that moved.
527
- */
528
- EventCategory["NetworkLinkOnStatusChanged"] = "network-link.onStatusChanged";
529
- /**
530
- * Emitted by the battery cap provider WHEN `wakeForStream` enters the
531
- * "wake in progress" window — between the Baichuan wake-up issue and
532
- * the camera's first dialed-back RTP packet. The stream-broker
533
- * manager subscribes to flip the per-broker placeholder reason to
534
- * `'waking'` so viewers see a labelled "WAKING UP" tile instead of
535
- * the generic `'reconnecting'` frame. Carries `{ deviceId }`. The
536
- * complementary "wake complete" signal is the existing
537
- * `BatteryOnStatusChanged { sleeping: false }` event.
538
- */
539
- EventCategory["BatteryOnWakeStarted"] = "battery.onWakeStarted";
540
- /**
541
- * Cap event fired by every device that registers the `doorbell`
542
- * capability. Mirrors `doorbellCapability.events.onPressed`. Carries
543
- * `{ deviceId, timestamp }`. Operators consuming the UI subscribe
544
- * here to render transient ring toasts and a "Recent presses" row
545
- * on the device detail page.
546
- */
547
- EventCategory["DoorbellOnPressed"] = "doorbell.onPressed";
548
- /** Mirrors `runningStateCapability.events.onStarted`. */
549
- EventCategory["RunningStarted"] = "running.started";
550
- /** Mirrors `runningStateCapability.events.onFinished`. */
551
- EventCategory["RunningFinished"] = "running.finished";
552
- /** Mirrors `deviceRunsCapability.events.onRunOpened` - a durable run record opened. */
553
- EventCategory["DeviceRunOpened"] = "device-run.opened";
554
- /** Mirrors `deviceRunsCapability.events.onRunClosed` - a durable run record closed. */
555
- EventCategory["DeviceRunClosed"] = "device-run.closed";
556
- /**
557
- * Cap event fired by every device that registers the `event-emitter`
558
- * capability. Mirrors `eventEmitterCapability.events.onEvent`. Carries
559
- * `{ deviceId, eventType, data, timestamp, seq }` — the device's EXACT
560
- * declared event verbatim (NO normalization). Subscribers (UI event
561
- * stream, advanced-notifier rules) react to fired events without
562
- * holding a cap reference.
563
- */
564
- EventCategory["EventEmitted"] = "event-emitter.event";
565
- /**
566
- * Periodic snapshot of the per-node detection-pipeline engine
567
- * registry (loaded engines, models resident, in-use cameras, idle
568
- * TTL). Emitted ~0.2Hz (every 5 s) by every detection-pipeline
569
- * process. The Engines tab subscribes to drive its inventory view
570
- * without polling `pipelineExecutor.listLoadedEngines`.
571
- */
572
- EventCategory["PipelineEngineMetricsSnapshot"] = "pipeline.engine-metrics-snapshot";
573
- /**
574
- * Per-node detection-engine runtime-provisioning transition. Emitted by
575
- * the detection-pipeline provider on every state change of its lazy
576
- * engine-provisioning machine (idle → installing → verifying → ready,
577
- * or → failed with a `nextRetryAt`). Payload is the
578
- * `EngineProvisioningState` snapshot; `event.source.nodeId` carries the
579
- * node. The Pipeline page subscribes to drive a live "installing
580
- * OpenVINO… / ready" indicator per node without polling
581
- * `pipelineExecutor.getEngineProvisioning`. Telemetry-grade (D8): the UI
582
- * also reads the cap snapshot on mount / reconnect. Phase 2.
583
- */
584
- EventCategory["PipelineEngineProvisioning"] = "pipeline.engine-provisioning";
585
- /**
586
- * Cluster topology snapshot. Carries the same payload returned by
587
- * `nodes.topology` (every reachable node + addons + processes).
588
- * Emitted by the hub on any agent / addon lifecycle change
589
- * (debounced) plus a periodic safety net. Replaces UI polling on
590
- * `nodes.topology` — admin-ui dashboards subscribe to drive the
591
- * cluster view directly from the event payload.
592
- */
593
- EventCategory["ClusterTopologySnapshot"] = "cluster.topology-snapshot";
594
- /**
595
- * Periodic per-node system metrics snapshot (CPU / memory / GPU /
596
- * disk / network). Emitted ~0.2 Hz by the metrics-provider addon
597
- * for each node. Drives the dashboard SystemStatus / ProcessResources
598
- * widgets without polling `metricsProvider.getCurrent`.
599
- */
600
- EventCategory["MetricsNodeResourcesSnapshot"] = "metrics.node-resources-snapshot";
601
- /**
602
- * Periodic per-node process-tree snapshot (camstack-related pids
603
- * with root / managed / system classification). Emitted ~0.2 Hz by
604
- * the metrics-provider addon. Drives the Cluster → Processes tab
605
- * without polling `metricsProvider.listNodeProcesses`.
606
- */
607
- EventCategory["MetricsNodeProcessesSnapshot"] = "metrics.node-processes-snapshot";
608
- /**
609
- * Capability binding change event emitted by `addon-pipeline-orchestrator`
610
- * when a user changes which addon implements a cap on a node. Subscribed
611
- * by every kernel process to update its local `preferredProviderRegistry`
612
- * so future capability lookups respect the new binding.
613
- */
614
- /**
615
- * A capability binding was changed for a node — addon X now provides
616
- * capability `cap` on node `nodeId`. Lives under the generic
617
- * `capability.*` namespace because capability bindings are a kernel-
618
- * level concept used by many addons, not strictly pipeline-scoped.
619
- */
620
- EventCategory["CapabilityBindingChanged"] = "capability.binding-changed";
621
- EventCategory["ModelDownloadProgress"] = "model.download.progress";
622
- EventCategory["AgentRegistered"] = "agent.registered";
623
- EventCategory["AgentUnregistered"] = "agent.unregistered";
624
- EventCategory["AgentOnline"] = "agent.online";
625
- EventCategory["AgentOffline"] = "agent.offline";
626
- /** Forked worker process (e.g. hub/pipeline) connected to the broker. */
627
- EventCategory["WorkerOnline"] = "worker.online";
628
- /** Forked worker process disconnected from the broker. */
629
- EventCategory["WorkerOffline"] = "worker.offline";
630
- EventCategory["AgentTaskDispatched"] = "agent.task.dispatched";
631
- EventCategory["AgentTaskAssigned"] = "agent.task.assigned";
632
- EventCategory["AgentTrpcConnected"] = "agent.trpc.connected";
633
- EventCategory["AgentWsConnected"] = "agent.ws.connected";
634
- EventCategory["AgentWsDisconnected"] = "agent.ws.disconnected";
635
- EventCategory["AgentBackupActivated"] = "agent.backup.activated";
636
- EventCategory["OrchestrationSettingsUpdated"] = "orchestration.settings-updated";
637
- EventCategory["MotionAnalysis"] = "detection.motion-analysis";
638
- /** All raw motion zones from CCL before minArea filter — for UI debug overlay. */
639
- EventCategory["MotionZonesRaw"] = "detection.motion-zones-raw";
640
- /**
641
- * Per-camera motion phase transition (`watching ↔ active`) emitted
642
- * by the runner. Mirrors the `motion.onMotionChanged` cap event
643
- * surface — payload `MotionOnMotionChangedPayload` carries
644
- * `{deviceId, detected, timestamp, source, regions?}`. Subscribers
645
- * include addons that need to react to motion state without
646
- * polling the runtime-state mirror.
647
- */
648
- EventCategory["MotionOnMotionChanged"] = "motion.on-motion-changed";
649
- EventCategory["DetectionResult"] = "detection.result";
650
- EventCategory["DetectionRaw"] = "detection.raw";
651
- EventCategory["DetectionCameraNative"] = "detection.camera-native";
652
- /**
653
- * Canonical per-chunk live audio pipeline output. Payload is
654
- * `PipelineAudioInferenceResultPayload` carrying a full `AudioResult`
655
- * (level + detections + debug). Lives on the pipeline.* namespace
656
- * alongside `pipeline.inference-result` (video) for symmetry.
657
- */
658
- EventCategory["PipelineAudioInferenceResult"] = "pipeline.audio-inference-result";
659
- EventCategory["DetectionPhaseTransition"] = "detection.phase-transition";
660
- /**
661
- * A camera provider's RAW onboard motion push (D673): the `MotionOnMotionChanged`
662
- * data shape without `triggeredBy`, built by `providerMotionEvent`. Its one
663
- * consumer is the orchestrator's camera-sensors bridge, which re-emits the
664
- * camera's EFFECTIVE onboard motion as `MotionOnMotionChanged` (a pass-through
665
- * for a camera with no attached sensor). Providers must not emit
666
- * `MotionOnMotionChanged` themselves; `scripts/check-onboard-motion-emitters.ts`
667
- * keeps it so (camera-grid is a named, deferred exception).
668
- */
669
- EventCategory["ProviderMotion"] = "provider.motion";
670
- EventCategory["ProviderDetection"] = "provider.detection";
671
- EventCategory["EnrichmentEmbeddingStored"] = "enrichment.embedding.stored";
672
- EventCategory["EnrichmentSceneStateChanged"] = "enrichment.scene.state-changed";
673
- EventCategory["EnrichmentActivitySummary"] = "enrichment.activity.summary";
674
- /**
675
- * Unified track-lifecycle event: ONE category carrying `phase:
676
- * 'start' | 'update' | 'end'` with a rich, typed
677
- * `PipelineAnalyticsTrackLifecyclePayload`. Supersedes the thin
678
- * `PipelineAnalyticsTrackStarted` / `PipelineAnalyticsTrackEnded`
679
- * pair — a consumer subscribes once and branches on `phase`.
680
- */
681
- EventCategory["PipelineAnalyticsTrackLifecycle"] = "pipeline-analytics.track-lifecycle";
682
- /**
683
- * @deprecated Superseded by {@link PipelineAnalyticsTrackLifecycle}
684
- * (`phase:'start'`). Kept as a soft alias — no known subscribers.
685
- */
686
- EventCategory["PipelineAnalyticsTrackStarted"] = "pipeline-analytics.track-started";
687
- /**
688
- * @deprecated Superseded by {@link PipelineAnalyticsTrackLifecycle}
689
- * (`phase:'end'`). Kept as a soft alias — no known subscribers.
690
- */
691
- EventCategory["PipelineAnalyticsTrackEnded"] = "pipeline-analytics.track-ended";
692
- EventCategory["PipelineAnalyticsDetectionEvent"] = "pipeline-analytics.detection-event";
693
- EventCategory["PipelineAnalyticsFrameTracked"] = "pipeline-analytics.frame-tracked";
694
- /**
695
- * Fired by `addon-post-analysis` when a parked (stationary) object appears
696
- * (a track was promoted to a stationary-registry entry) or departs (the
697
- * object moved / was removed). Telemetry (D8): lossy, drives a UI refresh of
698
- * the dedicated "Stationary" section — never the live event feed. Payload:
699
- * `{ deviceId, entryId, className, phase:'appeared'|'departed', timestamp, observedAt, zones }`.
700
- */
701
- EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
702
- /**
703
- * Fired by `addon-post-analysis` when a package-drop is confirmed inside
704
- * a package zone — a newly-appeared stationary object of a package class
705
- * that cleared the class / zone / dwell / size gates. Payload:
706
- * `PipelineAnalyticsPackageDeliveredPayload` carrying `{ deviceId,
707
- * entryId, className, zoneIds, keyFrameMediaId?, bbox, timestamp }`.
708
- * Telemetry (D8): the durable record is the `package-events` store row;
709
- * this bus topic drives notifier rules + live UI. See
710
- * docs/superpowers/specs/2026-07-17-package-zones-design.md §5.1.
711
- */
712
- EventCategory["PipelineAnalyticsPackageDelivered"] = "pipeline-analytics.package-delivered";
713
- /**
714
- * Fired by `addon-post-analysis` when a previously-delivered package
715
- * leaves its zone (the stationary entry departed — moved or swept).
716
- * Payload: `PipelineAnalyticsPackagePickedUpPayload` carrying
717
- * `{ deviceId, entryId, deliveredEventId, className, timestamp }`.
718
- * Telemetry (D8). See package-zones-design §5.2.
719
- */
720
- EventCategory["PipelineAnalyticsPackagePickedUp"] = "pipeline-analytics.package-picked-up";
721
- /**
722
- * Fired by `addon-post-analysis` whenever a gallery face row changes:
723
- * `kind:'buffered'` a new detected face was persisted, `'assigned'` /
724
- * `'unassigned'` its identity link changed, `'deleted'` the row was
725
- * removed. Telemetry semantics (D8): a lost event only delays a refresh,
726
- * never a correctness issue. Payload `PipelineAnalyticsFaceGalleryChangedPayload`.
727
- */
728
- EventCategory["PipelineAnalyticsFaceGalleryChanged"] = "pipeline-analytics.face-gallery-changed";
729
- /**
730
- * Fired by `addon-post-analysis` whenever a gallery plate row changes:
731
- * `kind:'buffered'` a new read was persisted (`PlateRecognizer.onTrackEnd`),
732
- * `'assigned'` / `'unassigned'` its vehicle link changed
733
- * (`PlateGalleryProvider`), `'deleted'` the row was removed. Telemetry
734
- * semantics (D8): a lost event only delays a refresh, never a correctness
735
- * issue. Payload `PipelineAnalyticsPlateGalleryChangedPayload`.
736
- */
737
- EventCategory["PipelineAnalyticsPlateGalleryChanged"] = "pipeline-analytics.plate-gallery-changed";
738
- EventCategory["CameraStreamsProfileSlotsChanged"] = "camera-streams.onProfileSlotsChanged";
739
- /**
740
- * Stream-broker health watchdog. Per-broker (deviceId/profile) emission.
741
- * `stream.offline` fires after STREAM_STALE_TIMEOUT_MS without an encoded
742
- * packet on an active broker. `stream.online` fires on first packet
743
- * after a stale gap (or on initial first packet). Payload includes
744
- * the assigned camStreamId as `profileKey`.
745
- */
746
- EventCategory["StreamOnline"] = "stream.online";
747
- EventCategory["StreamOffline"] = "stream.offline";
748
- EventCategory["NetworkTunnelStarted"] = "network.tunnel.started";
749
- EventCategory["NetworkTunnelStopped"] = "network.tunnel.stopped";
750
- /** Fired by the `local-network` cap when the host's interface set
751
- * changes (new IP from DHCP, VPN connect, docker bridge added). */
752
- EventCategory["LocalNetworkChanged"] = "network.local.changed";
753
- /**
754
- * Fired by a `mesh-network` provider (Tailscale, …) when its
755
- * mesh-reachable host changes (join / leave / MagicDNS or 100.x IP
756
- * change). Lets `local-network` fold the mesh endpoint into its
757
- * connection-endpoint list without a cross-package import. Payload
758
- * carries the preferred host (`host: ''` = no longer reachable),
759
- * the hub port, and the scheme. Telemetry-grade (D8): consumers
760
- * pull-reconcile from the provider's `getStatus` on reconnect. */
761
- EventCategory["MeshNetworkChanged"] = "network.mesh.changed";
762
- EventCategory["BackupCompleted"] = "backup.completed";
763
- /**
764
- * A whole backup RUN finished — every destination attempted, win or lose.
765
- *
766
- * `backup.completed` fires once per DESTINATION, which is the right grain for
767
- * a progress UI and the wrong one for a notification: an operator with three
768
- * destinations would be told three times. And a run where two of three
769
- * destinations succeeded is not a clean success — a single "backup
770
- * completed" that hid the failed one would be a lie, so the count of each is
771
- * carried here and the message says both.
772
- *
773
- * Emitted by the backup orchestrator only after the destination loop, so a
774
- * run that dies during the BUILD phase produces no completion at all.
775
- */
776
- EventCategory["BackupRunCompleted"] = "backup.run-completed";
777
- EventCategory["BackupRestored"] = "backup.restored";
778
- EventCategory["NotificationDispatched"] = "notification.dispatched";
779
- EventCategory["NotificationFailed"] = "notification.failed";
780
- /**
781
- * A notification rule that cannot work (D905): its stored row does not load,
782
- * or it still names a device / delivery target the prune CONFIRMED deleted.
783
- * One edge per rule per distinct set of reasons; never on an unanswered read.
784
- */
785
- EventCategory["NotificationRuleBroken"] = "notification.rule-broken";
786
- /** The clear of {@link NotificationRuleBroken}: the rule loads and names nothing deleted. */
787
- EventCategory["NotificationRuleRepaired"] = "notification.rule-repaired";
788
- /** A migration failed or was blocked; the Alert Center re-reads the ledger. */
789
- EventCategory["MigrationFailed"] = "migration.failed";
790
- /** A migration applied or a failure cleared; the Alert Center re-reads the ledger. */
791
- EventCategory["MigrationApplied"] = "migration.applied";
792
- EventCategory["DeviceUpdated"] = "device.updated";
793
- /**
794
- * Transport-level connectivity. Emitted by the device driver when
795
- * the underlying control socket actually connects / disconnects
796
- * (Baichuan TCP, ONVIF probe response, RTSP DESCRIBE, …) — NOT for
797
- * power-state transitions on a battery camera. For battery wake /
798
- * doze cycles see `DeviceAwake` / `DeviceSleeping`.
799
- */
800
- EventCategory["DeviceOnline"] = "device.online";
801
- /**
802
- * Stream-broker watchdog — emitted when no encoded packet has been
803
- * received for STREAM_STALE_TIMEOUT_MS on an active broker (rtsp or
804
- * push). Paired with DeviceOnline which fires on first packet after
805
- * a stale gap. Payload: DeviceStreamHealthPayload.
806
- */
807
- EventCategory["DeviceOffline"] = "device.offline";
808
- /**
809
- * Battery cam woke up — physical power-state transition reported
810
- * by the device firmware. Distinct from `DeviceOnline` so a UI panel
811
- * watching power state doesn't flap on every UDP socket reconnect.
812
- */
813
- EventCategory["DeviceAwake"] = "device.awake";
814
- /**
815
- * Battery cam went to sleep. See `DeviceAwake`.
816
- */
817
- EventCategory["DeviceSleeping"] = "device.sleeping";
818
- EventCategory["RetentionCleanup"] = "retention.cleanup";
819
- /**
820
- * Legacy bulk-update progress snapshot (payload `BulkUpdateState`). No longer
821
- * emitted — F3 removed the coordinator that produced it; "Update all" now runs
822
- * as one lifecycle engine job (`AddonsJobProgress`/`AddonsJobLog`). Retained
823
- * (with `BulkUpdateState`) only to avoid regenerating the event maps; removed
824
- * in F4 once live bulk progress is re-implemented over the engine events.
825
- */
826
- EventCategory["AddonsBulkUpdateProgress"] = "addons.bulk-update-progress";
827
- EventCategory["AddonsJobProgress"] = "addons.job-progress";
828
- EventCategory["AddonsJobLog"] = "addons.job-log";
829
- /**
830
- * A container's child visibility toggled (hidden/shown). Emitted by the
831
- * `accessories` cap when a child device is hidden or revealed.
832
- * Payload: `{ deviceId, childDeviceId, hidden }`.
833
- */
834
- EventCategory["AccessoriesChildVisibilityChanged"] = "accessories.onChildVisibilityChanged";
835
- /**
836
- * A container's child set changed (children added/removed/reordered).
837
- * Payload: `{ deviceId, childDeviceIds, hiddenChildIds }`.
838
- */
839
- EventCategory["AccessoriesChanged"] = "accessories.onAccessoriesChanged";
840
- /**
841
- * An action run started or ended. Emitted by the `actions` surface.
842
- * Payload: `ActionRunChanged` (`{ actionId, runId, isRunning, outcome?, lastRunAt, lastError }`).
843
- */
844
- EventCategory["ActionsRunChanged"] = "actions.onRunChanged";
845
- /**
846
- * Progress update from a running model conversion job.
847
- * Payload: `{ kind: 'model-convert', phase, sessionId?, pct?, detail? }`.
848
- * Emitted by `addon-model-studio` on the converting node.
849
- */
850
- EventCategory["ModelConvertProgress"] = "model-convert.progress";
851
- return EventCategory;
852
- }({});
853
- //#endregion
854
- //#region ../types/dist/device-type-D1fH5kaC.mjs
855
- /**
856
- import { errMsg } from '@camstack/types'
857
- * Extract a human-readable message from an unknown error value.
858
- * Replaces the ubiquitous `errMsg(err)` pattern.
859
- */
860
- function errMsg(err) {
861
- if (err instanceof Error) return err.message;
862
- if (typeof err === "string") return err;
863
- return String(err);
864
- }
865
- var DeviceType = /* @__PURE__ */ function(DeviceType) {
866
- DeviceType["Camera"] = "camera";
867
- DeviceType["Hub"] = "hub";
868
- DeviceType["Light"] = "light";
869
- DeviceType["Siren"] = "siren";
870
- DeviceType["Switch"] = "switch";
871
- DeviceType["Sensor"] = "sensor";
872
- DeviceType["Thermostat"] = "thermostat";
873
- /** Air-conditioner / heat-pump climate device (HVAC) — shares the
874
- * `climate-control` cap surface with `Thermostat` but renders a
875
- * dedicated AC-appropriate control UI (mode chips, fan speed,
876
- * independent vertical/horizontal swing). Sources: native Gree, and
877
- * reusable by other AC integrations. */
878
- DeviceType["Climate"] = "climate";
879
- DeviceType["Button"] = "button";
880
- /** Generic stateless event emitter — carries a device's EXACT declared
881
- * event vocabulary verbatim (no normalization). Installed with the
882
- * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
883
- * HA bus events (e.g. `zha_event`, generic). */
884
- DeviceType["EventEmitter"] = "event-emitter";
885
- /** Firmware/software update entity — current vs available version,
886
- * updatable flag, update state, and an install action. Installed with
887
- * the `update` cap. Sources: Homematic firmware-update channels (and
888
- * reusable by other providers, e.g. HA `update.*` entities). */
889
- DeviceType["Update"] = "update";
890
- DeviceType["Generic"] = "generic";
891
- /** Generic notification delivery target (HA `notify.<service>`, future
892
- * Telegram / Discord / ntfy / SMTP, …). One device per delivery
893
- * endpoint; the `notifier` cap defines the send surface. */
894
- DeviceType["Notifier"] = "notifier";
895
- /** Pre-recorded action sequence with optional parameters
896
- * (HA `script.*`). Runnable via `script-runner` cap. */
897
- DeviceType["Script"] = "script";
898
- /** Automation rule (HA `automation.*`) — enable/disable + manual
899
- * trigger surface exposed via `automation-control` cap. */
900
- DeviceType["Automation"] = "automation";
901
- /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
902
- DeviceType["Lock"] = "lock";
903
- /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
904
- * `valve.*`). `cover` cap with sub-roles for variant. */
905
- DeviceType["Cover"] = "cover";
906
- /** Pipe / water / gas valve with open/close/stop and optional
907
- * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
908
- * modelled on the same open/closed lifecycle. */
909
- DeviceType["Valve"] = "valve";
910
- /** Humidifier / dehumidifier with on/off + target humidity + mode
911
- * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
912
- * modelled on the same target / mode lifecycle. */
913
- DeviceType["Humidifier"] = "humidifier";
914
- /** Water heater / boiler with target temperature + operation mode +
915
- * away mode (HA `water_heater.*`). `water-heater` cap — a
916
- * climate-family actuator. */
917
- DeviceType["WaterHeater"] = "water-heater";
918
- /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
919
- DeviceType["Fan"] = "fan";
920
- /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
921
- * the camera surface — those use `Camera`. `media-player` cap. */
922
- DeviceType["MediaPlayer"] = "media-player";
923
- /** Security panel / alarm system (HA `alarm_control_panel.*`).
924
- * `alarm-panel` cap. */
925
- DeviceType["AlarmPanel"] = "alarm-panel";
926
- /** Generic user-settable input (HA `number` / `input_number` / `select`
927
- * / `input_select` / `text` / `input_text` / `input_datetime`).
928
- * Sub-type via `DeviceRole`: NumericControl / SelectControl /
929
- * TextControl / DateTimeControl. */
930
- DeviceType["Control"] = "control";
931
- /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
932
- * `presence` cap. */
933
- DeviceType["Presence"] = "presence";
934
- /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
935
- * `weather` cap. */
936
- DeviceType["Weather"] = "weather";
937
- /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
938
- DeviceType["Vacuum"] = "vacuum";
939
- /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
940
- * `lawn-mower-control` cap. */
941
- DeviceType["LawnMower"] = "lawn-mower";
942
- /** Physical HA device group — parent container for entity-children
943
- * adopted from a single HA device entry. Not renderable as a
944
- * standalone device; exists only to anchor child entities. */
945
- DeviceType["Container"] = "container";
946
- /** Single still-image entity (HA `image.*`). Read-only display of an
947
- * `entity_picture` signed URL the browser loads directly. `image` cap. */
948
- DeviceType["Image"] = "image";
949
- /** Smart pet feeder — cloud-connected food dispenser with a bowl food
950
- * level, battery, desiccant life, feeding state and manual-feed /
951
- * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
952
- * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
953
- * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
954
- * integrations sharing the same food/desiccant/hopper surface. */
955
- DeviceType["PetFeeder"] = "pet-feeder";
956
- /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
957
- * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
958
- * of a container and is BUILT as a composed device (D967). */
959
- DeviceType["PetFountain"] = "pet-fountain";
960
- /** Built-in dishwasher — program lifecycle, door / remote-start gates,
961
- * salt and rinse-aid levels. Installed with the `dishwasher` cap.
962
- * Sources: native Bosch Home Connect, reusable by similar integrations.
963
- * A plug-built dishwasher carries running-state and power instead. */
964
- DeviceType["Dishwasher"] = "dishwasher";
965
- /** Washing machine. A plug-built one is running while its power stays
966
- * above a threshold. No program cap — that surface is the dishwasher's. */
967
- DeviceType["Washer"] = "washer";
968
- /** A dumb plug-in stove or heater: a switch (and, with a meter, a power
969
- * reading) that only runs while the plug is on. Built from a smart plug;
970
- * the `timed-run` cap switches it off again after a chosen time. */
971
- DeviceType["Heater"] = "heater";
972
- /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
973
- DeviceType["Dryer"] = "dryer";
974
- /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
975
- * other radio-to-IP bridges). It carries the box's own facts (firmware,
976
- * address, connection state) and the actions the box really supports
977
- * (`reboot`, `device-admin-link`). Products are NOT its children — they are
978
- * adopted beside it — and its connection settings stay on the integration. */
979
- DeviceType["Gateway"] = "gateway";
980
- return DeviceType;
981
- }({});
982
- var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
983
- DeviceFeature["BatteryOperated"] = "battery-operated";
984
- DeviceFeature["Rebootable"] = "rebootable";
985
- /**
986
- * Device supports an on-demand re-sync of its derived spec with its
987
- * upstream source — drives the generic Re-sync button. The owning
988
- * provider implements the action via the `device-adoption.resync` cap.
989
- */
990
- DeviceFeature["Resyncable"] = "resyncable";
991
- DeviceFeature["NativeSnapshot"] = "native-snapshot";
992
- DeviceFeature["DoorbellButton"] = "doorbell-button";
993
- DeviceFeature["TwoWayAudio"] = "two-way-audio";
994
- DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
995
- /**
996
- * Camera supports the on-firmware autotrack subsystem (subject-
997
- * following). Distinct from `PanTiltZoom` because not every PTZ
998
- * camera ships autotrack — the admin UI uses this flag to gate
999
- * the autotrack toggle / settings card without re-deriving from
1000
- * the cap registry. Mirrors `ptz-autotrack` cap registration:
1001
- * driver sets this feature when probe confirms the firmware
1002
- * surface, and registers the cap in the same code path.
1003
- */
1004
- DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
1005
- /**
1006
- * Accessory exposes a "trigger on motion" toggle — the parent camera's
1007
- * motion detection automatically activates this device. Mirrors
1008
- * `motion-trigger` cap registration: drivers set this feature in the
1009
- * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
1010
- *
1011
- * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
1012
- * fast scalar without binding fetch), notifier rules, and `listAll`
1013
- * filters that want "all devices with on-motion behaviour".
1014
- */
1015
- DeviceFeature["MotionTrigger"] = "motion-trigger";
1016
- /** Light supports rgb-triplet color via `color` cap. */
1017
- DeviceFeature["LightColorRgb"] = "light-color-rgb";
1018
- /** Light supports HSV color via `color` cap. */
1019
- DeviceFeature["LightColorHsv"] = "light-color-hsv";
1020
- /** Light supports color-temperature (mired) via `color` cap. */
1021
- DeviceFeature["LightColorMired"] = "light-color-mired";
1022
- /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
1023
- * targetHigh). Gates the range slider UI. */
1024
- DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
1025
- /** Thermostat exposes target humidity and/or current humidity
1026
- * readings. Gates the humidity controls. */
1027
- DeviceFeature["ClimateHumidity"] = "climate-humidity";
1028
- /** Thermostat exposes a fan-mode selector. */
1029
- DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
1030
- /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
1031
- DeviceFeature["ClimatePreset"] = "climate-preset";
1032
- /** Thermostat exposes a vertical louver swing toggle. Gates the
1033
- * vertical-swing switch in the climate UI. Independent of horizontal. */
1034
- DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
1035
- /** Thermostat exposes a horizontal louver swing toggle. Gates the
1036
- * horizontal-swing switch in the climate UI. Independent of vertical. */
1037
- DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
1038
- /** Cover exposes intermediate position control (0..100). Gates the
1039
- * position slider UI. */
1040
- DeviceFeature["CoverPositionable"] = "cover-positionable";
1041
- /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
1042
- DeviceFeature["CoverTilt"] = "cover-tilt";
1043
- /** Cover has a real stop: `stop` halts the travel in place. Gates the stop
1044
- * control in exporters; a provider whose `stop` is a no-op never declares it. */
1045
- DeviceFeature["CoverStoppable"] = "cover-stoppable";
1046
- /** Valve exposes intermediate position control (0..100). Gates the
1047
- * position slider / drag surface UI. */
1048
- DeviceFeature["ValvePositionable"] = "valve-positionable";
1049
- /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
1050
- DeviceFeature["FanSpeed"] = "fan-speed";
1051
- /** Fan exposes a preset mode selector. */
1052
- DeviceFeature["FanPreset"] = "fan-preset";
1053
- /** Fan exposes blade direction (forward/reverse) — typical of
1054
- * ceiling fans. */
1055
- DeviceFeature["FanDirection"] = "fan-direction";
1056
- /** Fan exposes an oscillation toggle. */
1057
- DeviceFeature["FanOscillating"] = "fan-oscillating";
1058
- /** Lock requires a PIN code on lock/unlock. Gates the code-entry
1059
- * field on the UI lock-controls panel. */
1060
- DeviceFeature["LockPinRequired"] = "lock-pin-required";
1061
- /** Lock supports a latch-release ("open door") action distinct from
1062
- * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
1063
- * `supported_features`. Gates the Open Door button in the UI. */
1064
- DeviceFeature["LockOpen"] = "lock-open";
1065
- /** Media player exposes a seek-to-position surface. */
1066
- DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
1067
- /** Media player exposes a volume-level setter. */
1068
- DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
1069
- /** Media player exposes a mute toggle distinct from volume=0. */
1070
- DeviceFeature["MediaPlayerMute"] = "media-player-mute";
1071
- /** Media player exposes a shuffle toggle. */
1072
- DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
1073
- /** Media player exposes a repeat mode (off / all / one). */
1074
- DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
1075
- /** Media player exposes a source / input selector. */
1076
- DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
1077
- /** Media player exposes a play-arbitrary-media surface (URL / id). */
1078
- DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
1079
- /** Media player exposes next-track. */
1080
- DeviceFeature["MediaPlayerNext"] = "media-player-next";
1081
- /** Media player exposes previous-track. */
1082
- DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
1083
- /** Media player exposes stop distinct from pause. */
1084
- DeviceFeature["MediaPlayerStop"] = "media-player-stop";
1085
- /** Media player exposes volume up / down in steps (an infrared TV has no level). */
1086
- DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
1087
- /** Media player exposes channel up / down in steps. */
1088
- DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
1089
- /** Media player exposes power on / off. */
1090
- DeviceFeature["MediaPlayerPower"] = "media-player-power";
1091
- /** Alarm panel requires a PIN code on arm/disarm. */
1092
- DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
1093
- /** Presence device carries GPS coordinates (lat/lng/accuracy) in
1094
- * addition to a textual location. */
1095
- DeviceFeature["PresenceGps"] = "presence-gps";
1096
- /** Notifier accepts an inline / URL image attachment. */
1097
- DeviceFeature["NotifierImage"] = "notifier-image";
1098
- /** Notifier accepts a priority hint (high/normal/low). */
1099
- DeviceFeature["NotifierPriority"] = "notifier-priority";
1100
- /** Notifier accepts a free-form `data` payload for platform-specific
1101
- * fields. */
1102
- DeviceFeature["NotifierData"] = "notifier-data";
1103
- /** Notifier supports interactive action buttons / callbacks. */
1104
- DeviceFeature["NotifierActions"] = "notifier-actions";
1105
- /** Notifier supports per-call recipient targeting (multi-user). */
1106
- DeviceFeature["NotifierRecipients"] = "notifier-recipients";
1107
- /** Script runner accepts a variables map on each run invocation. */
1108
- DeviceFeature["ScriptVariables"] = "script-variables";
1109
- /** Automation `trigger` accepts a skipCondition flag — fires the
1110
- * automation's actions while bypassing its condition block. */
1111
- DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
1112
- /** Robot vacuum exposes a live cleaning map (image child). Gates the
1113
- * map tile in the vacuum UI. */
1114
- DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
1115
- /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
1116
- * switches group. */
1117
- DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
1118
- /** Robot mower exposes a live mowing map (SVG image child). Gates the
1119
- * mower map tile. */
1120
- DeviceFeature["MowerHasMap"] = "mower-has-map";
1121
- /** Robot mower exposes targeted mowing (all-area / zones / edges /
1122
- * spots) — gates the Mow action + map selectors. */
1123
- DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
1124
- return DeviceFeature;
1125
- }({});
1126
- /**
1127
- * Semantic role a device plays within its parent. Populated by driver
1128
- * addons when creating accessory devices (Reolink siren/floodlight/
1129
- * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
1130
- * admin UI to pick icons, labels, and widgets — a `Switch` with
1131
- * `role: Floodlight` renders as a bulb with a brightness slider,
1132
- * whereas a `Switch` with `role: Siren` renders as a klaxon.
1133
- *
1134
- * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
1135
- * sqlite as a nullable TEXT column — old rows keep working unchanged.
1136
- */
1137
- var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
1138
- DeviceRole["Siren"] = "siren";
1139
- DeviceRole["Floodlight"] = "floodlight";
1140
- DeviceRole["Spotlight"] = "spotlight";
1141
- DeviceRole["PirSensor"] = "pir-sensor";
1142
- DeviceRole["Chime"] = "chime";
1143
- DeviceRole["Autotrack"] = "autotrack";
1144
- DeviceRole["Nightvision"] = "nightvision";
1145
- DeviceRole["PrivacyMask"] = "privacy-mask";
1146
- DeviceRole["Doorbell"] = "doorbell";
1147
- /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
1148
- DeviceRole["CoverBlind"] = "cover-blind";
1149
- /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
1150
- DeviceRole["CoverShutter"] = "cover-shutter";
1151
- /** Exterior awning. HA `device_class: awning`. */
1152
- DeviceRole["CoverAwning"] = "cover-awning";
1153
- /** Curtain / drape. HA `device_class: curtain`. */
1154
- DeviceRole["CoverCurtain"] = "cover-curtain";
1155
- /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
1156
- DeviceRole["CoverGarage"] = "cover-garage";
1157
- /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
1158
- DeviceRole["CoverGate"] = "cover-gate";
1159
- /** Door. HA `device_class: door`. A security actuator: exports gate it. */
1160
- DeviceRole["CoverDoor"] = "cover-door";
1161
- /** Window. HA `device_class: window`. */
1162
- DeviceRole["CoverWindow"] = "cover-window";
1163
- /** Virtual HA toggle (input_boolean.*) — distinguishable from a
1164
- * real Switch device for UI rendering / export adapters. */
1165
- DeviceRole["BinaryHelper"] = "binary-helper";
1166
- /** A smart plug: a `switch` whose source says it is an outlet (HA
1167
- * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
1168
- DeviceRole["Outlet"] = "outlet";
1169
- /** Generic motion / occupancy / moving event source. Distinct from
1170
- * the camera accessory PirSensor role: that one is a camera child;
1171
- * this is a standalone HA / 3rd-party motion sensor. */
1172
- DeviceRole["MotionSensor"] = "motion-sensor";
1173
- DeviceRole["ContactSensor"] = "contact-sensor";
1174
- DeviceRole["LeakSensor"] = "leak-sensor";
1175
- DeviceRole["SmokeSensor"] = "smoke-sensor";
1176
- DeviceRole["COSensor"] = "co-sensor";
1177
- DeviceRole["GasSensor"] = "gas-sensor";
1178
- DeviceRole["TamperSensor"] = "tamper-sensor";
1179
- DeviceRole["VibrationSensor"] = "vibration-sensor";
1180
- DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
1181
- DeviceRole["SoundSensor"] = "sound-sensor";
1182
- /** Fallback for `binary_sensor` without a known `device_class`. */
1183
- DeviceRole["BinarySensor"] = "binary-sensor";
1184
- DeviceRole["TemperatureSensor"] = "temperature-sensor";
1185
- DeviceRole["HumiditySensor"] = "humidity-sensor";
1186
- DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
1187
- DeviceRole["PressureSensor"] = "pressure-sensor";
1188
- /** Wind speed or direction (weather-station `wind-sensor` cap). */
1189
- DeviceRole["WindSensor"] = "wind-sensor";
1190
- /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
1191
- DeviceRole["RainSensor"] = "rain-sensor";
1192
- /** UV index (weather-station `uv-sensor` cap). */
1193
- DeviceRole["UvSensor"] = "uv-sensor";
1194
- /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
1195
- * Distinct from AmbientLightSensor (lux). */
1196
- DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
1197
- /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
1198
- DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
1199
- DeviceRole["PowerSensor"] = "power-sensor";
1200
- DeviceRole["EnergySensor"] = "energy-sensor";
1201
- DeviceRole["VoltageSensor"] = "voltage-sensor";
1202
- DeviceRole["CurrentSensor"] = "current-sensor";
1203
- DeviceRole["AirQualitySensor"] = "air-quality-sensor";
1204
- /** Battery level (numeric % via `sensor` OR low-bool via
1205
- * `binary_sensor` — the cap distinguishes via the value type). */
1206
- DeviceRole["BatterySensor"] = "battery-sensor";
1207
- /** Fallback for `sensor` numeric without a known `device_class`. */
1208
- DeviceRole["NumericSensor"] = "numeric-sensor";
1209
- /** String / enum state (HA `sensor` with `state_class: enum` or
1210
- * `attributes.options`). */
1211
- DeviceRole["EnumSensor"] = "enum-sensor";
1212
- /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
1213
- * or `date`). The slice carries the raw ISO string verbatim (hosted on
1214
- * the `enum-sensor` cap); the UI renders it locale-formatted. */
1215
- DeviceRole["DateTimeSensor"] = "datetime-sensor";
1216
- /** Last-resort fallback when nothing else matches. */
1217
- DeviceRole["GenericSensor"] = "generic-sensor";
1218
- DeviceRole["NumericControl"] = "numeric-control";
1219
- DeviceRole["SelectControl"] = "select-control";
1220
- DeviceRole["TextControl"] = "text-control";
1221
- DeviceRole["DateTimeControl"] = "datetime-control";
1222
- /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
1223
- * rich features (image, priority, channel routing). */
1224
- DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
1225
- /** Chat / messaging service (HA `notify.telegram_*`,
1226
- * `notify.discord_*`, etc.). */
1227
- DeviceRole["MessagingNotifier"] = "messaging-notifier";
1228
- /** Email-based delivery (HA `notify.smtp`, etc.). */
1229
- DeviceRole["EmailNotifier"] = "email-notifier";
1230
- /** Fallback when the notifier service name doesn't match a known
1231
- * pattern. */
1232
- DeviceRole["GenericNotifier"] = "generic-notifier";
1233
- /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
1234
- * cap. A `DeviceType.Generic` device. */
1235
- DeviceRole["IrTransmitter"] = "ir-transmitter";
1236
- return DeviceRole;
1237
- }({});
1238
- //#endregion
1239
51
  //#region ../../node_modules/zod/v4/core/core.js
1240
52
  var _a$1;
1241
53
  function $constructor(name, initializer, params) {
@@ -6056,12 +4868,1200 @@ var ZodIssueCode = {
6056
4868
  var ZodFirstPartyTypeKind;
6057
4869
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6058
4870
  //#endregion
6059
- //#region ../../node_modules/zod/v4/classic/coerce.js
6060
- function number(params) {
6061
- return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
4871
+ //#region ../../node_modules/zod/v4/classic/coerce.js
4872
+ function number(params) {
4873
+ return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
4874
+ }
4875
+ //#endregion
4876
+ //#region ../types/dist/event-category-78tcBemo.mjs
4877
+ var EventCategory = /* @__PURE__ */ function(EventCategory) {
4878
+ EventCategory["SystemBoot"] = "system.boot";
4879
+ EventCategory["SystemAddonsReady"] = "system.addons-ready";
4880
+ EventCategory["SystemRestarting"] = "system.restarting";
4881
+ /**
4882
+ * Fired exactly once after the hub finishes booting following a
4883
+ * restart that was triggered by `RestartCoordinator` (today: framework
4884
+ * live-update). Payload is the marker that was on disk at boot
4885
+ * (`PendingRestartMarkerPayload`); admin UI listens for it to display a
4886
+ * success toast describing what changed.
4887
+ *
4888
+ * Spec: docs/superpowers/specs/2026-05-14-framework-live-update-design.md
4889
+ */
4890
+ EventCategory["SystemRestartCompleted"] = "system.restart-completed";
4891
+ /**
4892
+ * The hub reissued its own TLS certificate at boot (`ensureTlsCert`).
4893
+ * Emitted only when the material on disk actually changed, so an
4894
+ * operator who trusted the old certificate by hand is told rather than
4895
+ * discovering it as a browser error. Payload `TlsCertChangedPayload`.
4896
+ *
4897
+ * Rule: docs/decisions/adr-0227-*.md
4898
+ */
4899
+ EventCategory["SystemTlsCertChanged"] = "system.tls-cert-changed";
4900
+ /**
4901
+ * A newer addon or server-root package version was found by the
4902
+ * authoritative registry check. Emitted once when any observed
4903
+ * `latestVersion` changes (or a package/node first appears behind);
4904
+ * the payload carries the full currently-available list. Repeated
4905
+ * polling of the same latests is silent.
4906
+ */
4907
+ EventCategory["UpdateAvailable"] = "update.available";
4908
+ /**
4909
+ * Readiness transition for a capability provider. Every producer emits
4910
+ * this event on `onInitialize` completion, `onDestroy`, and
4911
+ * `$node.reconnect`; every consumer that needs to gate on a cross-process
4912
+ * cap subscribes via the kernel's readiness module (`awaitReady` /
4913
+ * `onReadyState`) instead of polling. Payload is
4914
+ * `SystemReadyStatePayload` — see event-bus.ts.
4915
+ */
4916
+ EventCategory["SystemReadyState"] = "system.ready-state";
4917
+ EventCategory["AddonStarted"] = "addon.started";
4918
+ EventCategory["AddonStopped"] = "addon.stopped";
4919
+ EventCategory["AddonRestarted"] = "addon.restarted";
4920
+ EventCategory["AddonUpdated"] = "addon.updated";
4921
+ EventCategory["AddonInstalled"] = "addon.installed";
4922
+ EventCategory["AddonUninstalled"] = "addon.uninstalled";
4923
+ EventCategory["AddonCrashed"] = "addon.crashed";
4924
+ /**
4925
+ * A RUNNER the D6 crash circuit-breaker gave up on — terminal.
4926
+ *
4927
+ * `AddonCrashed` is the routine, self-healing fact ("it crashed; it is being
4928
+ * respawned"); this is the one that never resolves itself. Emitted exactly
4929
+ * once per trip, by `process-service.ts`, carrying the node, the runner, the
4930
+ * addons it hosted and the crash count that tripped the breaker.
4931
+ *
4932
+ * It exists because a runner marked terminally `failed` used to be SILENT:
4933
+ * on 2026-08-18 the `recorder` runner died of three unhandled ffmpeg spawn
4934
+ * errors, the breaker stopped respawning it (correctly), `/health` kept
4935
+ * returning 200, and fleet-wide recording was gone for 3h15 before the
4936
+ * operator's phone told him. The Notification Center's system-event intake
4937
+ * consumes this category and maps it to the `addon-crash-loop` kind.
4938
+ */
4939
+ EventCategory["AddonRunnerFailed"] = "addon.runner-failed";
4940
+ /**
4941
+ * A runner whose addon refused to start with a typed reason (D877) - parked
4942
+ * in `refused`, never counted by the crash breaker, retried on operator
4943
+ * action. One event per refusal.
4944
+ */
4945
+ EventCategory["AddonRunnerRefused"] = "addon.runner-refused";
4946
+ /**
4947
+ * A runner settled on a state (D877): `running` (READY), `crashed`, `failed` or
4948
+ * `refused`. In-process telemetry for the boot check (re-evaluate while any
4949
+ * runner is still pending) and the alert centre (close a recovered runner).
4950
+ */
4951
+ EventCategory["ProcessStateChanged"] = "process.state-changed";
4952
+ /**
4953
+ * The node's one-shot runner boot check (D877): planned runners against the
4954
+ * process service, `degraded` when any is failed, refused or missing. The
4955
+ * alert centre turns it into one critical alert.
4956
+ */
4957
+ EventCategory["SystemRunnerCheck"] = "system.runner-check";
4958
+ /**
4959
+ * An alert EPISODE opened or closed, emitted by the alert centre for alerts
4960
+ * of severity `critical` or `error` (`AlertEpisodePayload`). The alert
4961
+ * centre sends no notification of its own: Notification Center turns this
4962
+ * into the `alert-critical` / `alert-error` / `alert-critical-resolved`
4963
+ * system-event kinds, and reconciles from the alerts store because a bus
4964
+ * event may be dropped (D8, D11).
4965
+ */
4966
+ EventCategory["AlertEpisode"] = "alert.episode";
4967
+ EventCategory["AddonError"] = "addon.error";
4968
+ EventCategory["AddonPageReady"] = "addon.page-ready";
4969
+ EventCategory["AddonWidgetReady"] = "addon.widget-ready";
4970
+ /**
4971
+ * Addon failed to load (import or initialize). Emitted by the kernel's
4972
+ * AddonHealthMonitor only AFTER the boot grace period ends — failures
4973
+ * during the first 5 minutes are silently retried without alerting
4974
+ * (slow-starting addons must have time to come up). Post-grace, this
4975
+ * event is emitted exactly once per failure-streak; AlertCenter
4976
+ * consumes it to create a persistent operator-visible alert.
4977
+ *
4978
+ * Payload: `{ packageName, addonId?, error: { message, stack }, retryCount, nextRetryAt }`.
4979
+ */
4980
+ EventCategory["AddonLoadFailed"] = "addon.load-failed";
4981
+ /**
4982
+ * A keychain value was stored or removed (D787). Carries the ref, its
4983
+ * owner and the new version — NEVER the value. A process holding the ref
4984
+ * drops its cached copy and the owning `BaseAddon` re-resolves its config.
4985
+ * Telemetry (D8): a process that misses it still revalidates by version.
4986
+ */
4987
+ EventCategory["KeychainChanged"] = "keychain.changed";
4988
+ /**
4989
+ * Addon recovered from a previous failure. Emitted when an addon
4990
+ * transitions from `failed` back to `healthy` (typically via the
4991
+ * monitor's auto-retry loop, or after manual `addons.retryLoad`).
4992
+ * AlertCenter dismisses the corresponding `AddonLoadFailed` alert
4993
+ * on this event.
4994
+ */
4995
+ EventCategory["AddonLoadRecovered"] = "addon.load-recovered";
4996
+ /**
4997
+ * An addon is RUNNING although its installed package is below what the
4998
+ * running server requires (`camstack.requires.addons` of the server, or the
4999
+ * addon's own `requires.server`). Raised once per streak by the unsupported-
5000
+ * addon tracker, on the hub and on agents; AlertCenter turns it into one
5001
+ * operator alert per node and package. The addon is never held back for it.
5002
+ *
5003
+ * Payload: `{ packageName, addonId, nodeId, installedVersion, requiredMinimum, serverVersion, unmet }`.
5004
+ */
5005
+ EventCategory["AddonUnsupportedVersion"] = "addon.unsupported-version";
5006
+ /**
5007
+ * The package of an {@link AddonUnsupportedVersion} alert reached a supported
5008
+ * version, or is no longer installed on the node. AlertCenter dismisses the matching alert.
5009
+ *
5010
+ * Payload: `{ packageName, addonId, nodeId }`.
5011
+ */
5012
+ EventCategory["AddonUnsupportedResolved"] = "addon.unsupported-resolved";
5013
+ /**
5014
+ * Monitor scheduled the next retry for a failed addon. Transient —
5015
+ * surfaced to the UI for live-updating the "next retry in Ns"
5016
+ * countdown on the Addons page row, NOT persisted as an alert.
5017
+ */
5018
+ EventCategory["AddonRetryScheduled"] = "addon.retry-scheduled";
5019
+ /**
5020
+ * A liveness invariant of this node is false — it has no devices, every
5021
+ * camera is offline, or a camera that was recording has stopped producing
5022
+ * segments. Emitted by the `liveness-monitor` builtin, once per fault (the
5023
+ * `findingId` is stable across ticks), only after its boot grace period.
5024
+ * AlertCenter raises a persistent operator-visible alert.
5025
+ *
5026
+ * It exists because on 2026-08-03 the hub ran four hours with zero devices
5027
+ * and zero recordings while every surface stayed quiet.
5028
+ *
5029
+ * Payload: `{ findingId, severity, title, message, deviceId? }`.
5030
+ */
5031
+ EventCategory["SystemLivenessFailed"] = "system.liveness-failed";
5032
+ /**
5033
+ * A previously reported liveness fault is true again. AlertCenter dismisses
5034
+ * the matching `SystemLivenessFailed` alert.
5035
+ *
5036
+ * Payload: `{ findingId }`.
5037
+ */
5038
+ EventCategory["SystemLivenessRecovered"] = "system.liveness-recovered";
5039
+ /**
5040
+ * Monitor is attempting to reload a failed addon NOW. UI uses this
5041
+ * to show a spinner during the retry attempt. Same transient nature
5042
+ * as AddonRetryScheduled.
5043
+ */
5044
+ EventCategory["AddonRetryAttempting"] = "addon.retry-attempting";
5045
+ EventCategory["DeviceRegistered"] = "device.registered";
5046
+ EventCategory["DeviceUnregistered"] = "device.unregistered";
5047
+ EventCategory["DeviceEnabled"] = "device.enabled";
5048
+ EventCategory["DeviceDisabled"] = "device.disabled";
5049
+ EventCategory["DeviceSettingsUpdated"] = "device.settings-updated";
5050
+ /**
5051
+ * Emitted when the set of native capability providers bound to a device
5052
+ * changes — e.g. an addon registers a new native cap via
5053
+ * `DeviceContext.registerNativeCap`, or all native bindings for a device
5054
+ * are cleared on removal. Hub consumers re-resolve device-proxy routes
5055
+ * when this fires.
5056
+ */
5057
+ EventCategory["DeviceBindingsChanged"] = "device.bindings-changed";
5058
+ /**
5059
+ * Emitted when the operator-organisational meta surface changes
5060
+ * (`name` / `location` / `disabled`). Payload: `{deviceId, field,
5061
+ * value}`. Live consumers (UI device list, alert center) react
5062
+ * without polling. Distinct from `DeviceSettingsUpdated` which
5063
+ * fires on hardware-config changes (host/port/credentials/etc).
5064
+ */
5065
+ EventCategory["DeviceMetaChanged"] = "device.meta-changed";
5066
+ /**
5067
+ * Emitted by `BaseDevice.updateSourceInfo()` after a successful patch
5068
+ * to the device's upstream-system identity / rendering envelope. The
5069
+ * full new SourceInfo travels in the payload so cross-process listeners
5070
+ * (UI, export adapters) don't need to re-read the meta blob.
5071
+ *
5072
+ * Payload: `{deviceId, sourceInfo}`.
5073
+ *
5074
+ * Distinct from `DeviceMetaChanged` which covers the operator-edited
5075
+ * surface (`name` / `location` / `disabled`). The two share the
5076
+ * persistence layer (both ride on `device-manager.setMetadata` for
5077
+ * SourceInfo, or `setName`/`setLocation`/`setDisabled` for the meta
5078
+ * fields) but consumers care about different subsets.
5079
+ */
5080
+ EventCategory["DeviceSourceInfoChanged"] = "device.source-info-changed";
5081
+ /**
5082
+ * Emitted by DeviceStreamWiringService after a successful
5083
+ * `stream-broker.registerDeviceStreams` call. Payload includes
5084
+ * deviceId — consumers look up the full registered device info via
5085
+ * `brokerManager.getRegisteredDevice(deviceId)`.
5086
+ *
5087
+ * Replaces the legacy `StreamRouterService.onDeviceRegistered` callback.
5088
+ */
5089
+ EventCategory["DeviceStreamsRegistered"] = "device.streams-registered";
5090
+ /**
5091
+ * Device-level "fully provisioned" signal — the EXPORT trigger. Emitted once
5092
+ * a device's persisted export-relevant shape (its `DeviceFeature` set) is
5093
+ * established or changes, carrying `{deviceId, fingerprint, generation}`.
5094
+ * Export adapters (Alexa / HAP) react to THIS instead of the chatty per-cap
5095
+ * `DeviceBindingsChanged`, turning a boot's incomplete→complete trickle into
5096
+ * one clean delta. `fingerprint` is `canonicalDeviceFingerprint(shape)`.
5097
+ *
5098
+ * Spec: docs/superpowers/specs/2026-06-01-alexa-hap-export-reconciler-design.md
5099
+ */
5100
+ EventCategory["DeviceProvisioned"] = "device.provisioned";
5101
+ /**
5102
+ * Fires once when a device's initial feature-probe completes successfully
5103
+ * (lastProbedAt 0→>0); exported shape is now stable. Telemetry (may be
5104
+ * dropped) — consumers MUST also gate on the device record's `probed` flag.
5105
+ */
5106
+ EventCategory["DeviceReady"] = "device.ready";
5107
+ EventCategory["IntegrationEnabled"] = "integration.enabled";
5108
+ EventCategory["IntegrationDisabled"] = "integration.disabled";
5109
+ EventCategory["IntegrationDeleted"] = "integration.deleted";
5110
+ /** Emitted when a broker connection's status flips (connected /
5111
+ * disconnected / error). Carries `{ brokerId, status, error? }`. */
5112
+ EventCategory["BrokerStatusChanged"] = "broker.status-changed";
5113
+ /** Emitted for every subscription-matched message routed by a
5114
+ * broker provider. Carries `{ brokerId, subscriptionId, key, payload }`.
5115
+ * Consumers filter by `brokerId` + `subscriptionId` in the handler. */
5116
+ EventCategory["BrokerMessage"] = "broker.message";
5117
+ EventCategory["ProviderStarted"] = "provider.started";
5118
+ EventCategory["ProviderStopped"] = "provider.stopped";
5119
+ EventCategory["ProcessCrashed"] = "process.crashed";
5120
+ EventCategory["ProcessRestartScheduled"] = "process.restart_scheduled";
5121
+ EventCategory["ProcessRestarted"] = "process.restarted";
5122
+ /**
5123
+ * The SET of storage locations changed — one was created, edited, enabled,
5124
+ * disabled or deleted through the location instances or the forced-delete action.
5125
+ *
5126
+ * Telemetry, not a transaction (D8/D11): every consumer that re-resolves on
5127
+ * it must also converge on its own periodic path, because a dropped event
5128
+ * must not leave a node writing to yesterday's disk set forever. It exists
5129
+ * because there was NO signal at all — an operator who added a second
5130
+ * recordings disk in the admin UI got nothing, and the recorder kept its
5131
+ * resolved locations until something else happened to re-resolve them
5132
+ * (D387). Payload `StorageLocationsChangedPayload`.
5133
+ */
5134
+ EventCategory["StorageLocationsChanged"] = "storage.locations-changed";
5135
+ EventCategory["RecordingStarted"] = "recording.started";
5136
+ EventCategory["RecordingStopped"] = "recording.stopped";
5137
+ EventCategory["RecordingError"] = "recording.error";
5138
+ EventCategory["RecordingHealthDegraded"] = "recording.health.degraded";
5139
+ /** Recording enabled but no segment finalized for the NC stall window. */
5140
+ EventCategory["RecordingHealthStalled"] = "recording.health.stalled";
5141
+ /** Recovery from {@link RecordingHealthStalled}. */
5142
+ EventCategory["RecordingHealthOk"] = "recording.health.ok";
5143
+ EventCategory["RecordingStorageCritical"] = "recording.storage.critical";
5144
+ /** Free space on a storage location crossed the warn bar. */
5145
+ EventCategory["StorageLocationLow"] = "storage.location.low";
5146
+ /** Free space at/under the critical bar, or capacity probe failed (`null`). */
5147
+ EventCategory["StorageLocationCritical"] = "storage.location.critical";
5148
+ /** Recovery from low/critical on a storage location. */
5149
+ EventCategory["StorageLocationOk"] = "storage.location.ok";
5150
+ EventCategory["RecordingSegmentWritten"] = "recording.segment.written";
5151
+ EventCategory["RecordingPolicyFallback"] = "recording.policy.fallback";
5152
+ EventCategory["RecordingRetentionCompleted"] = "recording.retention.completed";
5153
+ /** Export render progress (0–100). Telemetry (D8): a lost tick is a stale
5154
+ * progress bar the client reconciles via `recordingExport.getExport`. */
5155
+ EventCategory["RecordingExportProgress"] = "recording.export.progress";
5156
+ EventCategory["RecordingExportCompleted"] = "recording.export.completed";
5157
+ EventCategory["RecordingExportFailed"] = "recording.export.failed";
5158
+ EventCategory["DetectionEvent"] = "detection.event";
5159
+ EventCategory["SessionTrackNew"] = "session.track.new";
5160
+ EventCategory["SessionTrackExpired"] = "session.track.expired";
5161
+ EventCategory["BenchmarkProgress"] = "benchmark.progress";
5162
+ EventCategory["PlatformProbePhase"] = "platform-probe.phase";
5163
+ EventCategory["PipelineProgress"] = "pipeline.progress";
5164
+ /** Per-frame execution trace emitted by the pipeline executor for live observability. */
5165
+ EventCategory["PipelineTrace"] = "pipeline.trace";
5166
+ /**
5167
+ * Raw inference output emitted by `addon-pipeline-runner` after running the
5168
+ * detection pipeline on a frame. Carries the `FrameResult` (with
5169
+ * `detections[]`, `width`/`height`, timing debug) — never the frame
5170
+ * buffer itself. Hub-side consumers (analysis pipeline, class filters,
5171
+ * notifications) subscribe to this and re-emit `detection.result` after
5172
+ * post-processing.
5173
+ */
5174
+ EventCategory["PipelineInferenceResult"] = "pipeline.inference-result";
5175
+ /**
5176
+ * Camera lifecycle event emitted by `addon-pipeline-orchestrator` when it
5177
+ * assigns or unassigns a camera to/from an agent. Carries no frame data;
5178
+ * pure observability for UI dashboards and metrics consumers.
5179
+ */
5180
+ EventCategory["PipelineCameraAssigned"] = "pipeline.camera-assigned";
5181
+ EventCategory["PipelineCameraUnassigned"] = "pipeline.camera-unassigned";
5182
+ /**
5183
+ * A node the orchestrator would otherwise place cameras on has NO usable
5184
+ * inference device: the operator enabled one or more accelerators there and
5185
+ * the live probe reports every one of them unavailable. Emitted once per
5186
+ * TRANSITION into that state (never per dispatch), and the node is dropped
5187
+ * from the placement candidate set for as long as it holds.
5188
+ *
5189
+ * This exists because the state was previously invisible: agent-a
5190
+ * absorbed 283k inference errors in a day while still being handed cameras,
5191
+ * and nothing in the system said so.
5192
+ *
5193
+ * A node with no accelerators configured at all is NOT this — its devices
5194
+ * are `disabled`, not `unavailable`, and the runner's default CPU pool
5195
+ * serves it exactly as before.
5196
+ */
5197
+ EventCategory["PipelineNodeInferenceUnavailable"] = "pipeline.node-inference-unavailable";
5198
+ /**
5199
+ * A camera has an OPEN detection session and has produced no detection at
5200
+ * all for longer than the blind threshold — the camera is being decoded and
5201
+ * inferred and is returning nothing. Emitted once per transition into blind,
5202
+ * per camera.
5203
+ *
5204
+ * The failure it reports: a 1h43 detection blackout on the entrance camera
5205
+ * that nobody noticed, because "a camera that detects nothing" and "a quiet
5206
+ * camera" produce byte-identical silence.
5207
+ */
5208
+ EventCategory["PipelineDetectionBlind"] = "pipeline.detection-blind";
5209
+ /**
5210
+ * A camera's motion source has fired constantly with no subject for an hour
5211
+ * (motion duty or session count over the bar, zero detections) — it is not
5212
+ * blind, its motion zones or sensitivity want tuning (D907). Paired with
5213
+ * {@link PipelineMotionCalm}.
5214
+ */
5215
+ EventCategory["PipelineMotionNoisy"] = "pipeline.motion-noisy";
5216
+ /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
5217
+ EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
5218
+ /**
5219
+ * A camera's package LLM confirm has failed closed three times in a row
5220
+ * (D931, 2026-10-26). One edge per camera until
5221
+ * {@link PipelinePackageLlmAnswered}.
5222
+ */
5223
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
5224
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
5225
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
5226
+ /**
5227
+ * Per-camera pipeline config was mutated by the orchestrator
5228
+ * (a settings change via `setAgentInferenceDevices` /
5229
+ * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
5230
+ * subscribes to its own emission to hot-reload the assigned runner
5231
+ * via `attachCamera` so the next frame executes against the new
5232
+ * engine/steps without waiting for a rebalance or the next
5233
+ * `DeviceStreamsRegistered` cycle.
5234
+ */
5235
+ EventCategory["PipelineCameraUpdated"] = "pipeline.camera-updated";
5236
+ /**
5237
+ * The cluster camera-source OWNER changed (`clusterRoles.ingestNode`).
5238
+ * Emitted by addon-pipeline-orchestrator whenever it (re)derives node
5239
+ * capabilities — at boot, on agent online/offline, and on an ingest-node
5240
+ * flip. Carries the resolved `ownerNodeId`. The stream-broker consumes it to
5241
+ * keep its ingest-owner-gate decision current WITHOUT a per-`ensureBroker`
5242
+ * cross-process `getIngestOwner` query (push the authority's decision instead
5243
+ * of polling it on the hot path). Idempotent state — re-emitted on every
5244
+ * topology change, so a dropped event self-heals on the next one (plus the
5245
+ * broker's long backstop reconcile query).
5246
+ */
5247
+ EventCategory["PipelineIngestOwnerChanged"] = "pipeline.ingest-owner-changed";
5248
+ /**
5249
+ * Periodic snapshot of per-node pipeline-runner load
5250
+ * (`RunnerLocalLoad`). Emitted ~1Hz by every runner so UI dashboards
5251
+ * subscribe instead of polling `pipelineRunner.getLocalLoad`.
5252
+ * `nodeId` carried in the payload + on `event.source.nodeId`.
5253
+ */
5254
+ EventCategory["PipelineRunnerLoadSnapshot"] = "pipeline.runner-load-snapshot";
5255
+ /**
5256
+ * Periodic snapshot of per-camera pipeline metrics (`CameraMetrics`
5257
+ * + `deviceId` + `nodeId`). Emitted ~1Hz by every runner for each
5258
+ * attached camera. UI subscribes to drive overlay phase / fps /
5259
+ * inference time without polling `getCameraMetrics`.
5260
+ */
5261
+ EventCategory["PipelineCameraMetricsSnapshot"] = "pipeline.camera-metrics-snapshot";
5262
+ /**
5263
+ * Periodic snapshot of stream-broker per-broker statistics (input
5264
+ * fps, decoded fps, bitrate, codec). Emitted ~1Hz by every
5265
+ * stream-broker process for each active broker so the UI can drive
5266
+ * the Stream / Cluster dashboards without polling
5267
+ * `streamBroker.listAllProfileSlots` and friends.
5268
+ */
5269
+ EventCategory["StreamBrokerMetricsSnapshot"] = "stream-broker.metrics-snapshot";
5270
+ /**
5271
+ * Cap event fired by `stream-broker` when a profile slot enters
5272
+ * "demanded" state — a cam stream has been assigned and at least one
5273
+ * consumer (RTSP restream, decoded subscriber, WebRTC session, …) is
5274
+ * present. Camera-provider addons (Reolink Baichuan push, …)
5275
+ * subscribe to this category to start their underlying transport
5276
+ * lazily. Payload: `{ deviceId, camStreamId, profile }`.
5277
+ */
5278
+ EventCategory["StreamBrokerOnCamStreamDemand"] = "stream-broker.onCamStreamDemand";
5279
+ /**
5280
+ * Cap event fired by `stream-broker` when the last consumer leaves a
5281
+ * previously-demanded cam stream. Providers tear down their
5282
+ * underlying transport on receipt. Payload: `{ deviceId, camStreamId }`.
5283
+ */
5284
+ EventCategory["StreamBrokerOnCamStreamIdle"] = "stream-broker.onCamStreamIdle";
5285
+ /**
5286
+ * Cap event fired by `stream-broker` when a broker fails to dial a
5287
+ * managed-loopback source (today: `pull-rfc4571`) and the publisher
5288
+ * needs to refresh the cached URL. The lib's TCP server idle-tears-down
5289
+ * on its own schedule, so a re-publish with a fresh `host:port` is the
5290
+ * only way to keep the broker dialable. Camera providers (Reolink
5291
+ * Baichuan native, …) subscribe and respond by re-running their publish
5292
+ * pipeline.
5293
+ * Payload: `{ deviceId, camStreamId, brokerId }`.
5294
+ */
5295
+ EventCategory["StreamBrokerOnRequestStreamSourceRefresh"] = "stream-broker.onRequestStreamSourceRefresh";
5296
+ /**
5297
+ * A camera provider changed a device's stream parameters (codec /
5298
+ * resolution / bitrate / a stream added or removed). A LOW-LATENCY NUDGE
5299
+ * for the stream-broker's catalog reconcile: it carries NO authoritative
5300
+ * data — the broker re-PULLS that device's `stream-catalog` cap on receipt
5301
+ * (the 30s reconcile poll is the backstop if this nudge is dropped). Emitted
5302
+ * by providers from `stream-params.setProfile`. Payload: `{ deviceId }`.
5303
+ */
5304
+ EventCategory["StreamParamsChanged"] = "stream-params.changed";
5305
+ /**
5306
+ * Generic per-device runtime-state change. Fired by `device-manager`
5307
+ * whenever a persisted slice in any cap's `runtimeState` shape
5308
+ * mutates. Payload: `{deviceId, capName, slice}`. Subscribers are
5309
+ * the `deviceState` cap router (cross-process listeners) and the
5310
+ * deviceProxy reactive bindings (`device.state.<capName>.value`).
5311
+ * Cap-specific events (`battery.onStatusChanged`, …) still fire
5312
+ * — they're authoritative for callers that want a typed payload
5313
+ * without filtering on `capName`.
5314
+ */
5315
+ EventCategory["DeviceStateChanged"] = "device.state-changed";
5316
+ /**
5317
+ * The NATIVE shadow of a cap a composition has claimed fields of moved
5318
+ * (D663). Payload: `{ deviceId, capName, native }` — what the provider
5319
+ * wrote, BEFORE the owned fields are overlaid. Owner-only (D224): it is for
5320
+ * the composer's native self-reads, and the public event routers refuse it
5321
+ * (`isOwnerOnlyEventCategory`) so no consumer surface ever shows a second
5322
+ * truth beside the merged `DeviceStateChanged.slice`. Emitted only under a
5323
+ * `replace` claim, one per native write of that cap.
5324
+ */
5325
+ EventCategory["DeviceNativeShadowChanged"] = "device.native-shadow-changed";
5326
+ /**
5327
+ * Frame occupancy for a camera CHANGED — a tracked object was gained or
5328
+ * lost. Carries `{ deviceId, totalObjects, byClass, zones }`.
5329
+ *
5330
+ * Emitted only on a change, so a steady scene is silent. It exists so a
5331
+ * client can stop polling `zoneAnalytics.getCurrentSnapshot`: that was the
5332
+ * one live badge with no push signal at all, and it cost a request every
5333
+ * four seconds per visible camera.
5334
+ *
5335
+ * Like every event it is telemetry and may be dropped ([D8]) — a consumer
5336
+ * keeps a slow reconcile rather than trusting it alone.
5337
+ */
5338
+ EventCategory["ZoneAnalyticsOccupancyChanged"] = "zone-analytics.occupancy-changed";
5339
+ /**
5340
+ * Cap event fired by every device that registers the `battery`
5341
+ * capability. Mirrors the cap definition's `onStatusChanged`. Carries
5342
+ * `{ deviceId, status: BatteryStatus }`. Subscribers (alert center,
5343
+ * snapshot wrapper, UI) react to charge/sleep transitions without
5344
+ * polling `batteryCapability.getStatus`.
5345
+ */
5346
+ EventCategory["BatteryOnStatusChanged"] = "battery.onStatusChanged";
5347
+ /**
5348
+ * Cap event fired by every device that registers the `network-link`
5349
+ * capability. Mirrors the cap definition's `onStatusChanged`. Carries
5350
+ * `{ deviceId, status: NetworkLinkStatus }` — a link switch or a signal
5351
+ * reading that moved.
5352
+ */
5353
+ EventCategory["NetworkLinkOnStatusChanged"] = "network-link.onStatusChanged";
5354
+ /**
5355
+ * Emitted by the battery cap provider WHEN `wakeForStream` enters the
5356
+ * "wake in progress" window — between the Baichuan wake-up issue and
5357
+ * the camera's first dialed-back RTP packet. The stream-broker
5358
+ * manager subscribes to flip the per-broker placeholder reason to
5359
+ * `'waking'` so viewers see a labelled "WAKING UP" tile instead of
5360
+ * the generic `'reconnecting'` frame. Carries `{ deviceId }`. The
5361
+ * complementary "wake complete" signal is the existing
5362
+ * `BatteryOnStatusChanged { sleeping: false }` event.
5363
+ */
5364
+ EventCategory["BatteryOnWakeStarted"] = "battery.onWakeStarted";
5365
+ /**
5366
+ * Cap event fired by every device that registers the `doorbell`
5367
+ * capability. Mirrors `doorbellCapability.events.onPressed`. Carries
5368
+ * `{ deviceId, timestamp }`. Operators consuming the UI subscribe
5369
+ * here to render transient ring toasts and a "Recent presses" row
5370
+ * on the device detail page.
5371
+ */
5372
+ EventCategory["DoorbellOnPressed"] = "doorbell.onPressed";
5373
+ /** Mirrors `runningStateCapability.events.onStarted`. */
5374
+ EventCategory["RunningStarted"] = "running.started";
5375
+ /** Mirrors `runningStateCapability.events.onFinished`. */
5376
+ EventCategory["RunningFinished"] = "running.finished";
5377
+ /** Mirrors `deviceRunsCapability.events.onRunOpened` - a durable run record opened. */
5378
+ EventCategory["DeviceRunOpened"] = "device-run.opened";
5379
+ /** Mirrors `deviceRunsCapability.events.onRunClosed` - a durable run record closed. */
5380
+ EventCategory["DeviceRunClosed"] = "device-run.closed";
5381
+ /**
5382
+ * Cap event fired by every device that registers the `event-emitter`
5383
+ * capability. Mirrors `eventEmitterCapability.events.onEvent`. Carries
5384
+ * `{ deviceId, eventType, data, timestamp, seq }` — the device's EXACT
5385
+ * declared event verbatim (NO normalization). Subscribers (UI event
5386
+ * stream, advanced-notifier rules) react to fired events without
5387
+ * holding a cap reference.
5388
+ */
5389
+ EventCategory["EventEmitted"] = "event-emitter.event";
5390
+ /**
5391
+ * Periodic snapshot of the per-node detection-pipeline engine
5392
+ * registry (loaded engines, models resident, in-use cameras, idle
5393
+ * TTL). Emitted ~0.2Hz (every 5 s) by every detection-pipeline
5394
+ * process. The Engines tab subscribes to drive its inventory view
5395
+ * without polling `pipelineExecutor.listLoadedEngines`.
5396
+ */
5397
+ EventCategory["PipelineEngineMetricsSnapshot"] = "pipeline.engine-metrics-snapshot";
5398
+ /**
5399
+ * Per-node detection-engine runtime-provisioning transition. Emitted by
5400
+ * the detection-pipeline provider on every state change of its lazy
5401
+ * engine-provisioning machine (idle → installing → verifying → ready,
5402
+ * or → failed with a `nextRetryAt`). Payload is the
5403
+ * `EngineProvisioningState` snapshot; `event.source.nodeId` carries the
5404
+ * node. The Pipeline page subscribes to drive a live "installing
5405
+ * OpenVINO… / ready" indicator per node without polling
5406
+ * `pipelineExecutor.getEngineProvisioning`. Telemetry-grade (D8): the UI
5407
+ * also reads the cap snapshot on mount / reconnect. Phase 2.
5408
+ */
5409
+ EventCategory["PipelineEngineProvisioning"] = "pipeline.engine-provisioning";
5410
+ /**
5411
+ * Cluster topology snapshot. Carries the same payload returned by
5412
+ * `nodes.topology` (every reachable node + addons + processes).
5413
+ * Emitted by the hub on any agent / addon lifecycle change
5414
+ * (debounced) plus a periodic safety net. Replaces UI polling on
5415
+ * `nodes.topology` — admin-ui dashboards subscribe to drive the
5416
+ * cluster view directly from the event payload.
5417
+ */
5418
+ EventCategory["ClusterTopologySnapshot"] = "cluster.topology-snapshot";
5419
+ /**
5420
+ * Periodic per-node system metrics snapshot (CPU / memory / GPU /
5421
+ * disk / network). Emitted ~0.2 Hz by the metrics-provider addon
5422
+ * for each node. Drives the dashboard SystemStatus / ProcessResources
5423
+ * widgets without polling `metricsProvider.getCurrent`.
5424
+ */
5425
+ EventCategory["MetricsNodeResourcesSnapshot"] = "metrics.node-resources-snapshot";
5426
+ /**
5427
+ * Periodic per-node process-tree snapshot (camstack-related pids
5428
+ * with root / managed / system classification). Emitted ~0.2 Hz by
5429
+ * the metrics-provider addon. Drives the Cluster → Processes tab
5430
+ * without polling `metricsProvider.listNodeProcesses`.
5431
+ */
5432
+ EventCategory["MetricsNodeProcessesSnapshot"] = "metrics.node-processes-snapshot";
5433
+ /**
5434
+ * Capability binding change event emitted by `addon-pipeline-orchestrator`
5435
+ * when a user changes which addon implements a cap on a node. Subscribed
5436
+ * by every kernel process to update its local `preferredProviderRegistry`
5437
+ * so future capability lookups respect the new binding.
5438
+ */
5439
+ /**
5440
+ * A capability binding was changed for a node — addon X now provides
5441
+ * capability `cap` on node `nodeId`. Lives under the generic
5442
+ * `capability.*` namespace because capability bindings are a kernel-
5443
+ * level concept used by many addons, not strictly pipeline-scoped.
5444
+ */
5445
+ EventCategory["CapabilityBindingChanged"] = "capability.binding-changed";
5446
+ EventCategory["ModelDownloadProgress"] = "model.download.progress";
5447
+ EventCategory["AgentRegistered"] = "agent.registered";
5448
+ EventCategory["AgentUnregistered"] = "agent.unregistered";
5449
+ EventCategory["AgentOnline"] = "agent.online";
5450
+ EventCategory["AgentOffline"] = "agent.offline";
5451
+ /** Forked worker process (e.g. hub/pipeline) connected to the broker. */
5452
+ EventCategory["WorkerOnline"] = "worker.online";
5453
+ /** Forked worker process disconnected from the broker. */
5454
+ EventCategory["WorkerOffline"] = "worker.offline";
5455
+ EventCategory["AgentTaskDispatched"] = "agent.task.dispatched";
5456
+ EventCategory["AgentTaskAssigned"] = "agent.task.assigned";
5457
+ EventCategory["AgentTrpcConnected"] = "agent.trpc.connected";
5458
+ EventCategory["AgentWsConnected"] = "agent.ws.connected";
5459
+ EventCategory["AgentWsDisconnected"] = "agent.ws.disconnected";
5460
+ EventCategory["AgentBackupActivated"] = "agent.backup.activated";
5461
+ EventCategory["OrchestrationSettingsUpdated"] = "orchestration.settings-updated";
5462
+ EventCategory["MotionAnalysis"] = "detection.motion-analysis";
5463
+ /** All raw motion zones from CCL before minArea filter — for UI debug overlay. */
5464
+ EventCategory["MotionZonesRaw"] = "detection.motion-zones-raw";
5465
+ /**
5466
+ * Per-camera motion phase transition (`watching ↔ active`) emitted
5467
+ * by the runner. Mirrors the `motion.onMotionChanged` cap event
5468
+ * surface — payload `MotionOnMotionChangedPayload` carries
5469
+ * `{deviceId, detected, timestamp, source, regions?}`. Subscribers
5470
+ * include addons that need to react to motion state without
5471
+ * polling the runtime-state mirror.
5472
+ */
5473
+ EventCategory["MotionOnMotionChanged"] = "motion.on-motion-changed";
5474
+ EventCategory["DetectionResult"] = "detection.result";
5475
+ EventCategory["DetectionRaw"] = "detection.raw";
5476
+ EventCategory["DetectionCameraNative"] = "detection.camera-native";
5477
+ /**
5478
+ * Canonical per-chunk live audio pipeline output. Payload is
5479
+ * `PipelineAudioInferenceResultPayload` carrying a full `AudioResult`
5480
+ * (level + detections + debug). Lives on the pipeline.* namespace
5481
+ * alongside `pipeline.inference-result` (video) for symmetry.
5482
+ */
5483
+ EventCategory["PipelineAudioInferenceResult"] = "pipeline.audio-inference-result";
5484
+ EventCategory["DetectionPhaseTransition"] = "detection.phase-transition";
5485
+ /**
5486
+ * A camera provider's RAW onboard motion push (D673): the `MotionOnMotionChanged`
5487
+ * data shape without `triggeredBy`, built by `providerMotionEvent`. Its one
5488
+ * consumer is the orchestrator's camera-sensors bridge, which re-emits the
5489
+ * camera's EFFECTIVE onboard motion as `MotionOnMotionChanged` (a pass-through
5490
+ * for a camera with no attached sensor). Providers must not emit
5491
+ * `MotionOnMotionChanged` themselves; `scripts/check-onboard-motion-emitters.ts`
5492
+ * keeps it so (camera-grid is a named, deferred exception).
5493
+ */
5494
+ EventCategory["ProviderMotion"] = "provider.motion";
5495
+ EventCategory["ProviderDetection"] = "provider.detection";
5496
+ EventCategory["EnrichmentEmbeddingStored"] = "enrichment.embedding.stored";
5497
+ EventCategory["EnrichmentSceneStateChanged"] = "enrichment.scene.state-changed";
5498
+ EventCategory["EnrichmentActivitySummary"] = "enrichment.activity.summary";
5499
+ /**
5500
+ * Unified track-lifecycle event: ONE category carrying `phase:
5501
+ * 'start' | 'update' | 'end'` with a rich, typed
5502
+ * `PipelineAnalyticsTrackLifecyclePayload`. Supersedes the thin
5503
+ * `PipelineAnalyticsTrackStarted` / `PipelineAnalyticsTrackEnded`
5504
+ * pair — a consumer subscribes once and branches on `phase`.
5505
+ */
5506
+ EventCategory["PipelineAnalyticsTrackLifecycle"] = "pipeline-analytics.track-lifecycle";
5507
+ /**
5508
+ * @deprecated Superseded by {@link PipelineAnalyticsTrackLifecycle}
5509
+ * (`phase:'start'`). Kept as a soft alias — no known subscribers.
5510
+ */
5511
+ EventCategory["PipelineAnalyticsTrackStarted"] = "pipeline-analytics.track-started";
5512
+ /**
5513
+ * @deprecated Superseded by {@link PipelineAnalyticsTrackLifecycle}
5514
+ * (`phase:'end'`). Kept as a soft alias — no known subscribers.
5515
+ */
5516
+ EventCategory["PipelineAnalyticsTrackEnded"] = "pipeline-analytics.track-ended";
5517
+ EventCategory["PipelineAnalyticsDetectionEvent"] = "pipeline-analytics.detection-event";
5518
+ EventCategory["PipelineAnalyticsFrameTracked"] = "pipeline-analytics.frame-tracked";
5519
+ /**
5520
+ * Fired by `addon-post-analysis` when a parked (stationary) object appears
5521
+ * (a track was promoted to a stationary-registry entry) or departs (the
5522
+ * object moved / was removed). Telemetry (D8): lossy, drives a UI refresh of
5523
+ * the dedicated "Stationary" section — never the live event feed. Payload:
5524
+ * `{ deviceId, entryId, className, phase:'appeared'|'departed', timestamp, observedAt, zones }`.
5525
+ */
5526
+ EventCategory["PipelineAnalyticsStationaryChanged"] = "pipeline-analytics.stationary-changed";
5527
+ /**
5528
+ * Fired by `addon-post-analysis` when a package-drop is confirmed inside
5529
+ * a package zone — a newly-appeared stationary object of a package class
5530
+ * that cleared the class / zone / dwell / size gates. Payload:
5531
+ * `PipelineAnalyticsPackageDeliveredPayload` carrying `{ deviceId,
5532
+ * entryId, className, zoneIds, keyFrameMediaId?, bbox, timestamp }`.
5533
+ * Telemetry (D8): the durable record is the `package-events` store row;
5534
+ * this bus topic drives notifier rules + live UI. See
5535
+ * docs/superpowers/specs/2026-07-17-package-zones-design.md §5.1.
5536
+ */
5537
+ EventCategory["PipelineAnalyticsPackageDelivered"] = "pipeline-analytics.package-delivered";
5538
+ /**
5539
+ * Fired by `addon-post-analysis` when a previously-delivered package
5540
+ * leaves its zone (the stationary entry departed — moved or swept).
5541
+ * Payload: `PipelineAnalyticsPackagePickedUpPayload` carrying
5542
+ * `{ deviceId, entryId, deliveredEventId, className, timestamp }`.
5543
+ * Telemetry (D8). See package-zones-design §5.2.
5544
+ */
5545
+ EventCategory["PipelineAnalyticsPackagePickedUp"] = "pipeline-analytics.package-picked-up";
5546
+ /**
5547
+ * Fired by `addon-post-analysis` whenever a gallery face row changes:
5548
+ * `kind:'buffered'` a new detected face was persisted, `'assigned'` /
5549
+ * `'unassigned'` its identity link changed, `'deleted'` the row was
5550
+ * removed. Telemetry semantics (D8): a lost event only delays a refresh,
5551
+ * never a correctness issue. Payload `PipelineAnalyticsFaceGalleryChangedPayload`.
5552
+ */
5553
+ EventCategory["PipelineAnalyticsFaceGalleryChanged"] = "pipeline-analytics.face-gallery-changed";
5554
+ /**
5555
+ * Fired by `addon-post-analysis` whenever a gallery plate row changes:
5556
+ * `kind:'buffered'` a new read was persisted (`PlateRecognizer.onTrackEnd`),
5557
+ * `'assigned'` / `'unassigned'` its vehicle link changed
5558
+ * (`PlateGalleryProvider`), `'deleted'` the row was removed. Telemetry
5559
+ * semantics (D8): a lost event only delays a refresh, never a correctness
5560
+ * issue. Payload `PipelineAnalyticsPlateGalleryChangedPayload`.
5561
+ */
5562
+ EventCategory["PipelineAnalyticsPlateGalleryChanged"] = "pipeline-analytics.plate-gallery-changed";
5563
+ EventCategory["CameraStreamsProfileSlotsChanged"] = "camera-streams.onProfileSlotsChanged";
5564
+ /**
5565
+ * Stream-broker health watchdog. Per-broker (deviceId/profile) emission.
5566
+ * `stream.offline` fires after STREAM_STALE_TIMEOUT_MS without an encoded
5567
+ * packet on an active broker. `stream.online` fires on first packet
5568
+ * after a stale gap (or on initial first packet). Payload includes
5569
+ * the assigned camStreamId as `profileKey`.
5570
+ */
5571
+ EventCategory["StreamOnline"] = "stream.online";
5572
+ EventCategory["StreamOffline"] = "stream.offline";
5573
+ EventCategory["NetworkTunnelStarted"] = "network.tunnel.started";
5574
+ EventCategory["NetworkTunnelStopped"] = "network.tunnel.stopped";
5575
+ /** Fired by the `local-network` cap when the host's interface set
5576
+ * changes (new IP from DHCP, VPN connect, docker bridge added). */
5577
+ EventCategory["LocalNetworkChanged"] = "network.local.changed";
5578
+ /**
5579
+ * Fired by a `mesh-network` provider (Tailscale, …) when its
5580
+ * mesh-reachable host changes (join / leave / MagicDNS or 100.x IP
5581
+ * change). Lets `local-network` fold the mesh endpoint into its
5582
+ * connection-endpoint list without a cross-package import. Payload
5583
+ * carries the preferred host (`host: ''` = no longer reachable),
5584
+ * the hub port, and the scheme. Telemetry-grade (D8): consumers
5585
+ * pull-reconcile from the provider's `getStatus` on reconnect. */
5586
+ EventCategory["MeshNetworkChanged"] = "network.mesh.changed";
5587
+ EventCategory["BackupCompleted"] = "backup.completed";
5588
+ /**
5589
+ * A whole backup RUN finished — every destination attempted, win or lose.
5590
+ *
5591
+ * `backup.completed` fires once per DESTINATION, which is the right grain for
5592
+ * a progress UI and the wrong one for a notification: an operator with three
5593
+ * destinations would be told three times. And a run where two of three
5594
+ * destinations succeeded is not a clean success — a single "backup
5595
+ * completed" that hid the failed one would be a lie, so the count of each is
5596
+ * carried here and the message says both.
5597
+ *
5598
+ * Emitted by the backup orchestrator only after the destination loop, so a
5599
+ * run that dies during the BUILD phase produces no completion at all.
5600
+ */
5601
+ EventCategory["BackupRunCompleted"] = "backup.run-completed";
5602
+ EventCategory["BackupRestored"] = "backup.restored";
5603
+ EventCategory["NotificationDispatched"] = "notification.dispatched";
5604
+ EventCategory["NotificationFailed"] = "notification.failed";
5605
+ /**
5606
+ * A notification rule that cannot work (D905): its stored row does not load,
5607
+ * or it still names a device / delivery target the prune CONFIRMED deleted.
5608
+ * One edge per rule per distinct set of reasons; never on an unanswered read.
5609
+ */
5610
+ EventCategory["NotificationRuleBroken"] = "notification.rule-broken";
5611
+ /** The clear of {@link NotificationRuleBroken}: the rule loads and names nothing deleted. */
5612
+ EventCategory["NotificationRuleRepaired"] = "notification.rule-repaired";
5613
+ /** A migration failed or was blocked; the Alert Center re-reads the ledger. */
5614
+ EventCategory["MigrationFailed"] = "migration.failed";
5615
+ /** A migration applied or a failure cleared; the Alert Center re-reads the ledger. */
5616
+ EventCategory["MigrationApplied"] = "migration.applied";
5617
+ EventCategory["DeviceUpdated"] = "device.updated";
5618
+ /**
5619
+ * Transport-level connectivity. Emitted by the device driver when
5620
+ * the underlying control socket actually connects / disconnects
5621
+ * (Baichuan TCP, ONVIF probe response, RTSP DESCRIBE, …) — NOT for
5622
+ * power-state transitions on a battery camera. For battery wake /
5623
+ * doze cycles see `DeviceAwake` / `DeviceSleeping`.
5624
+ */
5625
+ EventCategory["DeviceOnline"] = "device.online";
5626
+ /**
5627
+ * Stream-broker watchdog — emitted when no encoded packet has been
5628
+ * received for STREAM_STALE_TIMEOUT_MS on an active broker (rtsp or
5629
+ * push). Paired with DeviceOnline which fires on first packet after
5630
+ * a stale gap. Payload: DeviceStreamHealthPayload.
5631
+ */
5632
+ EventCategory["DeviceOffline"] = "device.offline";
5633
+ /**
5634
+ * Battery cam woke up — physical power-state transition reported
5635
+ * by the device firmware. Distinct from `DeviceOnline` so a UI panel
5636
+ * watching power state doesn't flap on every UDP socket reconnect.
5637
+ */
5638
+ EventCategory["DeviceAwake"] = "device.awake";
5639
+ /**
5640
+ * Battery cam went to sleep. See `DeviceAwake`.
5641
+ */
5642
+ EventCategory["DeviceSleeping"] = "device.sleeping";
5643
+ EventCategory["RetentionCleanup"] = "retention.cleanup";
5644
+ /**
5645
+ * Legacy bulk-update progress snapshot (payload `BulkUpdateState`). No longer
5646
+ * emitted — F3 removed the coordinator that produced it; "Update all" now runs
5647
+ * as one lifecycle engine job (`AddonsJobProgress`/`AddonsJobLog`). Retained
5648
+ * (with `BulkUpdateState`) only to avoid regenerating the event maps; removed
5649
+ * in F4 once live bulk progress is re-implemented over the engine events.
5650
+ */
5651
+ EventCategory["AddonsBulkUpdateProgress"] = "addons.bulk-update-progress";
5652
+ EventCategory["AddonsJobProgress"] = "addons.job-progress";
5653
+ EventCategory["AddonsJobLog"] = "addons.job-log";
5654
+ /**
5655
+ * A container's child visibility toggled (hidden/shown). Emitted by the
5656
+ * `accessories` cap when a child device is hidden or revealed.
5657
+ * Payload: `{ deviceId, childDeviceId, hidden }`.
5658
+ */
5659
+ EventCategory["AccessoriesChildVisibilityChanged"] = "accessories.onChildVisibilityChanged";
5660
+ /**
5661
+ * A container's child set changed (children added/removed/reordered).
5662
+ * Payload: `{ deviceId, childDeviceIds, hiddenChildIds }`.
5663
+ */
5664
+ EventCategory["AccessoriesChanged"] = "accessories.onAccessoriesChanged";
5665
+ /**
5666
+ * An action run started or ended. Emitted by the `actions` surface.
5667
+ * Payload: `ActionRunChanged` (`{ actionId, runId, isRunning, outcome?, lastRunAt, lastError }`).
5668
+ */
5669
+ EventCategory["ActionsRunChanged"] = "actions.onRunChanged";
5670
+ /**
5671
+ * Progress update from a running model conversion job.
5672
+ * Payload: `{ kind: 'model-convert', phase, sessionId?, pct?, detail? }`.
5673
+ * Emitted by `addon-model-studio` on the converting node.
5674
+ */
5675
+ EventCategory["ModelConvertProgress"] = "model-convert.progress";
5676
+ return EventCategory;
5677
+ }({});
5678
+ //#endregion
5679
+ //#region ../types/dist/device-type-D1fH5kaC.mjs
5680
+ /**
5681
+ import { errMsg } from '@camstack/types'
5682
+ * Extract a human-readable message from an unknown error value.
5683
+ * Replaces the ubiquitous `errMsg(err)` pattern.
5684
+ */
5685
+ function errMsg(err) {
5686
+ if (err instanceof Error) return err.message;
5687
+ if (typeof err === "string") return err;
5688
+ return String(err);
6062
5689
  }
5690
+ var DeviceType = /* @__PURE__ */ function(DeviceType) {
5691
+ DeviceType["Camera"] = "camera";
5692
+ DeviceType["Hub"] = "hub";
5693
+ DeviceType["Light"] = "light";
5694
+ DeviceType["Siren"] = "siren";
5695
+ DeviceType["Switch"] = "switch";
5696
+ DeviceType["Sensor"] = "sensor";
5697
+ DeviceType["Thermostat"] = "thermostat";
5698
+ /** Air-conditioner / heat-pump climate device (HVAC) — shares the
5699
+ * `climate-control` cap surface with `Thermostat` but renders a
5700
+ * dedicated AC-appropriate control UI (mode chips, fan speed,
5701
+ * independent vertical/horizontal swing). Sources: native Gree, and
5702
+ * reusable by other AC integrations. */
5703
+ DeviceType["Climate"] = "climate";
5704
+ DeviceType["Button"] = "button";
5705
+ /** Generic stateless event emitter — carries a device's EXACT declared
5706
+ * event vocabulary verbatim (no normalization). Installed with the
5707
+ * `event-emitter` cap. Sources: HA `event.*` entities (structured) and
5708
+ * HA bus events (e.g. `zha_event`, generic). */
5709
+ DeviceType["EventEmitter"] = "event-emitter";
5710
+ /** Firmware/software update entity — current vs available version,
5711
+ * updatable flag, update state, and an install action. Installed with
5712
+ * the `update` cap. Sources: Homematic firmware-update channels (and
5713
+ * reusable by other providers, e.g. HA `update.*` entities). */
5714
+ DeviceType["Update"] = "update";
5715
+ DeviceType["Generic"] = "generic";
5716
+ /** Generic notification delivery target (HA `notify.<service>`, future
5717
+ * Telegram / Discord / ntfy / SMTP, …). One device per delivery
5718
+ * endpoint; the `notifier` cap defines the send surface. */
5719
+ DeviceType["Notifier"] = "notifier";
5720
+ /** Pre-recorded action sequence with optional parameters
5721
+ * (HA `script.*`). Runnable via `script-runner` cap. */
5722
+ DeviceType["Script"] = "script";
5723
+ /** Automation rule (HA `automation.*`) — enable/disable + manual
5724
+ * trigger surface exposed via `automation-control` cap. */
5725
+ DeviceType["Automation"] = "automation";
5726
+ /** Door / smart lock device (HA `lock.*`). `lock-control` cap. */
5727
+ DeviceType["Lock"] = "lock";
5728
+ /** Window covering, blinds, garage door, valve, etc. (HA `cover.*`,
5729
+ * `valve.*`). `cover` cap with sub-roles for variant. */
5730
+ DeviceType["Cover"] = "cover";
5731
+ /** Pipe / water / gas valve with open/close/stop and optional
5732
+ * position (HA `valve.*`). `valve` cap — a cover-sibling actuator
5733
+ * modelled on the same open/closed lifecycle. */
5734
+ DeviceType["Valve"] = "valve";
5735
+ /** Humidifier / dehumidifier with on/off + target humidity + mode
5736
+ * (HA `humidifier.*`). `humidifier` cap — a climate-family actuator
5737
+ * modelled on the same target / mode lifecycle. */
5738
+ DeviceType["Humidifier"] = "humidifier";
5739
+ /** Water heater / boiler with target temperature + operation mode +
5740
+ * away mode (HA `water_heater.*`). `water-heater` cap — a
5741
+ * climate-family actuator. */
5742
+ DeviceType["WaterHeater"] = "water-heater";
5743
+ /** Ceiling / standing / exhaust fan (HA `fan.*`). `fan-control` cap. */
5744
+ DeviceType["Fan"] = "fan";
5745
+ /** Audio / video playback endpoint (HA `media_player.*`). Disjoint from
5746
+ * the camera surface — those use `Camera`. `media-player` cap. */
5747
+ DeviceType["MediaPlayer"] = "media-player";
5748
+ /** Security panel / alarm system (HA `alarm_control_panel.*`).
5749
+ * `alarm-panel` cap. */
5750
+ DeviceType["AlarmPanel"] = "alarm-panel";
5751
+ /** Generic user-settable input (HA `number` / `input_number` / `select`
5752
+ * / `input_select` / `text` / `input_text` / `input_datetime`).
5753
+ * Sub-type via `DeviceRole`: NumericControl / SelectControl /
5754
+ * TextControl / DateTimeControl. */
5755
+ DeviceType["Control"] = "control";
5756
+ /** Person / device-tracker presence (HA `person.*`, `device_tracker.*`).
5757
+ * `presence` cap. */
5758
+ DeviceType["Presence"] = "presence";
5759
+ /** Weather provider (HA `weather.*`). Tier-3, low MVP priority.
5760
+ * `weather` cap. */
5761
+ DeviceType["Weather"] = "weather";
5762
+ /** Robot vacuum (HA `vacuum.*`). Tier-3. `vacuum-control` cap. */
5763
+ DeviceType["Vacuum"] = "vacuum";
5764
+ /** Robotic lawn mower (HA `lawn_mower.*`). Tier-3.
5765
+ * `lawn-mower-control` cap. */
5766
+ DeviceType["LawnMower"] = "lawn-mower";
5767
+ /** Physical HA device group — parent container for entity-children
5768
+ * adopted from a single HA device entry. Not renderable as a
5769
+ * standalone device; exists only to anchor child entities. */
5770
+ DeviceType["Container"] = "container";
5771
+ /** Single still-image entity (HA `image.*`). Read-only display of an
5772
+ * `entity_picture` signed URL the browser loads directly. `image` cap. */
5773
+ DeviceType["Image"] = "image";
5774
+ /** Smart pet feeder — cloud-connected food dispenser with a bowl food
5775
+ * level, battery, desiccant life, feeding state and manual-feed /
5776
+ * call-pet / maintenance actions. Installed with the `pet-feeder` cap;
5777
+ * dual-hopper models (D4S/D4SH) expose per-hopper portions. Sources:
5778
+ * native PetKit (`nodepetkit` `FeederDevice`), reusable by other feeder
5779
+ * integrations sharing the same food/desiccant/hopper surface. */
5780
+ DeviceType["PetFeeder"] = "pet-feeder";
5781
+ /** Pet water fountain — tank level, water drunk, pump and its duty cycle.
5782
+ * Installed with the `pet-fountain` cap. Reaches CamStack as loose entities
5783
+ * of a container and is BUILT as a composed device (D967). */
5784
+ DeviceType["PetFountain"] = "pet-fountain";
5785
+ /** Built-in dishwasher — program lifecycle, door / remote-start gates,
5786
+ * salt and rinse-aid levels. Installed with the `dishwasher` cap.
5787
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
5788
+ * A plug-built dishwasher carries running-state and power instead. */
5789
+ DeviceType["Dishwasher"] = "dishwasher";
5790
+ /** Washing machine. A plug-built one is running while its power stays
5791
+ * above a threshold. No program cap — that surface is the dishwasher's. */
5792
+ DeviceType["Washer"] = "washer";
5793
+ /** A dumb plug-in stove or heater: a switch (and, with a meter, a power
5794
+ * reading) that only runs while the plug is on. Built from a smart plug;
5795
+ * the `timed-run` cap switches it off again after a chosen time. */
5796
+ DeviceType["Heater"] = "heater";
5797
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
5798
+ DeviceType["Dryer"] = "dryer";
5799
+ /** The physical bridge box of a vendor ecosystem (Velux KLF-200, and later
5800
+ * other radio-to-IP bridges). It carries the box's own facts (firmware,
5801
+ * address, connection state) and the actions the box really supports
5802
+ * (`reboot`, `device-admin-link`). Products are NOT its children — they are
5803
+ * adopted beside it — and its connection settings stay on the integration. */
5804
+ DeviceType["Gateway"] = "gateway";
5805
+ return DeviceType;
5806
+ }({});
5807
+ var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
5808
+ DeviceFeature["BatteryOperated"] = "battery-operated";
5809
+ DeviceFeature["Rebootable"] = "rebootable";
5810
+ /**
5811
+ * Device supports an on-demand re-sync of its derived spec with its
5812
+ * upstream source — drives the generic Re-sync button. The owning
5813
+ * provider implements the action via the `device-adoption.resync` cap.
5814
+ */
5815
+ DeviceFeature["Resyncable"] = "resyncable";
5816
+ DeviceFeature["NativeSnapshot"] = "native-snapshot";
5817
+ DeviceFeature["DoorbellButton"] = "doorbell-button";
5818
+ DeviceFeature["TwoWayAudio"] = "two-way-audio";
5819
+ DeviceFeature["PanTiltZoom"] = "pan-tilt-zoom";
5820
+ /**
5821
+ * Camera supports the on-firmware autotrack subsystem (subject-
5822
+ * following). Distinct from `PanTiltZoom` because not every PTZ
5823
+ * camera ships autotrack — the admin UI uses this flag to gate
5824
+ * the autotrack toggle / settings card without re-deriving from
5825
+ * the cap registry. Mirrors `ptz-autotrack` cap registration:
5826
+ * driver sets this feature when probe confirms the firmware
5827
+ * surface, and registers the cap in the same code path.
5828
+ */
5829
+ DeviceFeature["PtzAutotrack"] = "ptz-autotrack";
5830
+ /**
5831
+ * Accessory exposes a "trigger on motion" toggle — the parent camera's
5832
+ * motion detection automatically activates this device. Mirrors
5833
+ * `motion-trigger` cap registration: drivers set this feature in the
5834
+ * same code path that calls `ctx.registerNativeCap(motionTriggerCapability, ...)`.
5835
+ *
5836
+ * Used by admin UI (gate the in-hero `MotionTriggerToggle` against a
5837
+ * fast scalar without binding fetch), notifier rules, and `listAll`
5838
+ * filters that want "all devices with on-motion behaviour".
5839
+ */
5840
+ DeviceFeature["MotionTrigger"] = "motion-trigger";
5841
+ /** Light supports rgb-triplet color via `color` cap. */
5842
+ DeviceFeature["LightColorRgb"] = "light-color-rgb";
5843
+ /** Light supports HSV color via `color` cap. */
5844
+ DeviceFeature["LightColorHsv"] = "light-color-hsv";
5845
+ /** Light supports color-temperature (mired) via `color` cap. */
5846
+ DeviceFeature["LightColorMired"] = "light-color-mired";
5847
+ /** Thermostat supports a `heat_cool` dual setpoint (targetLow +
5848
+ * targetHigh). Gates the range slider UI. */
5849
+ DeviceFeature["ClimateDualSetpoint"] = "climate-dual-setpoint";
5850
+ /** Thermostat exposes target humidity and/or current humidity
5851
+ * readings. Gates the humidity controls. */
5852
+ DeviceFeature["ClimateHumidity"] = "climate-humidity";
5853
+ /** Thermostat exposes a fan-mode selector. */
5854
+ DeviceFeature["ClimateFanMode"] = "climate-fan-mode";
5855
+ /** Thermostat exposes preset modes (eco / away / sleep / vendor). */
5856
+ DeviceFeature["ClimatePreset"] = "climate-preset";
5857
+ /** Thermostat exposes a vertical louver swing toggle. Gates the
5858
+ * vertical-swing switch in the climate UI. Independent of horizontal. */
5859
+ DeviceFeature["ClimateSwingVertical"] = "climate-swing-vertical";
5860
+ /** Thermostat exposes a horizontal louver swing toggle. Gates the
5861
+ * horizontal-swing switch in the climate UI. Independent of vertical. */
5862
+ DeviceFeature["ClimateSwingHorizontal"] = "climate-swing-horizontal";
5863
+ /** Cover exposes intermediate position control (0..100). Gates the
5864
+ * position slider UI. */
5865
+ DeviceFeature["CoverPositionable"] = "cover-positionable";
5866
+ /** Cover exposes slat-tilt control. Gates the tilt slider UI. */
5867
+ DeviceFeature["CoverTilt"] = "cover-tilt";
5868
+ /** Cover has a real stop: `stop` halts the travel in place. Gates the stop
5869
+ * control in exporters; a provider whose `stop` is a no-op never declares it. */
5870
+ DeviceFeature["CoverStoppable"] = "cover-stoppable";
5871
+ /** Valve exposes intermediate position control (0..100). Gates the
5872
+ * position slider / drag surface UI. */
5873
+ DeviceFeature["ValvePositionable"] = "valve-positionable";
5874
+ /** Fan exposes a speed-percentage setter. Gates the speed slider UI. */
5875
+ DeviceFeature["FanSpeed"] = "fan-speed";
5876
+ /** Fan exposes a preset mode selector. */
5877
+ DeviceFeature["FanPreset"] = "fan-preset";
5878
+ /** Fan exposes blade direction (forward/reverse) — typical of
5879
+ * ceiling fans. */
5880
+ DeviceFeature["FanDirection"] = "fan-direction";
5881
+ /** Fan exposes an oscillation toggle. */
5882
+ DeviceFeature["FanOscillating"] = "fan-oscillating";
5883
+ /** Lock requires a PIN code on lock/unlock. Gates the code-entry
5884
+ * field on the UI lock-controls panel. */
5885
+ DeviceFeature["LockPinRequired"] = "lock-pin-required";
5886
+ /** Lock supports a latch-release ("open door") action distinct from
5887
+ * unlock. Mirrors HA `LockEntityFeature.OPEN` (bit 1) in
5888
+ * `supported_features`. Gates the Open Door button in the UI. */
5889
+ DeviceFeature["LockOpen"] = "lock-open";
5890
+ /** Media player exposes a seek-to-position surface. */
5891
+ DeviceFeature["MediaPlayerSeek"] = "media-player-seek";
5892
+ /** Media player exposes a volume-level setter. */
5893
+ DeviceFeature["MediaPlayerVolume"] = "media-player-volume";
5894
+ /** Media player exposes a mute toggle distinct from volume=0. */
5895
+ DeviceFeature["MediaPlayerMute"] = "media-player-mute";
5896
+ /** Media player exposes a shuffle toggle. */
5897
+ DeviceFeature["MediaPlayerShuffle"] = "media-player-shuffle";
5898
+ /** Media player exposes a repeat mode (off / all / one). */
5899
+ DeviceFeature["MediaPlayerRepeat"] = "media-player-repeat";
5900
+ /** Media player exposes a source / input selector. */
5901
+ DeviceFeature["MediaPlayerSelectSource"] = "media-player-select-source";
5902
+ /** Media player exposes a play-arbitrary-media surface (URL / id). */
5903
+ DeviceFeature["MediaPlayerPlayMedia"] = "media-player-play-media";
5904
+ /** Media player exposes next-track. */
5905
+ DeviceFeature["MediaPlayerNext"] = "media-player-next";
5906
+ /** Media player exposes previous-track. */
5907
+ DeviceFeature["MediaPlayerPrevious"] = "media-player-previous";
5908
+ /** Media player exposes stop distinct from pause. */
5909
+ DeviceFeature["MediaPlayerStop"] = "media-player-stop";
5910
+ /** Media player exposes volume up / down in steps (an infrared TV has no level). */
5911
+ DeviceFeature["MediaPlayerVolumeStep"] = "media-player-volume-step";
5912
+ /** Media player exposes channel up / down in steps. */
5913
+ DeviceFeature["MediaPlayerChannelStep"] = "media-player-channel-step";
5914
+ /** Media player exposes power on / off. */
5915
+ DeviceFeature["MediaPlayerPower"] = "media-player-power";
5916
+ /** Alarm panel requires a PIN code on arm/disarm. */
5917
+ DeviceFeature["AlarmPinRequired"] = "alarm-pin-required";
5918
+ /** Presence device carries GPS coordinates (lat/lng/accuracy) in
5919
+ * addition to a textual location. */
5920
+ DeviceFeature["PresenceGps"] = "presence-gps";
5921
+ /** Notifier accepts an inline / URL image attachment. */
5922
+ DeviceFeature["NotifierImage"] = "notifier-image";
5923
+ /** Notifier accepts a priority hint (high/normal/low). */
5924
+ DeviceFeature["NotifierPriority"] = "notifier-priority";
5925
+ /** Notifier accepts a free-form `data` payload for platform-specific
5926
+ * fields. */
5927
+ DeviceFeature["NotifierData"] = "notifier-data";
5928
+ /** Notifier supports interactive action buttons / callbacks. */
5929
+ DeviceFeature["NotifierActions"] = "notifier-actions";
5930
+ /** Notifier supports per-call recipient targeting (multi-user). */
5931
+ DeviceFeature["NotifierRecipients"] = "notifier-recipients";
5932
+ /** Script runner accepts a variables map on each run invocation. */
5933
+ DeviceFeature["ScriptVariables"] = "script-variables";
5934
+ /** Automation `trigger` accepts a skipCondition flag — fires the
5935
+ * automation's actions while bypassing its condition block. */
5936
+ DeviceFeature["AutomationSkipCondition"] = "automation-skip-condition";
5937
+ /** Robot vacuum exposes a live cleaning map (image child). Gates the
5938
+ * map tile in the vacuum UI. */
5939
+ DeviceFeature["VacuumHasMap"] = "vacuum-has-map";
5940
+ /** Robot vacuum exposes AI obstacle-detection toggles. Gates the AI
5941
+ * switches group. */
5942
+ DeviceFeature["VacuumHasAi"] = "vacuum-has-ai";
5943
+ /** Robot mower exposes a live mowing map (SVG image child). Gates the
5944
+ * mower map tile. */
5945
+ DeviceFeature["MowerHasMap"] = "mower-has-map";
5946
+ /** Robot mower exposes targeted mowing (all-area / zones / edges /
5947
+ * spots) — gates the Mow action + map selectors. */
5948
+ DeviceFeature["MowerHasTargetedMowing"] = "mower-has-targeted-mowing";
5949
+ return DeviceFeature;
5950
+ }({});
5951
+ /**
5952
+ * Semantic role a device plays within its parent. Populated by driver
5953
+ * addons when creating accessory devices (Reolink siren/floodlight/
5954
+ * PIR/chime/autotrack/doorbell, ONVIF relay outputs, …). Used by the
5955
+ * admin UI to pick icons, labels, and widgets — a `Switch` with
5956
+ * `role: Floodlight` renders as a bulb with a brightness slider,
5957
+ * whereas a `Switch` with `role: Siren` renders as a klaxon.
5958
+ *
5959
+ * Undefined for top-level devices (cameras, NVRs, hubs). Persisted in
5960
+ * sqlite as a nullable TEXT column — old rows keep working unchanged.
5961
+ */
5962
+ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
5963
+ DeviceRole["Siren"] = "siren";
5964
+ DeviceRole["Floodlight"] = "floodlight";
5965
+ DeviceRole["Spotlight"] = "spotlight";
5966
+ DeviceRole["PirSensor"] = "pir-sensor";
5967
+ DeviceRole["Chime"] = "chime";
5968
+ DeviceRole["Autotrack"] = "autotrack";
5969
+ DeviceRole["Nightvision"] = "nightvision";
5970
+ DeviceRole["PrivacyMask"] = "privacy-mask";
5971
+ DeviceRole["Doorbell"] = "doorbell";
5972
+ /** Interior blind / shade / roller blind. HA `device_class: blind|shade`. */
5973
+ DeviceRole["CoverBlind"] = "cover-blind";
5974
+ /** Exterior roller shutter. HA `device_class: shutter`; Homematic BROLL. */
5975
+ DeviceRole["CoverShutter"] = "cover-shutter";
5976
+ /** Exterior awning. HA `device_class: awning`. */
5977
+ DeviceRole["CoverAwning"] = "cover-awning";
5978
+ /** Curtain / drape. HA `device_class: curtain`. */
5979
+ DeviceRole["CoverCurtain"] = "cover-curtain";
5980
+ /** Garage door. HA `device_class: garage`. A security actuator: exports gate it. */
5981
+ DeviceRole["CoverGarage"] = "cover-garage";
5982
+ /** Gate. HA `device_class: gate`. A security actuator: exports gate it. */
5983
+ DeviceRole["CoverGate"] = "cover-gate";
5984
+ /** Door. HA `device_class: door`. A security actuator: exports gate it. */
5985
+ DeviceRole["CoverDoor"] = "cover-door";
5986
+ /** Window. HA `device_class: window`. */
5987
+ DeviceRole["CoverWindow"] = "cover-window";
5988
+ /** Virtual HA toggle (input_boolean.*) — distinguishable from a
5989
+ * real Switch device for UI rendering / export adapters. */
5990
+ DeviceRole["BinaryHelper"] = "binary-helper";
5991
+ /** A smart plug: a `switch` whose source says it is an outlet (HA
5992
+ * `device_class: outlet`). The Alexa export shows it as a SMARTPLUG. */
5993
+ DeviceRole["Outlet"] = "outlet";
5994
+ /** Generic motion / occupancy / moving event source. Distinct from
5995
+ * the camera accessory PirSensor role: that one is a camera child;
5996
+ * this is a standalone HA / 3rd-party motion sensor. */
5997
+ DeviceRole["MotionSensor"] = "motion-sensor";
5998
+ DeviceRole["ContactSensor"] = "contact-sensor";
5999
+ DeviceRole["LeakSensor"] = "leak-sensor";
6000
+ DeviceRole["SmokeSensor"] = "smoke-sensor";
6001
+ DeviceRole["COSensor"] = "co-sensor";
6002
+ DeviceRole["GasSensor"] = "gas-sensor";
6003
+ DeviceRole["TamperSensor"] = "tamper-sensor";
6004
+ DeviceRole["VibrationSensor"] = "vibration-sensor";
6005
+ DeviceRole["ConnectivitySensor"] = "connectivity-sensor";
6006
+ DeviceRole["SoundSensor"] = "sound-sensor";
6007
+ /** Fallback for `binary_sensor` without a known `device_class`. */
6008
+ DeviceRole["BinarySensor"] = "binary-sensor";
6009
+ DeviceRole["TemperatureSensor"] = "temperature-sensor";
6010
+ DeviceRole["HumiditySensor"] = "humidity-sensor";
6011
+ DeviceRole["AmbientLightSensor"] = "ambient-light-sensor";
6012
+ DeviceRole["PressureSensor"] = "pressure-sensor";
6013
+ /** Wind speed or direction (weather-station `wind-sensor` cap). */
6014
+ DeviceRole["WindSensor"] = "wind-sensor";
6015
+ /** Rain accumulation or rate (weather-station `rain-sensor` cap). */
6016
+ DeviceRole["RainSensor"] = "rain-sensor";
6017
+ /** UV index (weather-station `uv-sensor` cap). */
6018
+ DeviceRole["UvSensor"] = "uv-sensor";
6019
+ /** Solar irradiance W/m² (weather-station `solar-radiation-sensor` cap).
6020
+ * Distinct from AmbientLightSensor (lux). */
6021
+ DeviceRole["SolarRadiationSensor"] = "solar-radiation-sensor";
6022
+ /** Soil moisture % (garden/weather `soil-moisture-sensor` cap). */
6023
+ DeviceRole["SoilMoistureSensor"] = "soil-moisture-sensor";
6024
+ DeviceRole["PowerSensor"] = "power-sensor";
6025
+ DeviceRole["EnergySensor"] = "energy-sensor";
6026
+ DeviceRole["VoltageSensor"] = "voltage-sensor";
6027
+ DeviceRole["CurrentSensor"] = "current-sensor";
6028
+ DeviceRole["AirQualitySensor"] = "air-quality-sensor";
6029
+ /** Battery level (numeric % via `sensor` OR low-bool via
6030
+ * `binary_sensor` — the cap distinguishes via the value type). */
6031
+ DeviceRole["BatterySensor"] = "battery-sensor";
6032
+ /** Fallback for `sensor` numeric without a known `device_class`. */
6033
+ DeviceRole["NumericSensor"] = "numeric-sensor";
6034
+ /** String / enum state (HA `sensor` with `state_class: enum` or
6035
+ * `attributes.options`). */
6036
+ DeviceRole["EnumSensor"] = "enum-sensor";
6037
+ /** Date / timestamp state (HA `sensor` with `device_class: timestamp`
6038
+ * or `date`). The slice carries the raw ISO string verbatim (hosted on
6039
+ * the `enum-sensor` cap); the UI renders it locale-formatted. */
6040
+ DeviceRole["DateTimeSensor"] = "datetime-sensor";
6041
+ /** Last-resort fallback when nothing else matches. */
6042
+ DeviceRole["GenericSensor"] = "generic-sensor";
6043
+ DeviceRole["NumericControl"] = "numeric-control";
6044
+ DeviceRole["SelectControl"] = "select-control";
6045
+ DeviceRole["TextControl"] = "text-control";
6046
+ DeviceRole["DateTimeControl"] = "datetime-control";
6047
+ /** Mobile push notifier (HA `notify.mobile_app_*`) — supports
6048
+ * rich features (image, priority, channel routing). */
6049
+ DeviceRole["MobilePushNotifier"] = "mobile-push-notifier";
6050
+ /** Chat / messaging service (HA `notify.telegram_*`,
6051
+ * `notify.discord_*`, etc.). */
6052
+ DeviceRole["MessagingNotifier"] = "messaging-notifier";
6053
+ /** Email-based delivery (HA `notify.smtp`, etc.). */
6054
+ DeviceRole["EmailNotifier"] = "email-notifier";
6055
+ /** Fallback when the notifier service name doesn't match a known
6056
+ * pattern. */
6057
+ DeviceRole["GenericNotifier"] = "generic-notifier";
6058
+ /** Emits a pulse train (infrared or RF) on request — the `ir-transmitter`
6059
+ * cap. A `DeviceType.Generic` device. */
6060
+ DeviceRole["IrTransmitter"] = "ir-transmitter";
6061
+ return DeviceRole;
6062
+ }({});
6063
6063
  //#endregion
6064
- //#region ../types/dist/sleep-AEnTN3b4.mjs
6064
+ //#region ../types/dist/sleep-CQP-REJp.mjs
6065
6065
  /**
6066
6066
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6067
6067
  * window to float samples (D455).
@@ -6425,6 +6425,59 @@ Object.fromEntries([
6425
6425
  function isValuelessField(field) {
6426
6426
  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" || field.type === "account-sign-in" || field.type === "secret-presence";
6427
6427
  }
6428
+ /**
6429
+ * Base collector: flatten a hydrated `ConfigUISchemaWithValues` into
6430
+ * `key → { value, default }`, recursing through `group` and `sub-tabs`
6431
+ * layout fields. Keeping `value` and `default` SEPARATE (rather than
6432
+ * pre-resolving) lets callers both read the effective value AND detect
6433
+ * overrides (`value` differs from `default`). Structural over `unknown`;
6434
+ * malformed input yields an empty map, never throws.
6435
+ */
6436
+ function collectHydratedFieldEntries(schema) {
6437
+ const out = /* @__PURE__ */ new Map();
6438
+ if (typeof schema !== "object" || schema === null) return out;
6439
+ const sections = schema.sections;
6440
+ if (!Array.isArray(sections)) return out;
6441
+ for (const section of sections) if (typeof section === "object" && section !== null) collectFieldEntriesInto(section.fields, out);
6442
+ return out;
6443
+ }
6444
+ /** Recursive field walker for {@link collectHydratedFieldEntries}. */
6445
+ function collectFieldEntriesInto(fields, into) {
6446
+ if (!Array.isArray(fields)) return;
6447
+ for (const field of fields) {
6448
+ if (typeof field !== "object" || field === null) continue;
6449
+ const type = field.type;
6450
+ if (type === "group") {
6451
+ collectFieldEntriesInto(field.fields, into);
6452
+ continue;
6453
+ }
6454
+ if (type === "sub-tabs") {
6455
+ const tabs = field.tabs;
6456
+ if (Array.isArray(tabs)) {
6457
+ for (const tab of tabs) if (typeof tab === "object" && tab !== null) collectFieldEntriesInto(tab.fields, into);
6458
+ }
6459
+ continue;
6460
+ }
6461
+ const key = field.key;
6462
+ if (typeof key !== "string" || key.length === 0) continue;
6463
+ if (!("value" in field)) continue;
6464
+ into.set(key, {
6465
+ value: field.value,
6466
+ default: field.default
6467
+ });
6468
+ }
6469
+ }
6470
+ /**
6471
+ * Flatten a hydrated settings payload into `key → EFFECTIVE value`
6472
+ * (`value ?? default`). The read-side inverse of `hydrateSchema` for the
6473
+ * common "just give me the resolved values" case. Built on
6474
+ * {@link collectHydratedFieldEntries}.
6475
+ */
6476
+ function collectHydratedFieldValues(schema) {
6477
+ const out = /* @__PURE__ */ new Map();
6478
+ for (const [key, entry] of collectHydratedFieldEntries(schema)) out.set(key, entry.value ?? entry.default);
6479
+ return out;
6480
+ }
6428
6481
  function hydrateSchema(schema, values, options = {}) {
6429
6482
  return {
6430
6483
  ...schema.tabs ? { tabs: [...schema.tabs] } : {},
@@ -6762,7 +6815,7 @@ function switchRecordsOverMigrationApi(api) {
6762
6815
  };
6763
6816
  } };
6764
6817
  }
6765
- function integrationRowsApiOver(api) {
6818
+ function integrationRowsApiOver$1(api) {
6766
6819
  return {
6767
6820
  list: async () => (await api.integrations.list({})).integrations,
6768
6821
  readLegacyIntegrationRows: (input) => api.legacyRows.read(input),
@@ -7664,6 +7717,21 @@ var SCOPED_SETTINGS_SCOPES = [
7664
7717
  "device-defaults",
7665
7718
  "node"
7666
7719
  ];
7720
+ /** The one refusal of a key a scope did not declare. */
7721
+ function assertDeclaredKeys(keys, declared) {
7722
+ const undeclared = keys.filter((key) => !declared.has(key));
7723
+ if (undeclared.length > 0) throw new Error(`scoped settings: [${undeclared.join(", ")}] not declared for this scope (declared: [${[...declared].join(", ")}])`);
7724
+ }
7725
+ /** Build the handle over an addon's scoped read, write and unset. */
7726
+ function createScopedSettings(deps) {
7727
+ return {
7728
+ async get() {
7729
+ return deps.schema.parse(await deps.read());
7730
+ },
7731
+ patch: (patch) => deps.write(Object.fromEntries(Object.entries(patch))),
7732
+ unset: (keys) => deps.unset(keys)
7733
+ };
7734
+ }
7667
7735
  /** A write to a secret path could not be stored in the keychain, so it was refused. */
7668
7736
  var KeychainWriteRefusedError = class extends Error {
7669
7737
  fieldPath;
@@ -8239,7 +8307,16 @@ function createHandle(raw, opts) {
8239
8307
  return blob;
8240
8308
  }
8241
8309
  },
8242
- writeAddonStore: writeAddon
8310
+ writeAddonStore: writeAddon,
8311
+ removeClearedKeys: (keys) => serialize("addon", async () => {
8312
+ const stored = await readStoredAddonBlob(raw);
8313
+ index.seen("addon", stored);
8314
+ const removable = keys.filter((key) => collectSecretMarkerRefs(stored[key]).length === 0);
8315
+ if (removable.length === 0) return;
8316
+ const patch = Object.fromEntries(removable.map((key) => [key, void 0]));
8317
+ await raw.writeAddonStore(patch);
8318
+ index.wrote("addon", patch);
8319
+ })
8243
8320
  };
8244
8321
  }
8245
8322
  /** The context the addon sees, plus the private raw view and write outcome. */
@@ -8570,7 +8647,7 @@ var BaseAddon = class {
8570
8647
  seq: 1e3,
8571
8648
  initialBody: spec.initialBody
8572
8649
  })], (a) => registrySwitchPortsOver({
8573
- api: integrationRowsApiOver(a),
8650
+ api: integrationRowsApiOver$1(a),
8574
8651
  records: switchRecordsOverMigrationApi(a),
8575
8652
  kind: spec.kind
8576
8653
  })).bind(api);
@@ -8784,6 +8861,151 @@ var BaseAddon = class {
8784
8861
  }
8785
8862
  }
8786
8863
  /**
8864
+ * Hook: values layered over the stored ones when the FORM of one scope is served
8865
+ * ({@link getScopedSettings}) — a preview that derives dependent options without writing.
8866
+ * Never applied to {@link ScopedSettings.get} or to a write. `null` ⇒ no overlay.
8867
+ */
8868
+ async scopedSettingsOverlay(_scope, _cap, _nodeId) {
8869
+ return null;
8870
+ }
8871
+ /**
8872
+ * Hook: a write of one scope changed stored values (not called when the store reports no change,
8873
+ * e.g. the write-back of a stored secret). `keys` are the bare field keys of the write, node keys included.
8874
+ */
8875
+ async onScopedSettingsChanged(_scope, _cap, _keys) {}
8876
+ /** Typed handle over the CLUSTER settings `cap` declares (`clusterSettingsSchema`). */
8877
+ clusterSettings(cap, schema) {
8878
+ return this.scopedHandle("cluster", cap, schema);
8879
+ }
8880
+ /** Typed handle over the fleet defaults of the per-device settings `cap` declares. */
8881
+ deviceDefaults(cap, schema) {
8882
+ return this.scopedHandle("device-defaults", cap, schema);
8883
+ }
8884
+ /**
8885
+ * Typed handle over the settings `cap` owns PER NODE. `nodeId` omitted ⇒ the local node. A node
8886
+ * with no row yields the schema defaults — never a bare-key fallback.
8887
+ */
8888
+ nodeSettings(cap, schema, nodeId) {
8889
+ return this.scopedHandle("node", cap, schema, nodeId);
8890
+ }
8891
+ /**
8892
+ * {@link state} for a value that is kept per node: stored under `<key>@<nodeId>`, and the
8893
+ * settings store carries `nodeId` in the row data (D986 §1), so a forgotten node's rows go in one
8894
+ * central delete. `nodeId` omitted ⇒ the local node.
8895
+ */
8896
+ nodeState(key, schema, fallback, nodeId) {
8897
+ this._declaredStateKeys.add(key);
8898
+ return createDurableState({
8899
+ key: nodeScopedKey(key, nodeId ?? this._ctx?.kernel?.localNodeId ?? ""),
8900
+ schema,
8901
+ fallback,
8902
+ read: async () => this.resolveStoredSecrets(await this.readAddonStoreWithRetry()),
8903
+ write: async (patch) => {
8904
+ await this._ctx?.settings?.writeAddonStore(patch);
8905
+ },
8906
+ onParseError: (k, e) => this._ctx?.logger?.warn?.(`durable-state: stored "${k}" failed validation — using fallback`, { meta: {
8907
+ addonId: this._ctx?.id,
8908
+ key: k,
8909
+ error: String(e)
8910
+ } })
8911
+ });
8912
+ }
8913
+ /** The loaded schema `cap` declares for `scope`, or `null`. */
8914
+ async declaredScopedSchema(scope, cap) {
8915
+ await this.loadScopedSchemas(cap);
8916
+ return this.loadedScopedSchemas(cap).find((entry) => entry.scope === scope)?.schema ?? null;
8917
+ }
8918
+ async requireScopedSchema(scope, cap) {
8919
+ const schema = await this.declaredScopedSchema(scope, cap);
8920
+ if (schema === null) throw new Error(`${this.constructor.name}: cap "${cap}" declares no ${scope} settings`);
8921
+ return schema;
8922
+ }
8923
+ scopedKeys(schema) {
8924
+ return new Set(schema.sections.flatMap((section) => collectValueFieldKeys(section.fields)));
8925
+ }
8926
+ /** `store` narrowed to the keys `schema` declares. */
8927
+ narrowToScope(store, schema) {
8928
+ const keys = this.scopedKeys(schema);
8929
+ return Object.fromEntries(Object.entries(store).filter(([key]) => keys.has(key)));
8930
+ }
8931
+ scopedHandle(scope, cap, schema, nodeId) {
8932
+ return createScopedSettings({
8933
+ schema,
8934
+ read: async () => {
8935
+ const declared = await this.requireScopedSchema(scope, cap);
8936
+ const stored = this.narrowToScope(await this.resolveGlobalStore(nodeId, cap), declared);
8937
+ const effective = collectHydratedFieldValues(hydrateSchema(declared, stored));
8938
+ return {
8939
+ ...Object.fromEntries([...effective].filter(([, value]) => value !== void 0)),
8940
+ ...stored
8941
+ };
8942
+ },
8943
+ write: (patch) => this.writeScoped(scope, cap, patch, nodeId),
8944
+ unset: (keys) => this.unsetScoped(scope, cap, keys, nodeId)
8945
+ });
8946
+ }
8947
+ /** The one write path of the scoped API: declared keys only, then the global patch. */
8948
+ async writeScoped(scope, cap, patch, nodeId) {
8949
+ assertDeclaredKeys(Object.keys(patch), this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8950
+ if (await this.applyGlobalPatch(patch, scope === "node" ? nodeId : void 0, cap)) await this.onScopedSettingsChanged(scope, cap, Object.keys(patch));
8951
+ }
8952
+ /**
8953
+ * Remove declared keys of one scope. A secret goes through {@link clearSecrets} — its keychain
8954
+ * value is released exactly as there — and the emptied row is then removed; every other key is
8955
+ * removed by an `undefined` patch (which a secret transform would read as "keep").
8956
+ */
8957
+ async unsetScoped(scope, cap, keys, nodeId) {
8958
+ assertDeclaredKeys(keys, this.scopedKeys(await this.requireScopedSchema(scope, cap)));
8959
+ const target = scope === "node" ? nodeId : void 0;
8960
+ const tree = this.addonSecretTree();
8961
+ const secretKeys = keys.filter((key) => tree.has(key));
8962
+ const plainKeys = keys.filter((key) => !tree.has(key));
8963
+ let changed = false;
8964
+ if (plainKeys.length > 0) {
8965
+ const patch = Object.fromEntries(plainKeys.map((key) => [key, void 0]));
8966
+ changed = await this.applyGlobalPatch(patch, target, cap) || changed;
8967
+ }
8968
+ if (secretKeys.length > 0) {
8969
+ await this.clearSecretKeys(secretKeys, target, cap);
8970
+ const node = normalizeNodeId(target ?? this._ctx?.kernel?.localNodeId);
8971
+ const rows = secretKeys.map((key) => scope === "node" ? nodeScopedKey(key, node) : key);
8972
+ if (this._secretSettings !== null) await this._secretSettings.removeClearedKeys(rows);
8973
+ else await this._ctx?.settings?.writeAddonStore(Object.fromEntries(rows.map((row) => [row, void 0])));
8974
+ changed = true;
8975
+ }
8976
+ if (changed) await this.onScopedSettingsChanged(scope, cap, [...keys]);
8977
+ }
8978
+ /**
8979
+ * The hydrated form of one scope of one capability's settings (any
8980
+ * {@link scopedSettingsOverlay} applied), or `null` when the capability declares nothing there.
8981
+ */
8982
+ async getScopedSettings(scope, cap, nodeId) {
8983
+ const schema = await this.declaredScopedSchema(scope, cap);
8984
+ if (schema === null) return null;
8985
+ const stored = this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema);
8986
+ const overlay = await this.scopedSettingsOverlay(scope, cap, nodeId);
8987
+ return hydrateSchema(schema, overlay ? await this.overlayKeepingMarkers(stored, overlay) : stored, { inlineSecretsAsViews: true });
8988
+ }
8989
+ /** The OPERATOR's write of declared keys of one scope (D787 secret semantics). */
8990
+ async updateScopedSettings(scope, cap, patch, nodeId) {
8991
+ this._operatorWriteDepth++;
8992
+ try {
8993
+ await this.writeScoped(scope, cap, patch, nodeId);
8994
+ } finally {
8995
+ this._operatorWriteDepth--;
8996
+ }
8997
+ }
8998
+ /**
8999
+ * The effective values (`stored ?? default`) of one scope, a secret as its `set` view — never
9000
+ * its value. `null` when the capability declares nothing for the scope.
9001
+ */
9002
+ async readScopedValues(scope, cap, nodeId) {
9003
+ const schema = await this.declaredScopedSchema(scope, cap);
9004
+ if (schema === null) return null;
9005
+ const form = hydrateSchema(schema, this.narrowToScope(await this.readRawProjectedStore(nodeId, cap), schema), { inlineSecretsAsViews: true });
9006
+ return Object.fromEntries([...collectHydratedFieldValues(form)].filter(([, value]) => value !== void 0));
9007
+ }
9008
+ /**
8787
9009
  * What the create wizard and the integration edit page render for one instance: the addon's
8788
9010
  * per-instance schema when it declares one, else its global schema (what they always showed).
8789
9011
  */
@@ -8935,6 +9157,9 @@ var BaseAddon = class {
8935
9157
  * a keychain (an older kernel) it writes `''`, as such code always did.
8936
9158
  */
8937
9159
  async clearSecrets(keys, nodeId) {
9160
+ await this.clearSecretKeys(keys, nodeId);
9161
+ }
9162
+ async clearSecretKeys(keys, nodeId, cap) {
8938
9163
  const tree = this.addonSecretTree();
8939
9164
  const keychain = this._ctx?.secrets !== void 0;
8940
9165
  const stored = keychain ? await this.readAddonStoreWithRetry() : {};
@@ -8947,25 +9172,27 @@ var BaseAddon = class {
8947
9172
  const keychainStored = tree.has(key) || collectSecretMarkerRefs(stored[key]).length > 0;
8948
9173
  patch[key] = keychain && keychainStored ? SECRET_CLEAR : "";
8949
9174
  }
8950
- await this.applyGlobalPatch(patch, nodeId);
9175
+ await this.applyGlobalPatch(patch, nodeId, cap);
8951
9176
  }
8952
- async applyGlobalPatch(barePatch, nodeId) {
8953
- await this.loadScopedSchemas();
8954
- const keys = this.perNodeKeys();
9177
+ /** Resolves `true` when the write changed stored values. */
9178
+ async applyGlobalPatch(barePatch, nodeId, cap) {
9179
+ await this.loadScopedSchemas(cap);
9180
+ const keys = this.perNodeKeys(cap);
8955
9181
  const localNode = normalizeNodeId(this._ctx?.kernel?.localNodeId);
8956
9182
  const target = normalizeNodeId(nodeId ?? this._ctx?.kernel?.localNodeId);
8957
9183
  const scoped = keys.size > 0 ? scopePatch(barePatch, keys, target) : barePatch;
8958
9184
  const handle = this._secretSettings;
8959
9185
  if (handle !== null) {
8960
9186
  const { changed } = await handle.writeAddonStore(scoped);
8961
- if (!changed) return;
9187
+ if (!changed) return false;
8962
9188
  } else await this._ctx?.settings?.writeAddonStore(scoped);
8963
- if (target !== localNode) return;
9189
+ if (target !== localNode) return true;
8964
9190
  await this.resolveConfig();
8965
9191
  await this.onConfigChanged();
8966
9192
  this.emitLifecycle(EventCategory.AddonUpdated, { level: "global" });
8967
9193
  const legacy = this.globalSettingsSchema();
8968
9194
  this.maybeAutoRestart(Object.keys(barePatch), [...legacy === null ? [] : [legacy], ...this.loadedScopedSchemas().map((entry) => entry.schema)]);
9195
+ return true;
8969
9196
  }
8970
9197
  /**
8971
9198
  * The set of field keys the global settings schema declares `perNode: true`
@@ -16017,20 +16244,6 @@ async function flattenNestedRecords(ports, spec) {
16017
16244
  summary: { converted: rows.size }
16018
16245
  };
16019
16246
  }
16020
- /** The ports over one kind's records handle. */
16021
- function flattenPortsOver(handle) {
16022
- return {
16023
- listNested: async (legacyKey) => (await handle.query({ limit: 1e3 })).records.filter((r) => legacyKey in r.data).map((r) => r.id),
16024
- readResolved: async (id) => {
16025
- const row = await handle.getResolved(id);
16026
- if (row === null) throw new Error("the record is gone");
16027
- return row.data;
16028
- },
16029
- patch: async (id, patch) => {
16030
- await handle.patch(id, patch);
16031
- }
16032
- };
16033
- }
16034
16247
  /**
16035
16248
  * The addon-side ports of `moveIntegrationRowsToRecords` (planning#416). An addon runs the move
16036
16249
  * from its own process, so the leftover rows are reached through the migration-only door
@@ -16078,25 +16291,6 @@ function integrationRowsPortsOverApi(deps) {
16078
16291
  }
16079
16292
  };
16080
16293
  }
16081
- function defaultIntegrationApiOver(api) {
16082
- return {
16083
- list: () => api.integrations.list.query(),
16084
- readLegacyIntegrationRows: (input) => api.migrations.readLegacyIntegrationRows.query(input),
16085
- clearLegacyIntegrationRows: (input) => api.migrations.clearLegacyIntegrationRows.mutate(input),
16086
- listDevices: (addonId) => api.deviceManager.listAll.query({
16087
- addonId,
16088
- projection: "slim"
16089
- }),
16090
- setDeviceIntegrationId: async (deviceId, integrationId) => {
16091
- await api.deviceManager.setIntegrationId.mutate({
16092
- deviceId,
16093
- integrationId
16094
- });
16095
- },
16096
- readDeviceHasConfig: async (deviceId) => Object.keys((await api.deviceManager.getDevice.query({ deviceId }))?.config ?? {}).length > 0,
16097
- readDeviceIntegrationId: async (deviceId) => (await api.deviceManager.getDevice.query({ deviceId }))?.integrationId ?? null
16098
- };
16099
- }
16100
16294
  /**
16101
16295
  * The trainer contract — what the hub hands the cloud trainer and what it
16102
16296
  * gets back (cloud training, D805+).
@@ -45494,6 +45688,8 @@ var SettingsSchemaWithValuesSchema = object({
45494
45688
  });
45495
45689
  /** Patch object — keys are field names, values are the new field values. */
45496
45690
  var SettingsPatchSchema = record(string(), unknown());
45691
+ /** The scope of a scoped-settings call (D986): where the capability declared the values. */
45692
+ var ScopedSettingsScopeSchema = _enum(SCOPED_SETTINGS_SCOPES);
45497
45693
  /** Standard success response for update operations. */
45498
45694
  var SettingsUpdateResultSchema = object({ success: literal(true) });
45499
45695
  method(object({
@@ -45509,6 +45705,25 @@ method(object({
45509
45705
  kind: "mutation",
45510
45706
  auth: "admin"
45511
45707
  }), method(object({
45708
+ addonId: string(),
45709
+ cap: string(),
45710
+ scope: ScopedSettingsScopeSchema,
45711
+ nodeId: string().optional()
45712
+ }), SettingsSchemaWithValuesSchema.nullable()), method(object({
45713
+ addonId: string(),
45714
+ cap: string(),
45715
+ scope: ScopedSettingsScopeSchema,
45716
+ nodeId: string().optional(),
45717
+ patch: SettingsPatchSchema
45718
+ }), SettingsUpdateResultSchema, {
45719
+ kind: "mutation",
45720
+ auth: "admin"
45721
+ }), method(object({
45722
+ addonId: string(),
45723
+ cap: string(),
45724
+ scope: ScopedSettingsScopeSchema,
45725
+ nodeId: string().optional()
45726
+ }), record(string(), unknown()).nullable()), method(object({
45512
45727
  addonId: string(),
45513
45728
  nodeId: string().optional()
45514
45729
  }), SettingsSchemaWithValuesSchema.nullable()), method(object({
@@ -78500,7 +78715,7 @@ var TerminalInstanceInfoSchema = object({
78500
78715
  /**
78501
78716
  * False while the node the instance is bound to is not in the cluster
78502
78717
  * topology (forgotten, renamed, re-paired). The instance is kept; the
78503
- * operator re-points it with `updateInstance({ targetNodeId })`.
78718
+ * operator re-points it on the Terminal page.
78504
78719
  */
78505
78720
  nodeInCluster: boolean(),
78506
78721
  profileId: string(),
@@ -78538,41 +78753,7 @@ var TerminalOutputBatchSchema = object({
78538
78753
  snapshot: string().optional(),
78539
78754
  events: array(TerminalOutputEventSchema).readonly()
78540
78755
  });
78541
- method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalInstanceInfoSchema).readonly(), { auth: "admin" }), method(object({
78542
- targetNodeId: string().min(1),
78543
- profileId: string().min(1),
78544
- name: string().trim().min(1).max(160).optional(),
78545
- executable: string().max(1024).optional(),
78546
- args: array(string().max(2048)).max(64).optional(),
78547
- cwd: string().max(1024).optional(),
78548
- environment: array(string().max(4096)).max(64).optional(),
78549
- profileSettings: ProfileSettingsBagSchema.optional()
78550
- }), TerminalInstanceInfoSchema, {
78551
- kind: "mutation",
78552
- auth: "admin"
78553
- }), method(object({
78554
- instanceId: string().min(1),
78555
- /** Re-point the instance to another node; the profile must exist there. */
78556
- targetNodeId: string().min(1).max(256).optional(),
78557
- name: string().trim().min(1).max(160).optional(),
78558
- executable: string().max(1024).optional(),
78559
- args: array(string().max(2048)).max(64).optional(),
78560
- cwd: string().max(1024).optional(),
78561
- environment: array(string().max(4096)).max(64).optional(),
78562
- profileSettings: ProfileSettingsBagSchema.optional()
78563
- }), TerminalInstanceInfoSchema, {
78564
- kind: "mutation",
78565
- auth: "admin"
78566
- }), method(object({ instanceId: string().min(1) }), _void(), {
78567
- kind: "mutation",
78568
- auth: "admin"
78569
- }), method(object({
78570
- instanceId: string().min(1),
78571
- enabled: boolean()
78572
- }), TerminalInstanceInfoSchema, {
78573
- kind: "mutation",
78574
- auth: "admin"
78575
- }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
78756
+ method(_void(), array(TerminalProfileInfoSchema).readonly(), { auth: "admin" }), method(_void(), array(TerminalLegacyCameraSchema).readonly(), { auth: "admin" }), method(object({
78576
78757
  stableId: string().min(1),
78577
78758
  name: string().trim().min(1).max(160).optional()
78578
78759
  }), TerminalInstanceInfoSchema, {
@@ -83392,6 +83573,20 @@ Object.freeze({
83392
83573
  access: "view",
83393
83574
  role: "admin"
83394
83575
  },
83576
+ "addonSettings.getScopedSettings": {
83577
+ capName: "addon-settings",
83578
+ capScope: "system",
83579
+ addonId: null,
83580
+ access: "view",
83581
+ role: "admin"
83582
+ },
83583
+ "addonSettings.readScopedValues": {
83584
+ capName: "addon-settings",
83585
+ capScope: "system",
83586
+ addonId: null,
83587
+ access: "view",
83588
+ role: "admin"
83589
+ },
83395
83590
  "addonSettings.unsetStoredSettingsKeys": {
83396
83591
  capName: "addon-settings",
83397
83592
  capScope: "system",
@@ -83413,6 +83608,13 @@ Object.freeze({
83413
83608
  access: "create",
83414
83609
  role: "admin"
83415
83610
  },
83611
+ "addonSettings.updateScopedSettings": {
83612
+ capName: "addon-settings",
83613
+ capScope: "system",
83614
+ addonId: null,
83615
+ access: "create",
83616
+ role: "admin"
83617
+ },
83416
83618
  "addonWidgets.listWidgets": {
83417
83619
  capName: "addon-widgets",
83418
83620
  capScope: "system",
@@ -92303,27 +92505,6 @@ Object.freeze({
92303
92505
  access: "create",
92304
92506
  role: "admin"
92305
92507
  },
92306
- "terminalSession.createInstance": {
92307
- capName: "terminal-session",
92308
- capScope: "system",
92309
- addonId: null,
92310
- access: "create",
92311
- role: "admin"
92312
- },
92313
- "terminalSession.deleteInstance": {
92314
- capName: "terminal-session",
92315
- capScope: "system",
92316
- addonId: null,
92317
- access: "delete",
92318
- role: "admin"
92319
- },
92320
- "terminalSession.listInstances": {
92321
- capName: "terminal-session",
92322
- capScope: "system",
92323
- addonId: null,
92324
- access: "view",
92325
- role: "admin"
92326
- },
92327
92508
  "terminalSession.listLegacyCameras": {
92328
92509
  capName: "terminal-session",
92329
92510
  capScope: "system",
@@ -92366,20 +92547,6 @@ Object.freeze({
92366
92547
  access: "create",
92367
92548
  role: "admin"
92368
92549
  },
92369
- "terminalSession.setInstanceEnabled": {
92370
- capName: "terminal-session",
92371
- capScope: "system",
92372
- addonId: null,
92373
- access: "create",
92374
- role: "admin"
92375
- },
92376
- "terminalSession.updateInstance": {
92377
- capName: "terminal-session",
92378
- capScope: "system",
92379
- addonId: null,
92380
- access: "create",
92381
- role: "admin"
92382
- },
92383
92550
  "terminalSession.writeInput": {
92384
92551
  capName: "terminal-session",
92385
92552
  capScope: "system",
@@ -98746,7 +98913,6 @@ function buildLinkedInstances(records, spec) {
98746
98913
  });
98747
98914
  return instances;
98748
98915
  }
98749
- var BROKER_ROWS_MAX = 1e3;
98750
98916
  var STRAY_REGISTRY_ID = /^int_\d+$/;
98751
98917
  async function moveBrokerLinksToConnections(ports) {
98752
98918
  const brokers = await ports.listBrokers();
@@ -98778,36 +98944,6 @@ async function moveBrokerLinksToConnections(ports) {
98778
98944
  }
98779
98945
  };
98780
98946
  }
98781
- /** The ports over the two record kinds of the addon. */
98782
- function brokerLinkPortsOver(deps) {
98783
- return {
98784
- warn: deps.warn,
98785
- listBrokers: async () => {
98786
- return (await deps.brokers.query({ limit: BROKER_ROWS_MAX })).records.map((row) => {
98787
- const name = row.data["name"];
98788
- const link = row.data["integrationId"];
98789
- return {
98790
- id: row.id,
98791
- name: typeof name === "string" && name.length > 0 ? name : row.id,
98792
- integrationId: typeof link === "string" ? link : void 0
98793
- };
98794
- });
98795
- },
98796
- readConnection: async (id) => {
98797
- const row = await deps.connections.get(id);
98798
- return row === null ? null : { brokerId: row.data.brokerId };
98799
- },
98800
- putConnection: async (id, body) => {
98801
- await deps.connections.put({
98802
- id,
98803
- data: body
98804
- }, { ifRev: 0 });
98805
- },
98806
- clearBrokerLink: async (brokerId) => {
98807
- await deps.brokers.patch(brokerId, { integrationId: null });
98808
- }
98809
- };
98810
- }
98811
98947
  //#endregion
98812
98948
  //#region src/speak/constants.ts
98813
98949
  /** Echo speech limits and the worker restart bound (same window as CrashSupervisor). */
@@ -144132,6 +144268,7 @@ var EchoSessionRegistry = class {
144132
144268
  #resolve = null;
144133
144269
  #dnd = /* @__PURE__ */ new Map();
144134
144270
  #volumes = /* @__PURE__ */ new Map();
144271
+ #groupPlayers = /* @__PURE__ */ new Map();
144135
144272
  setResolver(fn) {
144136
144273
  this.#resolve = fn;
144137
144274
  }
@@ -144170,10 +144307,35 @@ var EchoSessionRegistry = class {
144170
144307
  });
144171
144308
  return pending;
144172
144309
  }
144310
+ /**
144311
+ * The player state of a speaker group, shared by its members within one poll cycle.
144312
+ * A failed read is not kept for the next member.
144313
+ */
144314
+ groupPlayer(brokerId, group, now, deviceId) {
144315
+ const key = `${brokerId}\u0000${group.id}`;
144316
+ const shared = this.#groupPlayers.get(key);
144317
+ if (shared !== void 0 && now - shared.at < 25e3) return shared.pending;
144318
+ const session = this.get(brokerId);
144319
+ if (session === null) return Promise.reject(/* @__PURE__ */ new Error("no Echo session for this account"));
144320
+ const pending = session.playerState(group.id, group.deviceType, deviceId);
144321
+ this.#groupPlayers.set(key, {
144322
+ at: now,
144323
+ pending
144324
+ });
144325
+ pending.catch(() => {
144326
+ if (this.#groupPlayers.get(key)?.pending === pending) this.#groupPlayers.delete(key);
144327
+ });
144328
+ return pending;
144329
+ }
144173
144330
  /** Forget the shared volume list: a list fetched before a set must not answer a poll after it. */
144174
144331
  invalidateVolumes(brokerId) {
144175
144332
  this.#volumes.delete(brokerId);
144176
144333
  }
144334
+ /** Forget the shared group players: a transport command or a push makes a cached group answer stale. */
144335
+ invalidateGroupPlayers(brokerId) {
144336
+ const prefix = `${brokerId}\u0000`;
144337
+ for (const key of [...this.#groupPlayers.keys()]) if (key.startsWith(prefix)) this.#groupPlayers.delete(key);
144338
+ }
144177
144339
  /** Forget the shared Do Not Disturb list, for the same reason. */
144178
144340
  invalidateDnd(brokerId) {
144179
144341
  this.#dnd.delete(brokerId);
@@ -144189,6 +144351,7 @@ var EchoSessionRegistry = class {
144189
144351
  }
144190
144352
  event(brokerId, serial) {
144191
144353
  this.invalidateVolumes(brokerId);
144354
+ this.invalidateGroupPlayers(brokerId);
144192
144355
  this.invalidateDnd(brokerId);
144193
144356
  for (const listener of [...this.#pushListeners.get(`${brokerId}\u0000${serial}`) ?? []]) listener();
144194
144357
  }
@@ -144207,6 +144370,7 @@ var EchoSessionRegistry = class {
144207
144370
  this.#resolve = null;
144208
144371
  this.#dnd.clear();
144209
144372
  this.#volumes.clear();
144373
+ this.#groupPlayers.clear();
144210
144374
  }
144211
144375
  };
144212
144376
  function subscribe(map, key, listener) {
@@ -144249,6 +144413,11 @@ function mapEchoPlaybackState(value) {
144249
144413
  default: return "idle";
144250
144414
  }
144251
144415
  }
144416
+ /** Whether a player answer carries any playback: an active state, a text or artwork. */
144417
+ function hasPlayback(info) {
144418
+ if (mapEchoPlaybackState(info.state) !== "idle") return true;
144419
+ return nonEmpty(info.infoText?.title) !== null || nonEmpty(info.mainArt?.url) !== null;
144420
+ }
144252
144421
  function percent(value) {
144253
144422
  if (typeof value !== "number" || !Number.isFinite(value)) return null;
144254
144423
  return Math.round(Math.max(0, Math.min(100, value)));
@@ -144338,7 +144507,9 @@ var EchoDeviceSchema = object({
144338
144507
  dnd: boolean(),
144339
144508
  deviceType: string(),
144340
144509
  /** The room the account files the device under, when Amazon names one. */
144341
- room: string().optional()
144510
+ room: string().optional(),
144511
+ /** Serials of the speaker groups this device belongs to (Amazon's `parentClusters`); absent when none. */
144512
+ parentClusters: array(string()).optional()
144342
144513
  });
144343
144514
  var LoginStatusSchema = _enum(["ok", "failed"]);
144344
144515
  var SessionStatusSchema = object({
@@ -144567,6 +144738,8 @@ var AlexaEchoDevice = class extends BaseDevice {
144567
144738
  unsubscribers = [];
144568
144739
  polling = false;
144569
144740
  failureLogged = false;
144741
+ /** The name of the speaker group whose playback this Echo shows; null when it plays on its own. */
144742
+ viaGroup = null;
144570
144743
  volumeFailureLogged = false;
144571
144744
  health = "ok";
144572
144745
  /** When this device last wrote its volume / Do Not Disturb optimistically; a read that began earlier must not overwrite it. */
@@ -144694,9 +144867,11 @@ var AlexaEchoDevice = class extends BaseDevice {
144694
144867
  }
144695
144868
  const deviceType = this.amazonDeviceType !== "" ? this.amazonDeviceType : listed?.deviceType ?? "";
144696
144869
  try {
144697
- const fromPlayer = mapPlayerInfoToStatus(await session.playerState(this.serial, deviceType, this.id), {
144870
+ const own = await session.playerState(this.serial, deviceType, this.id);
144871
+ const now = Date.now();
144872
+ const fromPlayer = mapPlayerInfoToStatus(await this.withGroupPlayback(own, listed, session), {
144698
144873
  online: true,
144699
- now: Date.now()
144874
+ now
144700
144875
  });
144701
144876
  const read = await this.completeVolume(fromPlayer);
144702
144877
  const kept = this.keepNewerVolume(read, readStartedAt);
@@ -144711,6 +144886,49 @@ var AlexaEchoDevice = class extends BaseDevice {
144711
144886
  await this.readDoNotDisturb(readStartedAt);
144712
144887
  }
144713
144888
  /**
144889
+ * A member of a playing speaker group is answered empty by its own player read while
144890
+ * the group plays. When the member's answer carries no playback, serve the first
144891
+ * parent group that is playing or paused (its track and artwork; the volume stays the
144892
+ * member's own). A failed group read leaves the member's own answer.
144893
+ */
144894
+ async withGroupPlayback(own, listed, session) {
144895
+ const parents = listed?.parentClusters ?? [];
144896
+ if (hasPlayback(own) || parents.length === 0) {
144897
+ this.noteViaGroup(null);
144898
+ return own;
144899
+ }
144900
+ for (const parentId of parents) {
144901
+ const group = session.devices.find((device) => device.id === parentId);
144902
+ if (group === void 0 || !group.online) continue;
144903
+ try {
144904
+ const groupInfo = await alexaEchoSessions.groupPlayer(this.brokerId, group, Date.now(), this.id);
144905
+ if (groupInfo.state === "PLAYING" || groupInfo.state === "PAUSED" || groupInfo.state === "BUFFERING") {
144906
+ this.noteViaGroup(group.name);
144907
+ return {
144908
+ ...groupInfo,
144909
+ volume: own.volume
144910
+ };
144911
+ }
144912
+ } catch (err) {
144913
+ this.ctx.logger.debug("alexa echo: the group player could not be read", {
144914
+ tags: { deviceId: this.id },
144915
+ meta: {
144916
+ group: group.name,
144917
+ error: err instanceof Error ? err.message : String(err)
144918
+ }
144919
+ });
144920
+ }
144921
+ }
144922
+ this.noteViaGroup(null);
144923
+ return own;
144924
+ }
144925
+ /** Log each transition into and out of "playing via a group" once. */
144926
+ noteViaGroup(name) {
144927
+ if (name === this.viaGroup) return;
144928
+ this.viaGroup = name;
144929
+ this.ctx.logger.info(name === null ? "alexa echo: no longer playing via a group" : `alexa echo: playing via group ${name}`, { tags: { deviceId: this.id } });
144930
+ }
144931
+ /**
144714
144932
  * An idle Echo's player read carries no volume. Ask the account's volume list
144715
144933
  * (shared by every Echo of the account in this cycle); a failed read leaves null.
144716
144934
  */
@@ -144782,6 +145000,7 @@ var AlexaEchoDevice = class extends BaseDevice {
144782
145000
  }
144783
145001
  async command(command) {
144784
145002
  await this.session().npCommand(this.serial, command, this.id);
145003
+ alexaEchoSessions.invalidateGroupPlayers(this.brokerId);
144785
145004
  this.rereadSoon();
144786
145005
  }
144787
145006
  /**
@@ -145681,7 +145900,7 @@ function echoTargetStoreOver(records, options = {}) {
145681
145900
  /** The `echoTargets` setting as the legacy source: a row the migration left there is still served. */
145682
145901
  function legacyEchoTargetsOver(setting) {
145683
145902
  return {
145684
- list: async () => readEchoTargets({ echoTargets: setting.readRows() }).map((row) => ({
145903
+ list: async () => readEchoTargets({ echoTargets: await setting.readRows() }).map((row) => ({
145685
145904
  id: row.id,
145686
145905
  name: row.name,
145687
145906
  kind: alexaTargetKind().kind,
@@ -145693,11 +145912,12 @@ function legacyEchoTargetsOver(setting) {
145693
145912
  }
145694
145913
  })),
145695
145914
  delete: async (id) => {
145696
- const rest = setting.readRows().filter((row) => {
145915
+ const rows = await setting.readRows();
145916
+ const rest = rows.filter((row) => {
145697
145917
  const parsed = alexaEchoTargetSchema.safeParse(row);
145698
145918
  return !(parsed.success && parsed.data.id === id);
145699
145919
  });
145700
- if (rest.length !== setting.readRows().length) await setting.writeRows(rest);
145920
+ if (rest.length !== rows.length) await setting.writeRows(rest);
145701
145921
  }
145702
145922
  };
145703
145923
  }
@@ -145714,8 +145934,9 @@ function echoRowOf(target) {
145714
145934
  }
145715
145935
  /** The migration's ports over the `notify-alexa-echo` record kind. */
145716
145936
  function echoRecordPortsOver(records, logger) {
145717
- const handle = echoTargetStoreOver(records).handlesByKind().get(alexaTargetKind().kind);
145718
- if (handle === void 0) throw new Error("import-alexa: the Echo target record kind is not declared");
145937
+ const [kind] = Object.values(alexaNotifyRecordKinds);
145938
+ if (kind === void 0) throw new Error("import-alexa: the Echo target record kind is not declared");
145939
+ const handle = records.kind(kind);
145719
145940
  return {
145720
145941
  readEchoRecord: async (id) => (await handle.getResolved(id))?.data ?? null,
145721
145942
  writeEchoRecord: async (id, body) => {
@@ -146800,17 +147021,42 @@ async function flattenBrokerRecords(ports) {
146800
147021
  //#endregion
146801
147022
  //#region src/migrations/broker-links-to-connections.ts
146802
147023
  var BROKER_LINKS_TO_CONNECTIONS_ID = "import-alexa/2026-11-05-broker-links-to-connections";
147024
+ var BROKER_ROWS_MAX = 1e3;
146803
147025
  /** The broker records and the integration (`connections`) records of this addon. */
146804
147026
  function alexaBrokerLinkPorts(records, warn) {
146805
- return brokerLinkPortsOver({
146806
- brokers: records.kind({
146807
- name: alexaBrokersEntryKind.name,
146808
- declaration: alexaBrokersEntryKind.declaration,
146809
- schema: record(string(), unknown())
146810
- }),
146811
- connections: records.kind(alexaConnectionsEntryKind),
146812
- warn
147027
+ const brokers = records.kind({
147028
+ name: alexaBrokersEntryKind.name,
147029
+ declaration: alexaBrokersEntryKind.declaration,
147030
+ schema: record(string(), unknown())
146813
147031
  });
147032
+ const connections = records.kind(alexaConnectionsEntryKind);
147033
+ return {
147034
+ warn,
147035
+ listBrokers: async () => {
147036
+ return (await brokers.query({ limit: BROKER_ROWS_MAX })).records.map((row) => {
147037
+ const name = row.data["name"];
147038
+ const link = row.data["integrationId"];
147039
+ return {
147040
+ id: row.id,
147041
+ name: typeof name === "string" && name.length > 0 ? name : row.id,
147042
+ integrationId: typeof link === "string" ? link : void 0
147043
+ };
147044
+ });
147045
+ },
147046
+ readConnection: async (id) => {
147047
+ const row = await connections.get(id);
147048
+ return row === null ? null : { brokerId: row.data.brokerId };
147049
+ },
147050
+ putConnection: async (id, body) => {
147051
+ await connections.put({
147052
+ id,
147053
+ data: body
147054
+ }, { ifRev: 0 });
147055
+ },
147056
+ clearBrokerLink: async (brokerId) => {
147057
+ await brokers.patch(brokerId, { integrationId: null });
147058
+ }
147059
+ };
146814
147060
  }
146815
147061
  //#endregion
146816
147062
  //#region src/migrations/strip-echo-target-volume-and-mode.ts
@@ -146826,7 +147072,7 @@ function strippedRow(row) {
146826
147072
  return Object.fromEntries(Object.entries(row).filter(([key]) => !RETIRED_KEYS.includes(key)));
146827
147073
  }
146828
147074
  async function stripEchoTargetVolumeAndMode(ports) {
146829
- const rows = ports.readRows();
147075
+ const rows = await ports.readRows();
146830
147076
  const stripped = rows.filter(carriesRetiredKey).length;
146831
147077
  if (stripped === 0) return {
146832
147078
  outcome: "already-done",
@@ -146859,7 +147105,7 @@ async function stripEchoTargetVolumeAndMode(ports) {
146859
147105
  */
146860
147106
  var ECHO_TARGETS_TO_RECORDS_ID = "import-alexa/2026-11-06-echo-targets-to-records";
146861
147107
  async function moveEchoTargetsToRecords(ports) {
146862
- const rows = ports.readRows();
147108
+ const rows = await ports.readRows();
146863
147109
  if (rows.length === 0) return {
146864
147110
  outcome: "already-done",
146865
147111
  summary: { converted: 0 }
@@ -147010,6 +147256,118 @@ var BROKER_RECORD_MIGRATION_IDS = [
147010
147256
  BROKER_LINKS_TO_CONNECTIONS_ID
147011
147257
  ];
147012
147258
  //#endregion
147259
+ //#region src/migrations/records-door.ts
147260
+ function positionalHandleOver(handle) {
147261
+ return {
147262
+ get: (id) => handle.get({ id }),
147263
+ getResolved: (id) => handle.getResolved({ id }),
147264
+ put: (input, opts) => handle.put({
147265
+ ...input,
147266
+ ...opts?.ifRev === void 0 ? {} : { ifRev: opts.ifRev }
147267
+ }),
147268
+ patch: (id, partial) => handle.patch({
147269
+ id,
147270
+ partial
147271
+ }),
147272
+ delete: (id) => handle.delete({ id }),
147273
+ query: ({ limit }) => handle.query({ limit })
147274
+ };
147275
+ }
147276
+ /** The records door of the migration API, in the shape the migrations call. */
147277
+ function positionalRecordsOver(api) {
147278
+ return { kind: (definition) => positionalHandleOver(api.records.kind(definition)) };
147279
+ }
147280
+ //#endregion
147281
+ //#region src/migrations/ports.ts
147282
+ var NESTED_MAX = 1e3;
147283
+ /** The registry list and the migration-only rows door, over the migration API. */
147284
+ function integrationRowsApiOver(api) {
147285
+ return {
147286
+ list: async () => (await api.integrations.list({})).integrations,
147287
+ readLegacyIntegrationRows: (input) => api.legacyRows.read(input),
147288
+ clearLegacyIntegrationRows: (input) => api.legacyRows.clear(input)
147289
+ };
147290
+ }
147291
+ /** The keychain handle of the migration API: resolve and delete, owner-scoped by the transport. */
147292
+ function secretsOver(api) {
147293
+ return {
147294
+ resolve: async (refs) => {
147295
+ const { values } = await api.secrets.resolve({ refs: [...refs] });
147296
+ const out = {};
147297
+ for (const ref of refs) {
147298
+ const value = values[ref];
147299
+ out[ref] = value === null || value === void 0 ? { kind: "missing" } : {
147300
+ kind: "value",
147301
+ value,
147302
+ version: 0
147303
+ };
147304
+ }
147305
+ return out;
147306
+ },
147307
+ delete: async (refs) => (await api.secrets.delete({ refs: [...refs] })).deleted
147308
+ };
147309
+ }
147310
+ /** The Echo targets kept in the addon's `echoTargets` setting, read and written as stored. */
147311
+ function echoSettingPortsOver(api) {
147312
+ return {
147313
+ readRows: async () => {
147314
+ const { store } = await api.settings.read({});
147315
+ const rows = store["echoTargets"];
147316
+ return Array.isArray(rows) ? rows : [];
147317
+ },
147318
+ writeRows: async (rows) => {
147319
+ await api.settings.write({ patch: { echoTargets: [...rows] } });
147320
+ }
147321
+ };
147322
+ }
147323
+ /** The broker rows' flatten ports over the records door. */
147324
+ function flattenPortsOverDoor(records) {
147325
+ const handle = records.kind({
147326
+ name: alexaBrokersEntryKind.name,
147327
+ declaration: alexaBrokersEntryKind.declaration,
147328
+ schema: record(string(), unknown())
147329
+ });
147330
+ return {
147331
+ listNested: async (legacyKey) => (await handle.query({ limit: NESTED_MAX })).records.filter((row) => legacyKey in row.data).map((row) => row.id),
147332
+ readResolved: async (id) => {
147333
+ const row = await handle.getResolved(id);
147334
+ if (row === null) throw new Error("the record is gone");
147335
+ return row.data;
147336
+ },
147337
+ patch: async (id, patch) => {
147338
+ await handle.patch(id, patch);
147339
+ }
147340
+ };
147341
+ }
147342
+ /**
147343
+ * The catalogue's ports over {@link MigrationApiV1}, and nothing else (planning#447): the addon's
147344
+ * own settings store (the Echo rows), its keychain handle, the legacy integration rows door and its
147345
+ * broker, connection and Echo target records.
147346
+ */
147347
+ function alexaPortsOver(api) {
147348
+ const records = positionalRecordsOver(api);
147349
+ const warn = (message) => {
147350
+ api.log.warn({ message });
147351
+ };
147352
+ return {
147353
+ ...echoSettingPortsOver(api),
147354
+ ...flattenPortsOverDoor(records),
147355
+ ...alexaBrokerLinkPorts(records, warn),
147356
+ ...echoRecordPortsOver(records, { warn: (message, extras) => {
147357
+ api.log.warn({
147358
+ message,
147359
+ ...extras?.meta === void 0 ? {} : { meta: extras.meta }
147360
+ });
147361
+ } }),
147362
+ ...alexaIntegrationRowsPorts({
147363
+ api: integrationRowsApiOver(api),
147364
+ records,
147365
+ secrets: secretsOver(api),
147366
+ warn
147367
+ })
147368
+ };
147369
+ }
147370
+ //#endregion
147013
147371
  //#region src/speak/icon-routes.ts
147014
147372
  var ALEXA_ICON_SVG = "<svg role=\"img\" viewBox=\"0 0 24 24\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"#00CAFF\"><title>Amazon Alexa</title><path d=\"M12 0C5.37 0 0 5.37 0 12C0 18.09 4.53 23.11 10.4 23.9V21.5A1.59 1.59 0 0 0 9.32 19.97A8.41 8.41 0 0 1 3.6 11.8A8.37 8.37 0 0 1 12.09 3.6A8.4 8.4 0 0 1 20.4 12.31L20.39 12.38A8.68 8.68 0 0 1 20.36 12.76C20.36 12.83 20.35 12.9 20.34 12.96C20.34 13.04 20.33 13.12 20.32 13.19L20.3 13.29C19.27 20.07 10.45 23.87 10.4 23.9C10.92 23.97 11.46 24 12 24C18.63 24 24 18.63 24 12S18.63 0 12 0Z\"/></svg>";
147015
147373
  /** One day; the mark is immutable per addon version. */
@@ -147129,7 +147487,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
147129
147487
  infoOf: (id) => this.registry?.get(id) ?? null,
147130
147488
  onChanged: (id) => this.syncBrokerRuntime(id)
147131
147489
  });
147132
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: BROKER_RECORD_MIGRATION_IDS });
147490
+ await this.runMigrations(ADDON_MIGRATIONS, (api) => alexaPortsOver(api), { only: BROKER_RECORD_MIGRATION_IDS });
147133
147491
  this.activeBrokers = await this.loadPersistedBrokers();
147134
147492
  await this.adoptSessionFiles();
147135
147493
  this.registry = new AlexaBrokerRegistry(this.ctx.logger, {
@@ -147170,7 +147528,7 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
147170
147528
  deviceCode: this.deviceCodeLogin,
147171
147529
  commitCookie: (input) => this.commitCapturedCookie(input)
147172
147530
  }), ...speakIconRoutes()];
147173
- await this.runMigrations(defineAddonMigrations(ADDON_MIGRATIONS, this.migrationPorts(records)), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => !BROKER_RECORD_MIGRATION_IDS.includes(id)) });
147531
+ await this.runMigrations(ADDON_MIGRATIONS, (api) => alexaPortsOver(api), { only: ADDON_MIGRATIONS.map((m) => m.meta.id).filter((id) => !BROKER_RECORD_MIGRATION_IDS.includes(id)) });
147174
147532
  const echoStore = echoTargetStoreOver(records, {
147175
147533
  logger: this.ctx.logger,
147176
147534
  legacy: legacyEchoTargetsOver(this.echoSettingPorts())
@@ -147221,29 +147579,15 @@ var AlexaImportAddon = class extends BaseDeviceProvider {
147221
147579
  }
147222
147580
  ] };
147223
147581
  }
147224
- /** The `echoTargets` setting, read and written as the migration and the legacy store see it. */
147582
+ /**
147583
+ * The `echoTargets` setting the legacy Echo store still serves: the addon's own durable state
147584
+ * under the key it was always stored at, so a row an older release left there is still read.
147585
+ */
147225
147586
  echoSettingPorts() {
147587
+ const rows = this.state("echoTargets", array(unknown()), []);
147226
147588
  return {
147227
- readRows: () => Array.isArray(this.config.echoTargets) ? this.config.echoTargets : [],
147228
- writeRows: async (rows) => {
147229
- await this.updateGlobalSettings({ echoTargets: [...readEchoTargets({ echoTargets: rows })] });
147230
- }
147231
- };
147232
- }
147233
- migrationPorts(records) {
147234
- const secrets = this.ctx.secrets;
147235
- if (secrets === void 0) throw new Error("import-alexa: ctx.secrets is required");
147236
- return {
147237
- ...this.echoSettingPorts(),
147238
- ...flattenPortsOver(records.kind(alexaBrokersEntryKind)),
147239
- ...alexaBrokerLinkPorts(records, (message) => this.ctx.logger.warn(message)),
147240
- ...echoRecordPortsOver(records, this.ctx.logger),
147241
- ...alexaIntegrationRowsPorts({
147242
- api: defaultIntegrationApiOver(this.ctx.api),
147243
- records,
147244
- secrets,
147245
- warn: (message) => this.ctx.logger.warn(message)
147246
- })
147589
+ readRows: () => rows.get(),
147590
+ writeRows: (next) => rows.set([...readEchoTargets({ echoTargets: next })])
147247
147591
  };
147248
147592
  }
147249
147593
  /** One provider over the two record kinds: the account (broker) and the integrations using it. */