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