@camstack/system 1.2.253 → 1.2.255
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/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.js +1 -1
- package/dist/builtins/addon-pages-aggregator/addon-pages-aggregator.addon.mjs +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.js +1 -1
- package/dist/builtins/addon-widgets-aggregator/addon-widgets-aggregator.addon.mjs +1 -1
- package/dist/builtins/alerts/alerts.addon.js +1 -1
- package/dist/builtins/alerts/alerts.addon.mjs +1 -1
- package/dist/builtins/autotrack/autotrack-cameras.d.ts +76 -0
- package/dist/builtins/autotrack/autotrack-config.d.ts +89 -0
- package/dist/builtins/autotrack/autotrack-decision.d.ts +99 -0
- package/dist/builtins/autotrack/autotrack-loop.d.ts +75 -0
- package/dist/builtins/autotrack/autotrack.addon.d.ts +71 -0
- package/dist/builtins/autotrack/frame-error.d.ts +50 -0
- package/dist/builtins/autotrack/frame-measurements.d.ts +97 -0
- package/dist/builtins/autotrack/index.d.ts +2 -0
- package/dist/builtins/autotrack/index.js +981 -0
- package/dist/builtins/autotrack/index.mjs +975 -0
- package/dist/builtins/autotrack/ptz-mirror.d.ts +25 -0
- package/dist/builtins/autotrack/target-selection.d.ts +60 -0
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.js +1 -1
- package/dist/builtins/backup-orchestrator/backup-orchestrator.addon.mjs +1 -1
- package/dist/builtins/camera-grid/addon.d.ts +118 -0
- package/dist/builtins/camera-grid/flv-join.d.ts +66 -0
- package/dist/builtins/camera-grid/grid-camera-declarations.d.ts +59 -0
- package/dist/builtins/camera-grid/grid-camera-device.d.ts +32 -0
- package/dist/builtins/camera-grid/grid-child.d.ts +9 -0
- package/dist/builtins/camera-grid/grid-device-settings.d.ts +11 -0
- package/dist/builtins/camera-grid/grid-filter-graph.d.ts +53 -0
- package/dist/builtins/camera-grid/grid-frame-sample.d.ts +27 -0
- package/dist/builtins/camera-grid/grid-instances.d.ts +49 -0
- package/dist/builtins/camera-grid/grid-last-frame-store.d.ts +19 -0
- package/dist/builtins/camera-grid/grid-layout-native-provider.d.ts +8 -0
- package/dist/builtins/camera-grid/grid-plan.d.ts +22 -0
- package/dist/builtins/camera-grid/grid-profiles.d.ts +47 -0
- package/dist/builtins/camera-grid/grid-row-normalization.d.ts +10 -0
- package/dist/builtins/camera-grid/grid-sentry-sources.d.ts +52 -0
- package/dist/builtins/camera-grid/grid-snapshot.d.ts +62 -0
- package/dist/builtins/camera-grid/grid-stream-descriptor.d.ts +24 -0
- package/dist/builtins/camera-grid/grid-stream-invocation.d.ts +29 -0
- package/dist/builtins/camera-grid/grid-stream-relay.d.ts +36 -0
- package/dist/builtins/camera-grid/grid-stream-session.d.ts +113 -0
- package/dist/builtins/camera-grid/grid-wire-format.d.ts +14 -0
- package/dist/builtins/camera-grid/index.d.ts +44 -0
- package/dist/builtins/camera-grid/index.js +2309 -0
- package/dist/builtins/camera-grid/index.mjs +2276 -0
- package/dist/builtins/camera-grid/silence-analysis.d.ts +13 -0
- package/dist/builtins/console-logging/index.js +1 -1
- package/dist/builtins/console-logging/index.mjs +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.js +1 -1
- package/dist/builtins/core-blocks/core-blocks.addon.mjs +1 -1
- package/dist/builtins/device-manager/device-manager.addon.js +2 -2
- package/dist/builtins/device-manager/device-manager.addon.mjs +2 -2
- package/dist/builtins/doorbell/virtual-doorbell.addon.js +1 -1
- package/dist/builtins/doorbell/virtual-doorbell.addon.mjs +1 -1
- package/dist/builtins/hub-forwarder/index.js +1 -1
- package/dist/builtins/hub-forwarder/index.mjs +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.js +1 -1
- package/dist/builtins/liveness-monitor/liveness-monitor.addon.mjs +1 -1
- package/dist/builtins/local-auth/local-auth.addon.js +1 -1
- package/dist/builtins/local-auth/local-auth.addon.mjs +1 -1
- package/dist/builtins/local-network/local-network.addon.js +1 -1
- package/dist/builtins/local-network/local-network.addon.mjs +1 -1
- package/dist/builtins/loki-logging/index.js +1 -1
- package/dist/builtins/loki-logging/index.mjs +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.js +1 -1
- package/dist/builtins/native-metrics/native-metrics.addon.mjs +1 -1
- package/dist/builtins/platform-probe/index.js +1 -1
- package/dist/builtins/platform-probe/index.mjs +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.js +1 -1
- package/dist/builtins/remote-access-orchestrator/remote-access-orchestrator.addon.mjs +1 -1
- package/dist/builtins/snapshot/index.js +1 -1
- package/dist/builtins/snapshot/index.mjs +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.js +1 -1
- package/dist/builtins/sqlite-storage/filesystem-storage.addon.mjs +1 -1
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.js +0 -0
- package/dist/builtins/sqlite-storage/sqlite-settings.addon.mjs +0 -0
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.js +1 -1
- package/dist/builtins/storage-orchestrator/storage-orchestrator.addon.mjs +1 -1
- package/dist/builtins/system-config/system-config.addon.js +1 -1
- package/dist/builtins/system-config/system-config.addon.mjs +1 -1
- package/dist/builtins/winston-logging/index.js +1 -1
- package/dist/builtins/winston-logging/index.mjs +1 -1
- package/dist/{dist-B9y-3rme.js → dist-BJj6Akye.js} +1347 -13
- package/dist/{dist-DH6ThyTd.mjs → dist-Ch93tyHB.mjs} +1288 -14
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{retired-settings-keys-CnMg7wvS.js → retired-settings-keys-BRCn6-mt.js} +1 -1
- package/dist/{retired-settings-keys-DNl1lrr_.mjs → retired-settings-keys-uCaSKuwd.mjs} +1 -1
- package/package.json +32 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import "node:crypto";
|
|
2
3
|
//#region ../types/dist/event-category-BVDXG4tB.mjs
|
|
3
4
|
var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
4
5
|
EventCategory["SystemBoot"] = "system.boot";
|
|
@@ -690,7 +691,7 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
|
|
|
690
691
|
return EventCategory;
|
|
691
692
|
}({});
|
|
692
693
|
//#endregion
|
|
693
|
-
//#region ../types/dist/sleep-
|
|
694
|
+
//#region ../types/dist/sleep-GU_us3DG.mjs
|
|
694
695
|
/**
|
|
695
696
|
* The audio chunk plane's byte format, and the ONE expansion from a coded
|
|
696
697
|
* window to float samples (D455).
|
|
@@ -1025,6 +1026,22 @@ function typeOf(field) {
|
|
|
1025
1026
|
}
|
|
1026
1027
|
}
|
|
1027
1028
|
/**
|
|
1029
|
+
* Narrow a SystemEvent to a typed event by checking its category.
|
|
1030
|
+
* Returns `true` (and narrows the type) if the category matches.
|
|
1031
|
+
*
|
|
1032
|
+
* @example
|
|
1033
|
+
* ```typescript
|
|
1034
|
+
* eventBus.subscribe({ category: 'addon.started' }, (event) => {
|
|
1035
|
+
* if (isEvent(event, 'addon.started')) {
|
|
1036
|
+
* event.data.addonId // ✓ typed as string
|
|
1037
|
+
* }
|
|
1038
|
+
* })
|
|
1039
|
+
* ```
|
|
1040
|
+
*/
|
|
1041
|
+
function isEvent(event, category) {
|
|
1042
|
+
return event.category === category;
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1028
1045
|
* Create a typed event with less boilerplate.
|
|
1029
1046
|
*
|
|
1030
1047
|
* @example
|
|
@@ -3387,6 +3404,360 @@ function sleep(ms) {
|
|
|
3387
3404
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
3388
3405
|
}
|
|
3389
3406
|
//#endregion
|
|
3407
|
+
//#region ../types/dist/canonical-hash-biq7T_dN.mjs
|
|
3408
|
+
var AUDIO_ENCODER_BY_CODEC = {
|
|
3409
|
+
opus: "libopus",
|
|
3410
|
+
aac: "aac",
|
|
3411
|
+
pcmu: "pcm_mulaw",
|
|
3412
|
+
pcma: "pcm_alaw"
|
|
3413
|
+
};
|
|
3414
|
+
/**
|
|
3415
|
+
* Camera-microphone audio, per codec. Lives HERE rather than in
|
|
3416
|
+
* `encode-defaults.ts` only to avoid an import cycle (`encode-defaults` depends
|
|
3417
|
+
* on these types); it is re-exported from there, which is where to read it.
|
|
3418
|
+
*
|
|
3419
|
+
* Every source in this repo is a mono camera mic. The former broker preset
|
|
3420
|
+
* encoded Opus at `channels: 2`, spending bitrate duplicating one channel —
|
|
3421
|
+
* that is the value this consolidation changed.
|
|
3422
|
+
*/
|
|
3423
|
+
var AUDIO_PRESETS = {
|
|
3424
|
+
aac: {
|
|
3425
|
+
kind: "encode",
|
|
3426
|
+
codec: "aac",
|
|
3427
|
+
bitrateKbps: 128,
|
|
3428
|
+
sampleRateHz: 48e3,
|
|
3429
|
+
channels: 1
|
|
3430
|
+
},
|
|
3431
|
+
opus: {
|
|
3432
|
+
kind: "encode",
|
|
3433
|
+
codec: "opus",
|
|
3434
|
+
bitrateKbps: 64,
|
|
3435
|
+
sampleRateHz: 48e3,
|
|
3436
|
+
channels: 1
|
|
3437
|
+
},
|
|
3438
|
+
pcmu: {
|
|
3439
|
+
kind: "encode",
|
|
3440
|
+
codec: "pcmu",
|
|
3441
|
+
sampleRateHz: 8e3,
|
|
3442
|
+
channels: 1
|
|
3443
|
+
}
|
|
3444
|
+
};
|
|
3445
|
+
/** `-hide_banner -loglevel <level>` — every ffmpeg site opens with this. */
|
|
3446
|
+
function logBannerArgs(level) {
|
|
3447
|
+
return [
|
|
3448
|
+
"-hide_banner",
|
|
3449
|
+
"-loglevel",
|
|
3450
|
+
level
|
|
3451
|
+
];
|
|
3452
|
+
}
|
|
3453
|
+
/** `true` when the resolved value means "decode in software" (⇒ no `-hwaccel`). */
|
|
3454
|
+
function isSoftwareDecode(decodeHwAccel) {
|
|
3455
|
+
return !decodeHwAccel || decodeHwAccel === "none" || decodeHwAccel === "copy";
|
|
3456
|
+
}
|
|
3457
|
+
/**
|
|
3458
|
+
* Every INPUT option, in order, terminated by `-i <url>`. Nothing may be
|
|
3459
|
+
* appended to this list by a caller — that is the whole point of the function.
|
|
3460
|
+
*/
|
|
3461
|
+
function buildInputArgs(input, decodeHwAccel) {
|
|
3462
|
+
const args = [];
|
|
3463
|
+
if (!isSoftwareDecode(decodeHwAccel)) args.push("-hwaccel", String(decodeHwAccel));
|
|
3464
|
+
if (input.extraArgs?.length) args.push(...input.extraArgs);
|
|
3465
|
+
if (input.analyzeDurationUs !== void 0) args.push("-analyzeduration", String(input.analyzeDurationUs));
|
|
3466
|
+
if (input.probeSizeBytes !== void 0) args.push("-probesize", String(input.probeSizeBytes));
|
|
3467
|
+
if (input.lowDelay === true) args.push("-flags", "low_delay");
|
|
3468
|
+
if (input.fflags?.length) for (const flag of input.fflags) args.push("-fflags", flag);
|
|
3469
|
+
if (input.rtspTransport) args.push("-rtsp_transport", input.rtspTransport);
|
|
3470
|
+
args.push("-i", input.url);
|
|
3471
|
+
return args;
|
|
3472
|
+
}
|
|
3473
|
+
/**
|
|
3474
|
+
* Derive the output `-map` selectors. The three cases, in the order they are
|
|
3475
|
+
* decided:
|
|
3476
|
+
*
|
|
3477
|
+
* 1. **A filter graph** — the picture is the graph's declared video pad, and
|
|
3478
|
+
* the audio is its audio pad when it has one, otherwise input 0's mic. A
|
|
3479
|
+
* graph's output is not addressable any other way.
|
|
3480
|
+
* 2. **N inputs, no graph** — map input 0 EXPLICITLY. ffmpeg's own selection
|
|
3481
|
+
* picks the "best" stream across ALL inputs, so the egress would silently
|
|
3482
|
+
* become whichever camera happens to have the largest frame.
|
|
3483
|
+
* 3. **One input, no graph** — no `-map`. Anything else would change the argv
|
|
3484
|
+
* of every call site that exists today for no gain.
|
|
3485
|
+
*/
|
|
3486
|
+
function resolveStreamMaps(selection) {
|
|
3487
|
+
const graph = selection.filterGraph;
|
|
3488
|
+
if (graph) return {
|
|
3489
|
+
video: `[${graph.videoOutLabel}]`,
|
|
3490
|
+
audio: graph.audioOutLabel ? `[${graph.audioOutLabel}]` : "0:a:0?"
|
|
3491
|
+
};
|
|
3492
|
+
if (selection.extraInputs?.length) return {
|
|
3493
|
+
video: "0:v:0",
|
|
3494
|
+
audio: "0:a:0?"
|
|
3495
|
+
};
|
|
3496
|
+
return {
|
|
3497
|
+
video: null,
|
|
3498
|
+
audio: null
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
/** The `-vf` filter args, or `[]` when a consumer `-vf` already claims the slot. */
|
|
3502
|
+
function buildVideoFilterArgs(scale, outputArgs, filterGraph) {
|
|
3503
|
+
if (filterGraph) {
|
|
3504
|
+
if (scale) throw new Error("FfmpegInvocation: video.scale cannot be combined with a filter graph — scale inside the graph instead (the -vf would feed an output the graph already feeds)");
|
|
3505
|
+
if (outputArgs.some((a) => a === "-vf")) throw new Error("FfmpegInvocation: outputArgs carries a -vf while a filter graph feeds the same output — fold the filter into the graph");
|
|
3506
|
+
return [];
|
|
3507
|
+
}
|
|
3508
|
+
if (!scale) return [];
|
|
3509
|
+
if (outputArgs.some((a) => a === "-vf")) return [];
|
|
3510
|
+
if (scale.mode === "exact") return ["-vf", `scale=${scale.width}:${scale.height}`];
|
|
3511
|
+
return ["-vf", `scale='min(${scale.width},iw)':'min(${scale.height},ih)':force_original_aspect_ratio=decrease:force_divisible_by=2`];
|
|
3512
|
+
}
|
|
3513
|
+
/** Rate-control args for an encode plan. */
|
|
3514
|
+
function buildRateControlArgs(video) {
|
|
3515
|
+
const kbps = video.bitrateKbps;
|
|
3516
|
+
if (kbps === void 0) return [];
|
|
3517
|
+
const rc = video.rateControl ?? {
|
|
3518
|
+
kind: "cap",
|
|
3519
|
+
vbvSeconds: 2
|
|
3520
|
+
};
|
|
3521
|
+
const bufsize = Math.max(1, Math.round(kbps * rc.vbvSeconds));
|
|
3522
|
+
return [
|
|
3523
|
+
...rc.kind === "cbr" ? ["-b:v", `${kbps}k`] : [],
|
|
3524
|
+
"-maxrate",
|
|
3525
|
+
`${kbps}k`,
|
|
3526
|
+
"-bufsize",
|
|
3527
|
+
`${bufsize}k`
|
|
3528
|
+
];
|
|
3529
|
+
}
|
|
3530
|
+
/** The whole video block (`-vf` … `-c:v` … knobs), after `-i`. */
|
|
3531
|
+
function buildVideoArgs(video, outputArgs, filterGraph) {
|
|
3532
|
+
if (video.kind === "copy") {
|
|
3533
|
+
if (filterGraph) throw new Error("FfmpegInvocation: a filter graph produces a NEW picture, so video.kind must be `encode`");
|
|
3534
|
+
return [
|
|
3535
|
+
"-c:v",
|
|
3536
|
+
"copy",
|
|
3537
|
+
...video.bitstreamFilter ? ["-bsf:v", video.bitstreamFilter] : []
|
|
3538
|
+
];
|
|
3539
|
+
}
|
|
3540
|
+
const args = [
|
|
3541
|
+
...buildVideoFilterArgs(video.scale, outputArgs, filterGraph),
|
|
3542
|
+
"-c:v",
|
|
3543
|
+
video.encoder
|
|
3544
|
+
];
|
|
3545
|
+
if (video.preset !== void 0) args.push("-preset", video.preset);
|
|
3546
|
+
if (video.tune !== void 0) args.push("-tune", video.tune);
|
|
3547
|
+
if (video.singleSlicePerFrame === true && video.encoder === "libx264") args.push("-x264-params", "sliced-threads=0");
|
|
3548
|
+
if (video.profile !== void 0) args.push("-profile:v", video.profile);
|
|
3549
|
+
if (video.level !== void 0) args.push("-level", video.level);
|
|
3550
|
+
if (video.pixelFormat !== void 0) args.push("-pix_fmt", video.pixelFormat);
|
|
3551
|
+
if (video.fps !== void 0) args.push("-r", String(video.fps));
|
|
3552
|
+
if (video.gopFrames !== void 0) args.push("-g", String(video.gopFrames));
|
|
3553
|
+
if (video.forceKeyFramesSeconds !== void 0) args.push("-force_key_frames", `expr:gte(t,n_forced*${video.forceKeyFramesSeconds})`);
|
|
3554
|
+
if (video.bf !== void 0) args.push("-bf", String(video.bf));
|
|
3555
|
+
args.push(...buildRateControlArgs(video));
|
|
3556
|
+
if (video.bitstreamFilter !== void 0) args.push("-bsf:v", video.bitstreamFilter);
|
|
3557
|
+
return args;
|
|
3558
|
+
}
|
|
3559
|
+
/** The whole audio block, after `-i`. */
|
|
3560
|
+
function buildAudioArgs(audio) {
|
|
3561
|
+
if (audio.kind === "none") return ["-an"];
|
|
3562
|
+
if (audio.kind === "copy") return ["-c:a", "copy"];
|
|
3563
|
+
const args = [];
|
|
3564
|
+
if (audio.filter !== void 0) args.push("-af", audio.filter);
|
|
3565
|
+
args.push("-c:a", AUDIO_ENCODER_BY_CODEC[audio.codec]);
|
|
3566
|
+
if (audio.application !== void 0) args.push("-application", audio.application);
|
|
3567
|
+
if (audio.frameDurationMs !== void 0) args.push("-frame_duration", String(audio.frameDurationMs));
|
|
3568
|
+
if (audio.globalHeader === true) args.push("-flags", "+global_header");
|
|
3569
|
+
if (audio.sampleRateHz !== void 0) args.push("-ar", String(audio.sampleRateHz));
|
|
3570
|
+
if (audio.bitrateKbps !== void 0) args.push("-b:a", `${audio.bitrateKbps}k`);
|
|
3571
|
+
if (audio.vbvBufferKbits !== void 0) args.push("-bufsize", `${audio.vbvBufferKbits}k`);
|
|
3572
|
+
if (audio.channels !== void 0) args.push("-ac", String(audio.channels));
|
|
3573
|
+
return args;
|
|
3574
|
+
}
|
|
3575
|
+
/** RTP output-leg args (`-payload_type`, `-ssrc`, `-sdp_file`, `-f rtp <url>`). */
|
|
3576
|
+
function buildRtpOutputArgs(out) {
|
|
3577
|
+
const args = [];
|
|
3578
|
+
if (out.payloadType !== void 0) args.push("-payload_type", String(out.payloadType));
|
|
3579
|
+
if (out.ssrc !== void 0) args.push("-ssrc", String(out.ssrc));
|
|
3580
|
+
if (out.sdpFile !== void 0) args.push("-sdp_file", out.sdpFile);
|
|
3581
|
+
args.push("-f", "rtp", out.url);
|
|
3582
|
+
return args;
|
|
3583
|
+
}
|
|
3584
|
+
/** `true` when the sink is a raw elementary bytestream that cannot mux audio. */
|
|
3585
|
+
function isElementaryVideoSink(sink) {
|
|
3586
|
+
return sink.kind === "stdout" && (sink.container === "h264" || sink.container === "hevc");
|
|
3587
|
+
}
|
|
3588
|
+
/**
|
|
3589
|
+
* The fragmented-MP4 muxer flags, in the order the recorder has proven them
|
|
3590
|
+
* (`recorder/addon/ffmpeg-args.ts` passes the same `movflags` string through
|
|
3591
|
+
* `-segment_format_options`, across every vendor in the fleet):
|
|
3592
|
+
*
|
|
3593
|
+
* - `frag_keyframe` — cut a fragment at each key frame, so every fragment
|
|
3594
|
+
* opens on a sync sample. HKSV's whole requirement.
|
|
3595
|
+
* - `empty_moov` — write `ftyp`+`moov` up front with no samples in it, which
|
|
3596
|
+
* is what makes the head a standalone INITIALISATION segment.
|
|
3597
|
+
* - `default_base_moof` — fragment offsets are self-relative, so a fragment is
|
|
3598
|
+
* demuxable without the bytes that preceded it. D31's byte-range read path
|
|
3599
|
+
* depends on exactly this property of the recorder's segments.
|
|
3600
|
+
*/
|
|
3601
|
+
var FMP4_MOVFLAGS = "+frag_keyframe+empty_moov+default_base_moof";
|
|
3602
|
+
/**
|
|
3603
|
+
* The terminal sink args for every non-`rtp-outputs` sink. Exhaustive over the
|
|
3604
|
+
* union so a new member cannot fall through to `['-f', container, 'pipe:1']`,
|
|
3605
|
+
* which is what a plain `container` read would have done for `mp4` — a valid
|
|
3606
|
+
* argv that writes a NON-fragmented, unseekable-to-a-pipe MP4 and produces one
|
|
3607
|
+
* unusable byte stream.
|
|
3608
|
+
*/
|
|
3609
|
+
function buildStdoutOrRtspSinkArgs(sink) {
|
|
3610
|
+
if (sink.kind === "rtsp-listen") return [
|
|
3611
|
+
"-f",
|
|
3612
|
+
"rtsp",
|
|
3613
|
+
"-rtsp_transport",
|
|
3614
|
+
"tcp",
|
|
3615
|
+
"-rtsp_flags",
|
|
3616
|
+
"listen",
|
|
3617
|
+
sink.url
|
|
3618
|
+
];
|
|
3619
|
+
if (sink.kind === "rtp-outputs") return [];
|
|
3620
|
+
return sink.container === "mp4" ? buildFmp4SinkArgs(sink) : [
|
|
3621
|
+
"-f",
|
|
3622
|
+
sink.container,
|
|
3623
|
+
"pipe:1"
|
|
3624
|
+
];
|
|
3625
|
+
}
|
|
3626
|
+
/**
|
|
3627
|
+
* How far BELOW the negotiated fragment length `-min_frag_duration` is set.
|
|
3628
|
+
*
|
|
3629
|
+
* `-min_frag_duration` refuses to cut before that much media has accumulated,
|
|
3630
|
+
* and then waits for the next key frame. Set to exactly `fragmentMs`, the
|
|
3631
|
+
* commonest camera configuration in existence — a key-frame grid EQUAL to the
|
|
3632
|
+
* requested fragment length — lands the deadline on the same instant as the key
|
|
3633
|
+
* frame, loses the race, and skips to the following one: **every fragment comes
|
|
3634
|
+
* out at twice the requested length.**
|
|
3635
|
+
*
|
|
3636
|
+
* Measured on the live fleet 2026-08-07, camera 615, `-c:v copy` (D84):
|
|
3637
|
+
*
|
|
3638
|
+
* | slot | GOP | `-min_frag_duration` | median gap |
|
|
3639
|
+
* | --- | --- | --- | --- |
|
|
3640
|
+
* | 1280×720 | 40 f @ 10 fps = 4.0 s | 4000 ms | **7944 ms** |
|
|
3641
|
+
* | 1280×720 | 40 f @ 10 fps = 4.0 s | 3600 ms | 3973 ms |
|
|
3642
|
+
* | 3840×2160 | 100 f @ 25 fps = 4.0 s | 4000 ms | 8042 ms |
|
|
3643
|
+
* | 3840×2160 | 100 f @ 25 fps = 4.0 s | 3600 ms | 3998 ms |
|
|
3644
|
+
*
|
|
3645
|
+
* A doubled fragment is not a cosmetic overshoot: HKSV requires every fragment
|
|
3646
|
+
* to be no longer than the length the controller SELECTED, so the shipped-but-
|
|
3647
|
+
* inert phase-1 sink would have violated the contract on its first real clip.
|
|
3648
|
+
*
|
|
3649
|
+
* 10 % is chosen against the two failures either side of it. Too small and
|
|
3650
|
+
* ordinary jitter (measured spread 3953-4096 ms) re-loses the race; too large
|
|
3651
|
+
* and a source with a key frame slightly EARLY than the grid gets cut there,
|
|
3652
|
+
* yielding a short fragment for no reason.
|
|
3653
|
+
*/
|
|
3654
|
+
var FMP4_MIN_FRAG_MARGIN = .9;
|
|
3655
|
+
/** `-movflags … -min_frag_duration <us> -f mp4 pipe:1`. */
|
|
3656
|
+
function buildFmp4SinkArgs(sink) {
|
|
3657
|
+
return [
|
|
3658
|
+
"-movflags",
|
|
3659
|
+
FMP4_MOVFLAGS,
|
|
3660
|
+
"-min_frag_duration",
|
|
3661
|
+
String(Math.max(0, Math.round(sink.fragmentMs * FMP4_MIN_FRAG_MARGIN * 1e3))),
|
|
3662
|
+
"-f",
|
|
3663
|
+
"mp4",
|
|
3664
|
+
"pipe:1"
|
|
3665
|
+
];
|
|
3666
|
+
}
|
|
3667
|
+
/**
|
|
3668
|
+
* A second output mapping source audio to RTP-over-UDP. `0:a:0?` makes the
|
|
3669
|
+
* audio optional so a source with no audio skips it instead of failing the
|
|
3670
|
+
* whole invocation.
|
|
3671
|
+
*/
|
|
3672
|
+
function buildAudioSidecarArgs(sidecar) {
|
|
3673
|
+
return [
|
|
3674
|
+
"-map",
|
|
3675
|
+
"0:a:0?",
|
|
3676
|
+
...buildAudioArgs(sidecar.codec === "pcma" ? {
|
|
3677
|
+
kind: "encode",
|
|
3678
|
+
codec: "pcma",
|
|
3679
|
+
sampleRateHz: 8e3,
|
|
3680
|
+
channels: 1
|
|
3681
|
+
} : AUDIO_PRESETS[sidecar.codec]),
|
|
3682
|
+
...buildRtpOutputArgs({
|
|
3683
|
+
url: sidecar.rtpUrl,
|
|
3684
|
+
sdpFile: sidecar.sdpFile
|
|
3685
|
+
})
|
|
3686
|
+
];
|
|
3687
|
+
}
|
|
3688
|
+
/**
|
|
3689
|
+
* Assemble the full ffmpeg argument list. Layout:
|
|
3690
|
+
*
|
|
3691
|
+
* -hide_banner -loglevel <level>
|
|
3692
|
+
* [-hwaccel <backend|auto>] ─┐ INPUT options — strictly before -i.
|
|
3693
|
+
* [<input.extraArgs>] │
|
|
3694
|
+
* [-fflags <flag>…] │
|
|
3695
|
+
* [-rtsp_transport tcp] │
|
|
3696
|
+
* -i <url> ─┘ …repeated per extraInput
|
|
3697
|
+
* [-filter_complex <graph>] ─ global, after the LAST -i
|
|
3698
|
+
* [-map <video>] <video block> <threads> ─┐ OUTPUT options.
|
|
3699
|
+
* [-map <audio>] <audio block> │
|
|
3700
|
+
* <consumer outputArgs verbatim> │
|
|
3701
|
+
* <sink> ─┘ terminal
|
|
3702
|
+
*
|
|
3703
|
+
* The `-map`s are DERIVED (see {@link resolveStreamMaps}), never constants:
|
|
3704
|
+
* one input with no graph emits none at all, which is why the argv of every
|
|
3705
|
+
* call site that predates the N-input extension is unchanged to the byte.
|
|
3706
|
+
*
|
|
3707
|
+
* {@link FfmpegInvocation.audioSidecar} is the one map that stays literal
|
|
3708
|
+
* (`0:a:0?`): the sidecar is defined as the SOURCE's microphone on its own RTP
|
|
3709
|
+
* output, not as whatever the main output happens to carry.
|
|
3710
|
+
*/
|
|
3711
|
+
function buildFfmpegArgs(inv) {
|
|
3712
|
+
const graph = inv.filterGraph ?? null;
|
|
3713
|
+
const head = [
|
|
3714
|
+
...logBannerArgs(inv.logLevel),
|
|
3715
|
+
...buildInputArgs(inv.input, inv.decodeHwAccel),
|
|
3716
|
+
...(inv.extraInputs ?? []).flatMap((extra) => buildInputArgs(extra, inv.decodeHwAccel)),
|
|
3717
|
+
...graph ? ["-filter_complex", graph.graph] : []
|
|
3718
|
+
];
|
|
3719
|
+
const threadArgs = inv.threadCount > 0 ? ["-threads", String(inv.threadCount)] : [];
|
|
3720
|
+
const maps = resolveStreamMaps(inv);
|
|
3721
|
+
const videoMapArgs = maps.video ? ["-map", maps.video] : [];
|
|
3722
|
+
const audioMapArgs = maps.audio ? ["-map", maps.audio] : [];
|
|
3723
|
+
if (inv.sink.kind === "rtp-outputs") {
|
|
3724
|
+
const videoLeg = inv.sink.video ? [
|
|
3725
|
+
"-an",
|
|
3726
|
+
"-map",
|
|
3727
|
+
maps.video ?? "0:v:0",
|
|
3728
|
+
...buildVideoArgs(inv.video, inv.outputArgs, graph),
|
|
3729
|
+
...threadArgs,
|
|
3730
|
+
...inv.outputArgs,
|
|
3731
|
+
...buildRtpOutputArgs(inv.sink.video)
|
|
3732
|
+
] : [];
|
|
3733
|
+
const audioLeg = inv.sink.audio ? [
|
|
3734
|
+
"-vn",
|
|
3735
|
+
"-map",
|
|
3736
|
+
maps.audio ?? "0:a:0?",
|
|
3737
|
+
...buildAudioArgs(inv.audio),
|
|
3738
|
+
...buildRtpOutputArgs(inv.sink.audio)
|
|
3739
|
+
] : [];
|
|
3740
|
+
return [
|
|
3741
|
+
...head,
|
|
3742
|
+
...videoLeg,
|
|
3743
|
+
...audioLeg
|
|
3744
|
+
];
|
|
3745
|
+
}
|
|
3746
|
+
const mutedAudio = isElementaryVideoSink(inv.sink) || inv.audio.kind === "none";
|
|
3747
|
+
const audioArgs = isElementaryVideoSink(inv.sink) ? ["-an"] : buildAudioArgs(inv.audio);
|
|
3748
|
+
return [
|
|
3749
|
+
...head,
|
|
3750
|
+
...videoMapArgs,
|
|
3751
|
+
...buildVideoArgs(inv.video, inv.outputArgs, graph),
|
|
3752
|
+
...threadArgs,
|
|
3753
|
+
...mutedAudio ? [] : audioMapArgs,
|
|
3754
|
+
...audioArgs,
|
|
3755
|
+
...inv.outputArgs,
|
|
3756
|
+
...buildStdoutOrRtspSinkArgs(inv.sink),
|
|
3757
|
+
...inv.audioSidecar ? buildAudioSidecarArgs(inv.audioSidecar) : []
|
|
3758
|
+
];
|
|
3759
|
+
}
|
|
3760
|
+
//#endregion
|
|
3390
3761
|
//#region ../types/dist/err-msg-IQTHeDzc.mjs
|
|
3391
3762
|
/**
|
|
3392
3763
|
import { errMsg } from '@camstack/types'
|
|
@@ -3912,6 +4283,14 @@ var CameraSwitchGroupSchema = z.object({
|
|
|
3912
4283
|
fetchedAt: z.number()
|
|
3913
4284
|
});
|
|
3914
4285
|
/**
|
|
4286
|
+
* The wrapper capability each wrapper-backed switch controls. Named constants
|
|
4287
|
+
* because the same strings appear in `legacy-migrations.ts`, in
|
|
4288
|
+
* `isCapActiveForDevice` call sites and in the fake harness — a typo in any of
|
|
4289
|
+
* them is a switch that silently writes a binding nobody reads.
|
|
4290
|
+
*/
|
|
4291
|
+
var DETECTION_PIPELINE_CAP_NAME = "detection-pipeline";
|
|
4292
|
+
var AUDIO_ANALYSIS_CAP_NAME = "audio-analysis";
|
|
4293
|
+
/**
|
|
3915
4294
|
* The value a redacted secret is replaced with on every read surface.
|
|
3916
4295
|
*
|
|
3917
4296
|
* It is also the WRITE-BACK token: an `upsertLocation` that sends this value
|
|
@@ -10974,6 +11353,45 @@ var SourceInfoSchema = z.object({
|
|
|
10974
11353
|
raw: z.record(z.string(), z.unknown()).optional()
|
|
10975
11354
|
});
|
|
10976
11355
|
/**
|
|
11356
|
+
* Build the synthetic SourceInfo every existing provider falls back to
|
|
11357
|
+
* when no upstream value has been persisted. Keeps non-migrated providers
|
|
11358
|
+
* (Reolink / Hikvision / ONVIF / RTSP) functional without code
|
|
11359
|
+
* changes — `id` reuses the CamStack stableId, `system` reuses the addon
|
|
11360
|
+
* id. Real upstream identity replaces this once a provider migrates and
|
|
11361
|
+
* calls `updateSourceInfo()` with concrete values.
|
|
11362
|
+
*/
|
|
11363
|
+
function synthesizeSourceInfo(input) {
|
|
11364
|
+
return {
|
|
11365
|
+
id: input.stableId,
|
|
11366
|
+
system: input.addonId
|
|
11367
|
+
};
|
|
11368
|
+
}
|
|
11369
|
+
/**
|
|
11370
|
+
* Shallow-merge a partial patch over a prior SourceInfo. `undefined`
|
|
11371
|
+
* patch values are ignored (no field clobber); to clear a field
|
|
11372
|
+
* explicitly, providers omit it from the patch and rebuild from scratch
|
|
11373
|
+
* via `updateSourceInfo({})` cycles — uncommon enough that we don't
|
|
11374
|
+
* bother with a `null`-means-delete sentinel.
|
|
11375
|
+
*
|
|
11376
|
+
* The `raw` field is also shallow-merged: passing `raw: { foo: 1 }`
|
|
11377
|
+
* merges over existing `raw` keys instead of replacing the whole bag.
|
|
11378
|
+
*/
|
|
11379
|
+
function mergeSourceInfo(prev, patch) {
|
|
11380
|
+
const next = { ...prev };
|
|
11381
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
11382
|
+
if (v === void 0) continue;
|
|
11383
|
+
if (k === "raw" && typeof v === "object" && v !== null) {
|
|
11384
|
+
next.raw = {
|
|
11385
|
+
...prev.raw,
|
|
11386
|
+
...v
|
|
11387
|
+
};
|
|
11388
|
+
continue;
|
|
11389
|
+
}
|
|
11390
|
+
next[k] = v;
|
|
11391
|
+
}
|
|
11392
|
+
return next;
|
|
11393
|
+
}
|
|
11394
|
+
/**
|
|
10977
11395
|
* Coerce common unit-of-measurement string variants to a canonical form
|
|
10978
11396
|
* so the UI's display templates can rely on stable strings. Pure
|
|
10979
11397
|
* passthrough for unknown units — kept conservative on purpose, the goal
|
|
@@ -11019,6 +11437,27 @@ function normalizeUnit(raw) {
|
|
|
11019
11437
|
return trimmed;
|
|
11020
11438
|
}
|
|
11021
11439
|
/**
|
|
11440
|
+
* Key under which `SourceInfo` lives inside `DeviceMeta.metadata`. The
|
|
11441
|
+
* blob is a free-form `Record<string, unknown>` shared with hardware-
|
|
11442
|
+
* identity fields (`manufacturer`, `model`, `firmware`, …); SourceInfo
|
|
11443
|
+
* lives under a single nested key to keep it composable with those.
|
|
11444
|
+
*/
|
|
11445
|
+
var SOURCE_INFO_METADATA_KEY = "sourceInfo";
|
|
11446
|
+
/**
|
|
11447
|
+
* Extract a SourceInfo from a `DeviceMeta.metadata` blob, validating
|
|
11448
|
+
* with Zod. Returns `null` when the blob is missing or the nested
|
|
11449
|
+
* `sourceInfo` key fails validation — callers fall back to the
|
|
11450
|
+
* synthetic default in that case (see `synthesizeSourceInfo`).
|
|
11451
|
+
*/
|
|
11452
|
+
function extractSourceInfoFromMetadata(metadata) {
|
|
11453
|
+
if (!metadata) return null;
|
|
11454
|
+
const raw = metadata[SOURCE_INFO_METADATA_KEY];
|
|
11455
|
+
if (!raw) return null;
|
|
11456
|
+
const parsed = SourceInfoSchema.safeParse(raw);
|
|
11457
|
+
if (!parsed.success) return null;
|
|
11458
|
+
return parsed.data;
|
|
11459
|
+
}
|
|
11460
|
+
/**
|
|
11022
11461
|
* device-discovery — device-scoped capability for parents that host /
|
|
11023
11462
|
* enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
|
|
11024
11463
|
* integrations). The parent advertises a list of discoverable children,
|
|
@@ -30645,11 +31084,15 @@ var motionTriggerCapability = {
|
|
|
30645
31084
|
*
|
|
30646
31085
|
* Adding a fifth name to that list would have been the wrong fix twice over:
|
|
30647
31086
|
* that page is per-camera DETECTION tuning, and a grid's geometry belongs
|
|
30648
|
-
* beside PTZ and motion zones on the camera itself
|
|
30649
|
-
*
|
|
30650
|
-
*
|
|
30651
|
-
*
|
|
30652
|
-
*
|
|
31087
|
+
* beside PTZ and motion zones on the camera itself. The device page is
|
|
31088
|
+
* BINDING-driven (D12), so the way in is a capability bound to the device —
|
|
31089
|
+
* and this cap carries its section the way `recording` does, by RETURNING it
|
|
31090
|
+
* from `getDeviceSettingsContribution`.
|
|
31091
|
+
*
|
|
31092
|
+
* Seven other widgets are still declared the other way, through a
|
|
31093
|
+
* `deviceConfig.ui` block the framework derives a section from. That route
|
|
31094
|
+
* gives the addon no say in where its own panel lands and no way to decline
|
|
31095
|
+
* for a device the panel does not suit, which is why this one does not use it.
|
|
30653
31096
|
*
|
|
30654
31097
|
* ## Why one addon may implement it
|
|
30655
31098
|
*
|
|
@@ -30739,13 +31182,24 @@ var cameraGridLayoutCapability = {
|
|
|
30739
31182
|
deviceNative: true,
|
|
30740
31183
|
mode: "singleton",
|
|
30741
31184
|
deviceTypes: [DeviceType.Camera],
|
|
30742
|
-
|
|
30743
|
-
|
|
30744
|
-
|
|
30745
|
-
|
|
30746
|
-
|
|
30747
|
-
|
|
30748
|
-
|
|
31185
|
+
/**
|
|
31186
|
+
* The section is built by the ADDON and returned from
|
|
31187
|
+
* `getDeviceSettingsContribution`, not derived by the framework from a
|
|
31188
|
+
* `deviceConfig.ui` block.
|
|
31189
|
+
*
|
|
31190
|
+
* Both mechanisms render the same widget. This one hands the addon two
|
|
31191
|
+
* things the framework-derived route cannot give it:
|
|
31192
|
+
*
|
|
31193
|
+
* - it chooses its own section, `tab`, `location` and `order`, the way any
|
|
31194
|
+
* other setting does, instead of receiving them from a cap declaration;
|
|
31195
|
+
* - it can DECLINE per device. A camera that is not a grid gets no section
|
|
31196
|
+
* at all, rather than a widget that renders its own "not a grid" state.
|
|
31197
|
+
*
|
|
31198
|
+
* `recording` is the precedent (`recorder/recording-device-settings.ts`): it
|
|
31199
|
+
* returns `null` for anything that is not a camera, so the Recording tab
|
|
31200
|
+
* never appears there.
|
|
31201
|
+
*/
|
|
31202
|
+
exposesDeviceSettings: true,
|
|
30749
31203
|
methods: {
|
|
30750
31204
|
/**
|
|
30751
31205
|
* The grid behind this device.
|
|
@@ -33354,6 +33808,25 @@ var ptzAutotrackCapability = {
|
|
|
33354
33808
|
scope: "device",
|
|
33355
33809
|
deviceNative: true,
|
|
33356
33810
|
mode: "singleton",
|
|
33811
|
+
/**
|
|
33812
|
+
* Two kinds of provider behind one name, the same shape `snapshot` and
|
|
33813
|
+
* `doorbell` use:
|
|
33814
|
+
*
|
|
33815
|
+
* - **Native**: the camera's own firmware autotrack, registered per device by
|
|
33816
|
+
* the driver when the probe says it exists (Reolink: yes; Hikvision:
|
|
33817
|
+
* model-dependent).
|
|
33818
|
+
* - **Wrapper**: the emulated implementation (`@camstack/system`'s `autotrack` builtin),
|
|
33819
|
+
* which follows a subject with `ptz.move` pulses driven by the detection
|
|
33820
|
+
* pipeline. `defaultActive: false` — the operator binds it per camera in
|
|
33821
|
+
* the bindings UI, exactly as `virtual-doorbell` is bound.
|
|
33822
|
+
*
|
|
33823
|
+
* The binding chooses WHICH implementation answers; `setEnabled` stays the
|
|
33824
|
+
* one switch for the function either way. A camera never has both driving its
|
|
33825
|
+
* head, which is the whole reason this is not a second cap with a second
|
|
33826
|
+
* toggle (D62, D520).
|
|
33827
|
+
*/
|
|
33828
|
+
kind: "wrapper",
|
|
33829
|
+
defaultActive: false,
|
|
33357
33830
|
deviceTypes: [DeviceType.Camera],
|
|
33358
33831
|
deviceConfig: { ui: {
|
|
33359
33832
|
kind: "widget",
|
|
@@ -37496,6 +37969,807 @@ var AccessoryKind = {
|
|
|
37496
37969
|
PrivacyMask: DeviceRole.PrivacyMask
|
|
37497
37970
|
};
|
|
37498
37971
|
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
37972
|
+
var DeviceConfig = class DeviceConfig {
|
|
37973
|
+
schema;
|
|
37974
|
+
data;
|
|
37975
|
+
persistFn;
|
|
37976
|
+
constructor(schema, data, persist) {
|
|
37977
|
+
this.schema = schema;
|
|
37978
|
+
this.data = data;
|
|
37979
|
+
this.persistFn = persist;
|
|
37980
|
+
}
|
|
37981
|
+
/**
|
|
37982
|
+
* Build a `DeviceConfig` from a persisted blob, with automatic
|
|
37983
|
+
* recovery from schema-validation failures. Boot must never be
|
|
37984
|
+
* blocked by stale persisted values: if Zod rejects the blob,
|
|
37985
|
+
* we drop every offending top-level field, retry, and persist
|
|
37986
|
+
* the cleaned blob so the bad value is healed in the DB on next
|
|
37987
|
+
* write. The most common trigger is a tightened range constraint
|
|
37988
|
+
* (e.g. `max(100) → max(50)`) on a field that already has an
|
|
37989
|
+
* out-of-range value persisted from the previous schema. Without
|
|
37990
|
+
* this safety net, the device would fail to instantiate and end
|
|
37991
|
+
* up with no caps registered — exactly the failure mode that
|
|
37992
|
+
* stranded device 15 when `motionSensitivity: 90` no longer fit
|
|
37993
|
+
* the new `1..50` schema.
|
|
37994
|
+
*
|
|
37995
|
+
* Recovery rules:
|
|
37996
|
+
* 1. Try `safeParse(initialData)`. If it succeeds, done.
|
|
37997
|
+
* 2. On failure, walk `error.issues`, collect the top-level path
|
|
37998
|
+
* of each issue, and drop those keys from `initialData`.
|
|
37999
|
+
* 3. Re-run `safeParse`. If the cleaned blob now passes (Zod
|
|
38000
|
+
* fills the missing keys with schema defaults / undefined for
|
|
38001
|
+
* `.optional()`), persist it via `persist()` so the bad
|
|
38002
|
+
* values disappear from the DB, and return the device.
|
|
38003
|
+
* 4. If the cleaned blob STILL fails (very rare — would require
|
|
38004
|
+
* a non-recoverable required field), fall back to
|
|
38005
|
+
* `schema.parse({})` so the device still boots with pure
|
|
38006
|
+
* schema defaults. Persist nothing in that path so the next
|
|
38007
|
+
* successful `setAll` still writes a coherent blob.
|
|
38008
|
+
*/
|
|
38009
|
+
static fromSchema(schema, persist, initialData = {}, onRecover) {
|
|
38010
|
+
const first = schema.safeParse(initialData);
|
|
38011
|
+
if (first.success) return new DeviceConfig(schema, first.data, persist);
|
|
38012
|
+
const droppedKeys = /* @__PURE__ */ new Set();
|
|
38013
|
+
for (const issue of first.error.issues) {
|
|
38014
|
+
const top = issue.path[0];
|
|
38015
|
+
if (typeof top === "string") droppedKeys.add(top);
|
|
38016
|
+
}
|
|
38017
|
+
const cleaned = { ...initialData };
|
|
38018
|
+
for (const k of droppedKeys) delete cleaned[k];
|
|
38019
|
+
const second = schema.safeParse(cleaned);
|
|
38020
|
+
onRecover?.({
|
|
38021
|
+
droppedKeys: [...droppedKeys],
|
|
38022
|
+
issues: first.error.issues
|
|
38023
|
+
});
|
|
38024
|
+
if (second.success) {
|
|
38025
|
+
persist(second.data).catch(() => {});
|
|
38026
|
+
return new DeviceConfig(schema, second.data, persist);
|
|
38027
|
+
}
|
|
38028
|
+
return new DeviceConfig(schema, schema.parse({}), persist);
|
|
38029
|
+
}
|
|
38030
|
+
get values() {
|
|
38031
|
+
return this.data;
|
|
38032
|
+
}
|
|
38033
|
+
get(key) {
|
|
38034
|
+
return this.data[key];
|
|
38035
|
+
}
|
|
38036
|
+
async set(key, value) {
|
|
38037
|
+
const next = this.schema.parse({
|
|
38038
|
+
...this.data,
|
|
38039
|
+
[key]: value
|
|
38040
|
+
});
|
|
38041
|
+
this.data = next;
|
|
38042
|
+
await this.persistFn(this.data);
|
|
38043
|
+
}
|
|
38044
|
+
/**
|
|
38045
|
+
* Merge an untyped patch onto the current config and persist. Accepts
|
|
38046
|
+
* `Record<string, unknown>` because the patch typically comes from the
|
|
38047
|
+
* UI form layer (a `ConfigField.key → value` map) where the caller
|
|
38048
|
+
* doesn't hold the Zod schema's static type. Runtime validation is
|
|
38049
|
+
* authoritative: `this.schema.parse` rejects unknown keys or invalid
|
|
38050
|
+
* shapes before touching storage.
|
|
38051
|
+
*/
|
|
38052
|
+
async setAll(partial) {
|
|
38053
|
+
const next = this.schema.parse({
|
|
38054
|
+
...this.data,
|
|
38055
|
+
...partial
|
|
38056
|
+
});
|
|
38057
|
+
this.data = next;
|
|
38058
|
+
await this.persistFn(this.data);
|
|
38059
|
+
}
|
|
38060
|
+
async deleteKey(key) {
|
|
38061
|
+
const { [key]: _, ...rest } = this.data;
|
|
38062
|
+
const next = this.schema.parse(rest);
|
|
38063
|
+
this.data = next;
|
|
38064
|
+
await this.persistFn(this.data);
|
|
38065
|
+
}
|
|
38066
|
+
entries() {
|
|
38067
|
+
const shape = this.schema.shape;
|
|
38068
|
+
return Object.entries(shape).map(([key, fieldSchema]) => ({
|
|
38069
|
+
key,
|
|
38070
|
+
schema: fieldSchema,
|
|
38071
|
+
value: this.data[key],
|
|
38072
|
+
description: fieldSchema.description
|
|
38073
|
+
}));
|
|
38074
|
+
}
|
|
38075
|
+
};
|
|
38076
|
+
/**
|
|
38077
|
+
* Concrete implementation. Routes every successful write through
|
|
38078
|
+
* `writer(capName, slice)` — the kernel hooks this up to
|
|
38079
|
+
* `device-state.setCapSlice`, the canonical cross-layer write
|
|
38080
|
+
* entrypoint, which handles disk persistence (debounced on the hub)
|
|
38081
|
+
* and mirror updates.
|
|
38082
|
+
*
|
|
38083
|
+
* Schema validation runs in-process before the writer is called —
|
|
38084
|
+
* the round-trip should never carry an invalid slice. `flush()`
|
|
38085
|
+
* awaits any in-flight writer promises so shutdown is lossless.
|
|
38086
|
+
*
|
|
38087
|
+
* `initial` is the persisted blob loaded at boot. Slices for caps
|
|
38088
|
+
* whose schema hasn't been installed yet are kept in-memory verbatim
|
|
38089
|
+
* and validated when the cap registers later.
|
|
38090
|
+
*/
|
|
38091
|
+
var DeviceRuntimeState = class DeviceRuntimeState {
|
|
38092
|
+
writer;
|
|
38093
|
+
/** In-flight writer promises tracked so `flush()` can await them. */
|
|
38094
|
+
pendingWrites = /* @__PURE__ */ new Set();
|
|
38095
|
+
/** Per-cap committed slice — after schema validation when known. */
|
|
38096
|
+
slices;
|
|
38097
|
+
/** Per-cap registered schema (set by `installCapSchema`). */
|
|
38098
|
+
schemas = /* @__PURE__ */ new Map();
|
|
38099
|
+
listeners = /* @__PURE__ */ new Set();
|
|
38100
|
+
capListeners = /* @__PURE__ */ new Map();
|
|
38101
|
+
constructor(initial, writer) {
|
|
38102
|
+
this.writer = writer;
|
|
38103
|
+
this.slices = /* @__PURE__ */ new Map();
|
|
38104
|
+
for (const [k, v] of Object.entries(initial)) if (v && typeof v === "object" && !Array.isArray(v)) this.slices.set(k, { ...v });
|
|
38105
|
+
}
|
|
38106
|
+
static fromInitial(initial, writer) {
|
|
38107
|
+
return new DeviceRuntimeState(initial, writer);
|
|
38108
|
+
}
|
|
38109
|
+
installCapSchema(capName, schema) {
|
|
38110
|
+
const existing = this.schemas.get(capName);
|
|
38111
|
+
if (existing) {
|
|
38112
|
+
if (existing !== schema) throw new Error(`[DeviceRuntimeState] capability "${capName}" registered a different runtime-state schema; each cap must declare ONE shape across every provider`);
|
|
38113
|
+
return;
|
|
38114
|
+
}
|
|
38115
|
+
this.schemas.set(capName, schema);
|
|
38116
|
+
const stored = this.slices.get(capName);
|
|
38117
|
+
if (stored) {
|
|
38118
|
+
const result = schema.safeParse(stored);
|
|
38119
|
+
if (result.success) this.slices.set(capName, result.data);
|
|
38120
|
+
else this.slices.delete(capName);
|
|
38121
|
+
}
|
|
38122
|
+
}
|
|
38123
|
+
getCapState(capName) {
|
|
38124
|
+
const slice = this.slices.get(capName);
|
|
38125
|
+
if (!slice) return void 0;
|
|
38126
|
+
return Object.freeze({ ...slice });
|
|
38127
|
+
}
|
|
38128
|
+
getCapField(capName, key) {
|
|
38129
|
+
return this.slices.get(capName)?.[key];
|
|
38130
|
+
}
|
|
38131
|
+
setCapState(capName, value) {
|
|
38132
|
+
this.applyCapWrite(capName, value, false);
|
|
38133
|
+
}
|
|
38134
|
+
patchCapState(capName, partial) {
|
|
38135
|
+
this.applyCapWrite(capName, partial, true);
|
|
38136
|
+
}
|
|
38137
|
+
/**
|
|
38138
|
+
* Internal worker. `merge` controls whether `value` replaces or
|
|
38139
|
+
* shallow-merges into the existing slice. Schema validation runs
|
|
38140
|
+
* on the FINAL composed object regardless.
|
|
38141
|
+
*/
|
|
38142
|
+
applyCapWrite(capName, value, merge) {
|
|
38143
|
+
const schema = this.schemas.get(capName);
|
|
38144
|
+
if (!schema) throw new Error(`[DeviceRuntimeState] no schema registered for cap "${capName}" — did the device register it via ctx.registerNativeCap before writing?`);
|
|
38145
|
+
const current = this.slices.get(capName) ?? {};
|
|
38146
|
+
const next = merge ? {
|
|
38147
|
+
...current,
|
|
38148
|
+
...value
|
|
38149
|
+
} : { ...value };
|
|
38150
|
+
const parsed = schema.parse(next);
|
|
38151
|
+
if (shallowEqual(current, parsed)) return;
|
|
38152
|
+
this.slices.set(capName, parsed);
|
|
38153
|
+
this.fireListeners([capName]);
|
|
38154
|
+
const writePromise = this.writer(capName, { ...parsed }).catch(() => {});
|
|
38155
|
+
this.pendingWrites.add(writePromise);
|
|
38156
|
+
writePromise.finally(() => {
|
|
38157
|
+
this.pendingWrites.delete(writePromise);
|
|
38158
|
+
});
|
|
38159
|
+
}
|
|
38160
|
+
fireListeners(changed) {
|
|
38161
|
+
const snap = this.snapshot();
|
|
38162
|
+
for (const cb of this.listeners) try {
|
|
38163
|
+
cb(changed, snap);
|
|
38164
|
+
} catch {}
|
|
38165
|
+
for (const capName of changed) {
|
|
38166
|
+
const subs = this.capListeners.get(capName);
|
|
38167
|
+
if (!subs) continue;
|
|
38168
|
+
const slice = this.getCapState(capName);
|
|
38169
|
+
for (const cb of subs) try {
|
|
38170
|
+
cb(slice);
|
|
38171
|
+
} catch {}
|
|
38172
|
+
}
|
|
38173
|
+
}
|
|
38174
|
+
subscribe(cb) {
|
|
38175
|
+
this.listeners.add(cb);
|
|
38176
|
+
return () => {
|
|
38177
|
+
this.listeners.delete(cb);
|
|
38178
|
+
};
|
|
38179
|
+
}
|
|
38180
|
+
subscribeCap(capName, cb) {
|
|
38181
|
+
let subs = this.capListeners.get(capName);
|
|
38182
|
+
if (!subs) {
|
|
38183
|
+
subs = /* @__PURE__ */ new Set();
|
|
38184
|
+
this.capListeners.set(capName, subs);
|
|
38185
|
+
}
|
|
38186
|
+
const adapter = (slice) => {
|
|
38187
|
+
cb(slice);
|
|
38188
|
+
};
|
|
38189
|
+
subs.add(adapter);
|
|
38190
|
+
return () => {
|
|
38191
|
+
const set = this.capListeners.get(capName);
|
|
38192
|
+
if (!set) return;
|
|
38193
|
+
set.delete(adapter);
|
|
38194
|
+
if (set.size === 0) this.capListeners.delete(capName);
|
|
38195
|
+
};
|
|
38196
|
+
}
|
|
38197
|
+
snapshot() {
|
|
38198
|
+
const out = {};
|
|
38199
|
+
for (const [k, v] of this.slices) out[k] = Object.freeze({ ...v });
|
|
38200
|
+
return Object.freeze(out);
|
|
38201
|
+
}
|
|
38202
|
+
async flush() {
|
|
38203
|
+
if (this.pendingWrites.size === 0) return;
|
|
38204
|
+
const inflight = [...this.pendingWrites];
|
|
38205
|
+
await Promise.allSettled(inflight);
|
|
38206
|
+
}
|
|
38207
|
+
};
|
|
38208
|
+
function shallowEqual(a, b) {
|
|
38209
|
+
const ak = Object.keys(a);
|
|
38210
|
+
const bk = Object.keys(b);
|
|
38211
|
+
if (ak.length !== bk.length) return false;
|
|
38212
|
+
for (const k of ak) if (a[k] !== b[k]) return false;
|
|
38213
|
+
return true;
|
|
38214
|
+
}
|
|
38215
|
+
/**
|
|
38216
|
+
* Runtime registry: cap-property-name → cap definition. `BaseDevice`'s
|
|
38217
|
+
* `state` getter looks up the cap definition here to construct a
|
|
38218
|
+
* `sliceProxy()` lazily on first access. Generated alongside the type
|
|
38219
|
+
* so type and runtime registry can never drift apart.
|
|
38220
|
+
*/
|
|
38221
|
+
var DEVICE_LOCAL_STATE_CAPS = {
|
|
38222
|
+
airQualitySensor: airQualitySensorCapability,
|
|
38223
|
+
alarmPanel: alarmPanelCapability,
|
|
38224
|
+
ambientLightSensor: ambientLightSensorCapability,
|
|
38225
|
+
audioMetrics: audioMetricsCapability,
|
|
38226
|
+
automationControl: automationControlCapability,
|
|
38227
|
+
battery: batteryCapability,
|
|
38228
|
+
binary: binaryCapability,
|
|
38229
|
+
brightness: brightnessCapability,
|
|
38230
|
+
cameraStreams: cameraStreamsCapability,
|
|
38231
|
+
carbonMonoxide: carbonMonoxideCapability,
|
|
38232
|
+
climateControl: climateControlCapability,
|
|
38233
|
+
color: colorCapability,
|
|
38234
|
+
connectivity: connectivityCapability,
|
|
38235
|
+
consumables: consumablesCapability,
|
|
38236
|
+
contact: contactCapability,
|
|
38237
|
+
control: controlCapability,
|
|
38238
|
+
cover: coverCapability,
|
|
38239
|
+
dayNight: dayNightCapability,
|
|
38240
|
+
deviceDiscovery: deviceDiscoveryCapability,
|
|
38241
|
+
deviceStatus: deviceStatusCapability,
|
|
38242
|
+
doorbell: doorbellCapability,
|
|
38243
|
+
enumSensor: enumSensorCapability,
|
|
38244
|
+
eventEmitter: eventEmitterCapability,
|
|
38245
|
+
fanControl: fanControlCapability,
|
|
38246
|
+
featureProbe: featureProbeCapability,
|
|
38247
|
+
flood: floodCapability,
|
|
38248
|
+
gas: gasCapability,
|
|
38249
|
+
humidifier: humidifierCapability,
|
|
38250
|
+
humiditySensor: humiditySensorCapability,
|
|
38251
|
+
image: imageCapability,
|
|
38252
|
+
imageSettings: imageSettingsCapability,
|
|
38253
|
+
intercom: intercomCapability,
|
|
38254
|
+
lawnMowerControl: lawnMowerControlCapability,
|
|
38255
|
+
lockControl: lockControlCapability,
|
|
38256
|
+
mediaPlayer: mediaPlayerCapability,
|
|
38257
|
+
motion: motionCapability,
|
|
38258
|
+
motionTrigger: motionTriggerCapability,
|
|
38259
|
+
motionZones: motionZonesCapability,
|
|
38260
|
+
nativeObjectDetection: nativeObjectDetectionCapability,
|
|
38261
|
+
navigation: navigationCapability,
|
|
38262
|
+
networkLink: networkLinkCapability,
|
|
38263
|
+
notifier: notifierCapability,
|
|
38264
|
+
numericSensor: numericSensorCapability,
|
|
38265
|
+
petFeeder: petFeederCapability,
|
|
38266
|
+
powerMeter: powerMeterCapability,
|
|
38267
|
+
presence: presenceCapability,
|
|
38268
|
+
pressureSensor: pressureSensorCapability,
|
|
38269
|
+
privacyMask: privacyMaskCapability,
|
|
38270
|
+
ptzAutotrack: ptzAutotrackCapability,
|
|
38271
|
+
recordingSignal: recordingSignalCapability,
|
|
38272
|
+
sceneMonitor: sceneMonitorCapability,
|
|
38273
|
+
scriptRunner: scriptRunnerCapability,
|
|
38274
|
+
smoke: smokeCapability,
|
|
38275
|
+
streamCatalog: streamCatalogCapability,
|
|
38276
|
+
streamParams: streamParamsCapability,
|
|
38277
|
+
streamSignals: streamSignalsCapability,
|
|
38278
|
+
switch: switchCapability,
|
|
38279
|
+
tamper: tamperCapability,
|
|
38280
|
+
temperatureSensor: temperatureSensorCapability,
|
|
38281
|
+
update: updateCapability,
|
|
38282
|
+
vacuumControl: vacuumControlCapability,
|
|
38283
|
+
valve: valveCapability,
|
|
38284
|
+
vibration: vibrationCapability,
|
|
38285
|
+
waterHeater: waterHeaterCapability,
|
|
38286
|
+
weather: weatherCapability,
|
|
38287
|
+
zoneAnalytics: zoneAnalyticsCapability,
|
|
38288
|
+
zoneRules: zoneRulesCapability,
|
|
38289
|
+
zones: zonesCapability
|
|
38290
|
+
};
|
|
38291
|
+
var BaseDevice = class {
|
|
38292
|
+
id;
|
|
38293
|
+
stableId;
|
|
38294
|
+
type;
|
|
38295
|
+
name;
|
|
38296
|
+
parentDeviceId;
|
|
38297
|
+
role;
|
|
38298
|
+
/**
|
|
38299
|
+
* Cap-keyed runtime-state slice is the single source of truth for
|
|
38300
|
+
* `online`. Both getter and setter proxy to the slice — drivers can
|
|
38301
|
+
* write `this.online = true` ergonomically, and the cap event fires
|
|
38302
|
+
* automatically through the runtime-state writer. `markOnline()` is
|
|
38303
|
+
* kept as the explicit method form mandated by `IDevice`.
|
|
38304
|
+
*/
|
|
38305
|
+
get online() {
|
|
38306
|
+
return this.runtimeState.getCapState("device-status")?.online ?? false;
|
|
38307
|
+
}
|
|
38308
|
+
set online(value) {
|
|
38309
|
+
this.markOnline(value);
|
|
38310
|
+
}
|
|
38311
|
+
/**
|
|
38312
|
+
* Generic per-cap runtime-state namespace. One entry per cap with
|
|
38313
|
+
* `runtimeState:` declared, auto-generated by codegen — see
|
|
38314
|
+
* `device-local-state.ts`. Drivers access via:
|
|
38315
|
+
*
|
|
38316
|
+
* `this.state.battery.sleeping = true` // patches the battery slice
|
|
38317
|
+
* `const pct = this.state.battery.percentage` // reads the battery slice
|
|
38318
|
+
* `this.state.deviceStatus.online = true` // mirrors `markOnline(true)`
|
|
38319
|
+
*
|
|
38320
|
+
* Adding a new cap with `runtimeState:` automatically extends this
|
|
38321
|
+
* namespace — drivers don't have to declare proxies. Reads return
|
|
38322
|
+
* `undefined` when the slice hasn't been seeded; writes patch via
|
|
38323
|
+
* `runtimeState.patchCapState` and validate against the cap's schema
|
|
38324
|
+
* (so partial writes need the slice to be seeded with the required
|
|
38325
|
+
* fields first — drivers do this on cap registration).
|
|
38326
|
+
*
|
|
38327
|
+
* For caps not exposed in `DeviceLocalState`, drivers can build their
|
|
38328
|
+
* own typed proxy via `this.sliceProxy(cap)`.
|
|
38329
|
+
*/
|
|
38330
|
+
get state() {
|
|
38331
|
+
if (!this._stateProxyCache) {
|
|
38332
|
+
const cache = {};
|
|
38333
|
+
const handler = { get: (_target, key) => {
|
|
38334
|
+
const k = key;
|
|
38335
|
+
if (k in cache) return cache[k];
|
|
38336
|
+
const cap = DEVICE_LOCAL_STATE_CAPS[k];
|
|
38337
|
+
if (!cap) return void 0;
|
|
38338
|
+
const proxy = this.sliceProxy(cap);
|
|
38339
|
+
cache[k] = proxy;
|
|
38340
|
+
return proxy;
|
|
38341
|
+
} };
|
|
38342
|
+
this._stateProxyCache = new Proxy(cache, handler);
|
|
38343
|
+
}
|
|
38344
|
+
return this._stateProxyCache;
|
|
38345
|
+
}
|
|
38346
|
+
_stateProxyCache;
|
|
38347
|
+
config;
|
|
38348
|
+
/**
|
|
38349
|
+
* Per-device runtime state, cap-keyed. Always installed — slices
|
|
38350
|
+
* for individual caps materialise as those caps register their
|
|
38351
|
+
* native providers (`ctx.registerNativeCap`). The cap's own
|
|
38352
|
+
* `runtimeState` schema is the source of truth for the slice
|
|
38353
|
+
* shape; drivers don't redeclare it, they just write through.
|
|
38354
|
+
*
|
|
38355
|
+
* Read: `this.runtimeState.getCapState('battery')` →
|
|
38356
|
+
* `{percentage, charging, sleeping, lastUpdated}` for any
|
|
38357
|
+
* provider that registers `batteryCapability`.
|
|
38358
|
+
* Write: `this.runtimeState.setCapState('battery', { … })`.
|
|
38359
|
+
*
|
|
38360
|
+
* Cross-process consumers reach this state through the
|
|
38361
|
+
* `deviceState` cap router (or via cap-specific events the driver
|
|
38362
|
+
* emits — e.g. `battery.onStatusChanged`). The local handle is
|
|
38363
|
+
* accessed in-process by the driver to avoid roundtrips.
|
|
38364
|
+
*/
|
|
38365
|
+
runtimeState;
|
|
38366
|
+
ctx;
|
|
38367
|
+
/**
|
|
38368
|
+
* Operator-organisational location label (room / area / zone).
|
|
38369
|
+
* Read from `ctx.deviceMeta.location`; mutated via
|
|
38370
|
+
* `kernel.devices.setLocation(id, value)`. Free-text — providers
|
|
38371
|
+
* don't interpret it; the UI groups devices by this for filters
|
|
38372
|
+
* like "show me all cameras in Kitchen". `null` when unset.
|
|
38373
|
+
*/
|
|
38374
|
+
location;
|
|
38375
|
+
/**
|
|
38376
|
+
* Soft-disabled flag. When `true`, the device class is still
|
|
38377
|
+
* instantiated and visible in the UI (so the operator can flip
|
|
38378
|
+
* back on without re-adding) but lifecycle hooks (publishToBroker,
|
|
38379
|
+
* alarm-stream subscribe, …) MUST be gated by the driver to skip
|
|
38380
|
+
* work. The `BaseDevice` enforces this by exposing the flag here;
|
|
38381
|
+
* it does NOT mutate cap behaviour automatically — drivers consult
|
|
38382
|
+
* `this.disabled` at the top of their lifecycle methods. Read from
|
|
38383
|
+
* `ctx.deviceMeta.disabled`; mutated via
|
|
38384
|
+
* `kernel.devices.setDisabled(id, value)`.
|
|
38385
|
+
*/
|
|
38386
|
+
disabled;
|
|
38387
|
+
/**
|
|
38388
|
+
* Cached materialised `SourceInfo` — either the value persisted under
|
|
38389
|
+
* `metadata.sourceInfo` at construction time, or a synthetic
|
|
38390
|
+
* `{ id: stableId, system: addonId }` for providers that haven't
|
|
38391
|
+
* migrated yet. Lazily populated on first `sourceInfo` read so the
|
|
38392
|
+
* cost of Zod-parsing the meta blob is paid once per device boot.
|
|
38393
|
+
* Invalidated by `updateSourceInfo()` so providers see the new value
|
|
38394
|
+
* back through the getter without a re-fetch from the meta surface.
|
|
38395
|
+
*/
|
|
38396
|
+
_sourceInfoCache = null;
|
|
38397
|
+
constructor(ctx, schema, options) {
|
|
38398
|
+
this.ctx = ctx;
|
|
38399
|
+
this.id = ctx.id;
|
|
38400
|
+
this.stableId = ctx.stableId;
|
|
38401
|
+
this.type = options.type;
|
|
38402
|
+
if (!ctx.deviceMeta) throw new Error(`BaseDevice constructor: ctx.deviceMeta is required (id=${ctx.id} stableId=${ctx.stableId})`);
|
|
38403
|
+
this.name = ctx.deviceMeta.name;
|
|
38404
|
+
this.location = ctx.deviceMeta.location;
|
|
38405
|
+
this.disabled = ctx.deviceMeta.disabled;
|
|
38406
|
+
this.role = options.role;
|
|
38407
|
+
this.parentDeviceId = ctx.parentDeviceId;
|
|
38408
|
+
const seedData = ctx.persistedConfig ?? {};
|
|
38409
|
+
this.config = DeviceConfig.fromSchema(schema, (data) => ctx.persistConfig(data), seedData, ({ droppedKeys, issues }) => {
|
|
38410
|
+
ctx.logger.warn("Device config recovery: dropping invalid persisted fields", {
|
|
38411
|
+
tags: {
|
|
38412
|
+
deviceId: ctx.id,
|
|
38413
|
+
stableId: ctx.stableId
|
|
38414
|
+
},
|
|
38415
|
+
meta: {
|
|
38416
|
+
droppedKeys: [...droppedKeys],
|
|
38417
|
+
firstIssue: issues[0]?.message ?? null
|
|
38418
|
+
}
|
|
38419
|
+
});
|
|
38420
|
+
});
|
|
38421
|
+
let cachedProxy = null;
|
|
38422
|
+
const writer = async (capName, slice) => {
|
|
38423
|
+
if (!cachedProxy) cachedProxy = ctx.fetchDevice(ctx.id);
|
|
38424
|
+
await (await cachedProxy).deviceState.setCapSlice({
|
|
38425
|
+
capName,
|
|
38426
|
+
slice
|
|
38427
|
+
});
|
|
38428
|
+
};
|
|
38429
|
+
const initial = ctx.initialRuntimeState ?? {};
|
|
38430
|
+
this.runtimeState = DeviceRuntimeState.fromInitial(initial, writer);
|
|
38431
|
+
ctx.bindRuntimeState?.(this.runtimeState);
|
|
38432
|
+
ctx.registerNativeCap?.(deviceStatusCapability, {});
|
|
38433
|
+
const seed = {
|
|
38434
|
+
online: true,
|
|
38435
|
+
lastChangedAt: Date.now()
|
|
38436
|
+
};
|
|
38437
|
+
this.runtimeState.setCapState("device-status", seed);
|
|
38438
|
+
ctx.registerNativeCap?.(featureProbeCapability, {});
|
|
38439
|
+
this.runtimeState.setCapState("feature-probe", {
|
|
38440
|
+
flags: {},
|
|
38441
|
+
deviceType: null,
|
|
38442
|
+
model: null,
|
|
38443
|
+
channelCount: null,
|
|
38444
|
+
lastProbedAt: 0,
|
|
38445
|
+
lastFetchedAt: 0
|
|
38446
|
+
});
|
|
38447
|
+
}
|
|
38448
|
+
deviceActions = /* @__PURE__ */ new Map();
|
|
38449
|
+
/** Declare a device custom action + its typed handler. Idempotent per name. */
|
|
38450
|
+
registerDeviceAction(name, spec, handler) {
|
|
38451
|
+
this.deviceActions.set(name, {
|
|
38452
|
+
spec,
|
|
38453
|
+
handler
|
|
38454
|
+
});
|
|
38455
|
+
}
|
|
38456
|
+
/** Invoke a registered device action. Validates input against the spec. */
|
|
38457
|
+
async runDeviceAction(action, input) {
|
|
38458
|
+
const entry = this.deviceActions.get(action);
|
|
38459
|
+
if (!entry) throw new Error(`unknown device action "${action}" on device ${this.id}`);
|
|
38460
|
+
const parsed = entry.spec.input.parse(input);
|
|
38461
|
+
return entry.handler(parsed);
|
|
38462
|
+
}
|
|
38463
|
+
async removeDevice() {}
|
|
38464
|
+
/**
|
|
38465
|
+
* Set the device's online flag. Called by `BaseDeviceProvider` after
|
|
38466
|
+
* aggregating per-profile stream-broker health, or directly by drivers
|
|
38467
|
+
* that have provider-side liveness signals (e.g. ONVIF heartbeats,
|
|
38468
|
+
* Reolink Baichuan firmware push events). Mirrors the new value into
|
|
38469
|
+
* the `device-status` runtime-state slice so cross-process consumers
|
|
38470
|
+
* pick it up via the standard cap-state channel. Subclasses can
|
|
38471
|
+
* override to gate side effects on the transition.
|
|
38472
|
+
*/
|
|
38473
|
+
markOnline(online) {
|
|
38474
|
+
if (this.online === online) return;
|
|
38475
|
+
const next = {
|
|
38476
|
+
online,
|
|
38477
|
+
lastChangedAt: Date.now()
|
|
38478
|
+
};
|
|
38479
|
+
this.runtimeState.setCapState("device-status", next);
|
|
38480
|
+
}
|
|
38481
|
+
/**
|
|
38482
|
+
* Upstream-system identity + rendering envelope for this device. See
|
|
38483
|
+
* `SourceInfo` for the field contract. Always returns a valid object:
|
|
38484
|
+
* if the persisted `metadata.sourceInfo` blob is absent or fails Zod
|
|
38485
|
+
* validation, falls back to a synthetic `{ id: stableId, system: addonId }`
|
|
38486
|
+
* so providers that haven't migrated keep working without code changes.
|
|
38487
|
+
*
|
|
38488
|
+
* The value is cached after the first read. `updateSourceInfo()`
|
|
38489
|
+
* invalidates the cache so subsequent reads see the new patch. The
|
|
38490
|
+
* returned object is frozen to prevent accidental in-place mutation —
|
|
38491
|
+
* use `updateSourceInfo({ patch })` to change fields.
|
|
38492
|
+
*/
|
|
38493
|
+
get sourceInfo() {
|
|
38494
|
+
if (this._sourceInfoCache) return this._sourceInfoCache;
|
|
38495
|
+
const resolved = extractSourceInfoFromMetadata(this.ctx.deviceMeta.metadata) ?? synthesizeSourceInfo({
|
|
38496
|
+
stableId: this.stableId,
|
|
38497
|
+
addonId: this.ctx.deviceMeta.addonId
|
|
38498
|
+
});
|
|
38499
|
+
this._sourceInfoCache = Object.freeze({ ...resolved });
|
|
38500
|
+
return this._sourceInfoCache;
|
|
38501
|
+
}
|
|
38502
|
+
/**
|
|
38503
|
+
* Convenience accessor for the upstream dispatch key. Equivalent to
|
|
38504
|
+
* `this.sourceInfo.id` — providers use this to keep a
|
|
38505
|
+
* `Map<sourceId, IDevice>` for routing inbound push events.
|
|
38506
|
+
*/
|
|
38507
|
+
get sourceId() {
|
|
38508
|
+
return this.sourceInfo.id;
|
|
38509
|
+
}
|
|
38510
|
+
/**
|
|
38511
|
+
* Patch the device's `SourceInfo`. Shallow-merges `patch` over the
|
|
38512
|
+
* current value, persists the merged result under
|
|
38513
|
+
* `metadata.sourceInfo` via the `device-manager.setMetadata` cap, and
|
|
38514
|
+
* emits `EventCategory.DeviceSourceInfoChanged` for live consumers.
|
|
38515
|
+
*
|
|
38516
|
+
* Safe to call from anywhere in the device's lifetime — the call is
|
|
38517
|
+
* idempotent for `undefined` patch values (ignored) and best-effort
|
|
38518
|
+
* for persistence (a transient device-manager error doesn't unwind
|
|
38519
|
+
* the local cache update, so subsequent reads still see the patch).
|
|
38520
|
+
*
|
|
38521
|
+
* Drivers populate this on adoption + on every metadata change push
|
|
38522
|
+
* from the upstream source. Subscribers (UI, export adapters) react
|
|
38523
|
+
* via the `DeviceSourceInfoChanged` event without polling.
|
|
38524
|
+
*/
|
|
38525
|
+
async updateSourceInfo(patch) {
|
|
38526
|
+
const next = mergeSourceInfo(this.sourceInfo, patch);
|
|
38527
|
+
this._sourceInfoCache = Object.freeze({ ...next });
|
|
38528
|
+
const action = this.ctx.api?.deviceManager?.setMetadata;
|
|
38529
|
+
if (action) try {
|
|
38530
|
+
await action.mutate({
|
|
38531
|
+
deviceId: this.id,
|
|
38532
|
+
patch: { [SOURCE_INFO_METADATA_KEY]: next }
|
|
38533
|
+
});
|
|
38534
|
+
} catch {}
|
|
38535
|
+
this.ctx.eventBus.emit(createEvent("device.source-info-changed", {
|
|
38536
|
+
type: "device",
|
|
38537
|
+
id: this.stableId
|
|
38538
|
+
}, {
|
|
38539
|
+
deviceId: this.id,
|
|
38540
|
+
sourceInfo: next
|
|
38541
|
+
}));
|
|
38542
|
+
}
|
|
38543
|
+
/**
|
|
38544
|
+
* Re-publish the device's current `features` array to the persisted
|
|
38545
|
+
* meta blob. Drivers call this after a probe finishes when the live
|
|
38546
|
+
* `features` getter has gained new flags (e.g. `hasIntercom` flips
|
|
38547
|
+
* to true → `DeviceFeature.TwoWayAudio` joins the list).
|
|
38548
|
+
*
|
|
38549
|
+
* Without this, only the construction-time snapshot is written —
|
|
38550
|
+
* `deviceManager.registerDevice` is invoked once per boot, so probe-
|
|
38551
|
+
* driven additions don't reach the persisted index until the next
|
|
38552
|
+
* server restart, and `getDevice` / `listAll` keep returning the
|
|
38553
|
+
* stale list for forked-worker devices (whose live IDevice instance
|
|
38554
|
+
* is invisible to the hub registry).
|
|
38555
|
+
*
|
|
38556
|
+
* Idempotent: re-calling with the same features just no-ops on the
|
|
38557
|
+
* persisted meta. Best-effort: lookup or write failures are logged
|
|
38558
|
+
* at debug and swallowed — the live `device.features` getter is
|
|
38559
|
+
* still authoritative within this process, so callers never block
|
|
38560
|
+
* device boot on a meta refresh.
|
|
38561
|
+
*/
|
|
38562
|
+
async refreshFeatures() {
|
|
38563
|
+
const action = this.ctx.api?.deviceManager?.registerDevice;
|
|
38564
|
+
if (!action) return;
|
|
38565
|
+
try {
|
|
38566
|
+
await action.mutate({
|
|
38567
|
+
addonId: this.ctx.deviceMeta.addonId,
|
|
38568
|
+
stableId: this.stableId,
|
|
38569
|
+
id: this.id,
|
|
38570
|
+
type: this.type,
|
|
38571
|
+
name: this.name,
|
|
38572
|
+
parentDeviceId: this.parentDeviceId,
|
|
38573
|
+
features: [...this.features],
|
|
38574
|
+
config: {}
|
|
38575
|
+
});
|
|
38576
|
+
} catch (err) {}
|
|
38577
|
+
}
|
|
38578
|
+
/**
|
|
38579
|
+
* Typed read-through to a cap-keyed runtime-state slice. Drivers
|
|
38580
|
+
* call `this.getCapSlice(batteryCapability)` and the return type
|
|
38581
|
+
* is inferred from the cap's `runtimeState` Zod schema — no string
|
|
38582
|
+
* key, no manual generic. Returns `null` when the slice hasn't
|
|
38583
|
+
* been written yet (e.g. driver hasn't seeded battery yet).
|
|
38584
|
+
*/
|
|
38585
|
+
getCapSlice(cap) {
|
|
38586
|
+
return this.runtimeState.getCapState(cap.name) ?? null;
|
|
38587
|
+
}
|
|
38588
|
+
/**
|
|
38589
|
+
* Typed writer to a cap-keyed runtime-state slice. Routes through
|
|
38590
|
+
* the runtime-state writer (validate → persist → emit cap event).
|
|
38591
|
+
* Equivalent to `this.runtimeState.setCapState(cap.name, value)`
|
|
38592
|
+
* but with the cap's `runtimeState` schema enforcing the value
|
|
38593
|
+
* shape at compile time. Mirrors the symmetry of
|
|
38594
|
+
* `getCapSlice` / `setCapSlice` for cross-cap consistency.
|
|
38595
|
+
*/
|
|
38596
|
+
setCapSlice(cap, value) {
|
|
38597
|
+
this.runtimeState.setCapState(cap.name, value);
|
|
38598
|
+
}
|
|
38599
|
+
/**
|
|
38600
|
+
* Field-level read/write proxy over a cap's runtime-state slice.
|
|
38601
|
+
* Drivers that want ergonomic per-field access declare:
|
|
38602
|
+
*
|
|
38603
|
+
* ```ts
|
|
38604
|
+
* protected battery = this.sliceProxy(batteryCapability)
|
|
38605
|
+
* // …
|
|
38606
|
+
* this.battery.sleeping = true // patches the slice
|
|
38607
|
+
* const charging = this.battery.charging // reads the slice
|
|
38608
|
+
* ```
|
|
38609
|
+
*
|
|
38610
|
+
* Reads return `undefined` when the slice hasn't been seeded yet
|
|
38611
|
+
* (cap not registered, or seeded but the field is absent). Writes
|
|
38612
|
+
* route through `runtimeState.patchCapState` so the cap's `runtimeState`
|
|
38613
|
+
* schema validates the merged result and the cap event fires.
|
|
38614
|
+
*
|
|
38615
|
+
* Pattern is generic — same shape works for `battery`, `device-status`,
|
|
38616
|
+
* `motion`, `doorbell`, anything with a `runtimeState:` schema. Drivers
|
|
38617
|
+
* declare one proxy per cap they read/write directly.
|
|
38618
|
+
*/
|
|
38619
|
+
sliceProxy(cap) {
|
|
38620
|
+
return new Proxy({}, {
|
|
38621
|
+
get: (_, key) => {
|
|
38622
|
+
return this.runtimeState.getCapState(cap.name)?.[key];
|
|
38623
|
+
},
|
|
38624
|
+
set: (_, key, value) => {
|
|
38625
|
+
this.runtimeState.patchCapState(cap.name, { [key]: value });
|
|
38626
|
+
return true;
|
|
38627
|
+
},
|
|
38628
|
+
has: (_, key) => {
|
|
38629
|
+
const slice = this.runtimeState.getCapState(cap.name);
|
|
38630
|
+
return slice ? key in slice : false;
|
|
38631
|
+
},
|
|
38632
|
+
ownKeys: () => {
|
|
38633
|
+
const slice = this.runtimeState.getCapState(cap.name);
|
|
38634
|
+
return slice ? Object.keys(slice) : [];
|
|
38635
|
+
},
|
|
38636
|
+
getOwnPropertyDescriptor: (_, key) => {
|
|
38637
|
+
const slice = this.runtimeState.getCapState(cap.name);
|
|
38638
|
+
if (!slice || !(key in slice)) return void 0;
|
|
38639
|
+
return {
|
|
38640
|
+
configurable: true,
|
|
38641
|
+
enumerable: true,
|
|
38642
|
+
value: slice[key]
|
|
38643
|
+
};
|
|
38644
|
+
}
|
|
38645
|
+
});
|
|
38646
|
+
}
|
|
38647
|
+
/**
|
|
38648
|
+
* Default empty settings UI. Drivers override this to expose an
|
|
38649
|
+
* editable form in the device-details page. Returning an empty sections
|
|
38650
|
+
* array signals "nothing to contribute" — the aggregator drops the
|
|
38651
|
+
* contribution entirely rather than rendering a blank panel.
|
|
38652
|
+
*/
|
|
38653
|
+
getSettingsUISchema() {
|
|
38654
|
+
return { sections: [] };
|
|
38655
|
+
}
|
|
38656
|
+
/**
|
|
38657
|
+
* Default write path: forward the flat patch directly to storage.
|
|
38658
|
+
* Drivers that project a UI shape different from storage (e.g. `RtspCamera`
|
|
38659
|
+
* exposing `mainStreamUrl`/`subStreamUrl` over `streams[]`) override this
|
|
38660
|
+
* to reshape before `config.setAll`.
|
|
38661
|
+
*/
|
|
38662
|
+
async applySettingsPatch(patch) {
|
|
38663
|
+
await this.config.setAll(patch);
|
|
38664
|
+
}
|
|
38665
|
+
/**
|
|
38666
|
+
* Phase 3 — populate device-scoped state needed by downstream phases
|
|
38667
|
+
* (accessory reconciliation, public `features` array, optional cap
|
|
38668
|
+
* registration). Called ONCE per construction, after register but
|
|
38669
|
+
* before `getAccessoryChildren()`.
|
|
38670
|
+
*
|
|
38671
|
+
* Drivers write the `feature-probe` runtime-state slice via
|
|
38672
|
+
* `this.runtimeState.setCapState('feature-probe', {...})` — flag bag
|
|
38673
|
+
* is open (Reolink writes `hasPtz/hasIntercom`, Hikvision writes
|
|
38674
|
+
* `hasSupplementalLight/hasAlarmIo`, etc).
|
|
38675
|
+
*
|
|
38676
|
+
* Default: nothing to probe → mark the device PROBED (set `lastProbedAt`) so
|
|
38677
|
+
* the kernel treats it as ready immediately. A device that derives its shape
|
|
38678
|
+
* from a spec (a container, or an accessory sensor) rather than from a
|
|
38679
|
+
* hardware probe has no probe to "complete"; without stamping `lastProbedAt`
|
|
38680
|
+
* it would look perpetually un-probed — logging "Initial probe did not
|
|
38681
|
+
* complete" on every boot and spinning a pointless retry chain. Drivers that
|
|
38682
|
+
* DO probe override this and write their own `feature-probe` slice (including
|
|
38683
|
+
* `lastProbedAt`) once their probe actually succeeds.
|
|
38684
|
+
*/
|
|
38685
|
+
async onProbe() {
|
|
38686
|
+
const base = this.runtimeState.getCapState("feature-probe") ?? {
|
|
38687
|
+
flags: {},
|
|
38688
|
+
deviceType: null,
|
|
38689
|
+
model: null,
|
|
38690
|
+
channelCount: null,
|
|
38691
|
+
lastProbedAt: 0,
|
|
38692
|
+
lastFetchedAt: 0
|
|
38693
|
+
};
|
|
38694
|
+
this.runtimeState.setCapState("feature-probe", {
|
|
38695
|
+
...base,
|
|
38696
|
+
lastProbedAt: Date.now()
|
|
38697
|
+
});
|
|
38698
|
+
}
|
|
38699
|
+
/**
|
|
38700
|
+
* Phase 5 — fired after the device + its accessories are registered.
|
|
38701
|
+
* Drivers publish streams to the broker, kick off background tasks,
|
|
38702
|
+
* or subscribe to lib events that need a fully-registered device id.
|
|
38703
|
+
*
|
|
38704
|
+
* Default: no-op.
|
|
38705
|
+
*
|
|
38706
|
+
* RENAMED FROM `onCreated` (which still exists for back-compat in this
|
|
38707
|
+
* pass). The new name reflects the post-probe, post-accessory contract.
|
|
38708
|
+
*/
|
|
38709
|
+
async onActivate() {}
|
|
38710
|
+
/**
|
|
38711
|
+
* Re-run the probe + reconcile accessories + refresh features meta.
|
|
38712
|
+
* Drivers call this when device-side state changes (battery cam wakes,
|
|
38713
|
+
* firmware update, manual operator trigger).
|
|
38714
|
+
*
|
|
38715
|
+
* The kernel injects `_kernelReprobe` on registration so this method
|
|
38716
|
+
* delegates to the same orchestrator that runs the boot-time phase
|
|
38717
|
+
* 3 + 4 sequence. Drivers should NOT override this — they override
|
|
38718
|
+
* `onProbe()` instead.
|
|
38719
|
+
*/
|
|
38720
|
+
async reprobe() {
|
|
38721
|
+
if (this._kernelReprobe) await this._kernelReprobe();
|
|
38722
|
+
else await this.onProbe();
|
|
38723
|
+
}
|
|
38724
|
+
/**
|
|
38725
|
+
* Kernel-injected callback that runs the full post-probe orchestration
|
|
38726
|
+
* (onProbe → registerDevice meta refresh → accessory reconciliation).
|
|
38727
|
+
* Set by `device-cap-proxy.register()`. Drivers should not touch this
|
|
38728
|
+
* directly — call `reprobe()` instead.
|
|
38729
|
+
*/
|
|
38730
|
+
_kernelReprobe;
|
|
38731
|
+
/**
|
|
38732
|
+
* Declare accessory child devices the kernel should auto-spawn
|
|
38733
|
+
* after `onProbe()` resolves. Each spec fully describes one child
|
|
38734
|
+
* — stableId suffix (deterministic per kind for restore-safety),
|
|
38735
|
+
* meta (type / name / location), config (initial blob the child
|
|
38736
|
+
* self-hydrates), and a factory that constructs the concrete
|
|
38737
|
+
* class with whatever closure-captured refs it needs (typically
|
|
38738
|
+
* `this` for the parent reference).
|
|
38739
|
+
*
|
|
38740
|
+
* The kernel handles the rest: allocateDeviceId, persistInitialConfig
|
|
38741
|
+
* (skipped on restore when the row already exists),
|
|
38742
|
+
* persistInitialMeta, createContext, factory invocation, register,
|
|
38743
|
+
* and recursive lifecycle (probe + accessories + activate).
|
|
38744
|
+
*
|
|
38745
|
+
* Implementations should derive children from
|
|
38746
|
+
* `this.runtimeState.getCapState('feature-probe')` (post-probe truth).
|
|
38747
|
+
* Drivers can use the `getProbeFlags()` helper to read the flag bag
|
|
38748
|
+
* with a typed cast.
|
|
38749
|
+
*
|
|
38750
|
+
* Default: no children.
|
|
38751
|
+
*/
|
|
38752
|
+
getAccessoryChildren() {
|
|
38753
|
+
return [];
|
|
38754
|
+
}
|
|
38755
|
+
/**
|
|
38756
|
+
* Read the current feature-probe flag bag with a typed cast. Helper
|
|
38757
|
+
* for `getAccessoryChildren()` and `features` getters that derive
|
|
38758
|
+
* outputs from the probe results.
|
|
38759
|
+
*/
|
|
38760
|
+
getProbeFlags() {
|
|
38761
|
+
return this.runtimeState.getCapState("feature-probe")?.flags ?? {};
|
|
38762
|
+
}
|
|
38763
|
+
/**
|
|
38764
|
+
* Returns true once `onProbe` has completed at least once
|
|
38765
|
+
* (`lastProbedAt > 0`). Drivers gate `getAccessoryChildren()` on this
|
|
38766
|
+
* to avoid spawning stale accessories on a fresh device whose probe
|
|
38767
|
+
* hasn't landed yet.
|
|
38768
|
+
*/
|
|
38769
|
+
hasProbed() {
|
|
38770
|
+
return (this.runtimeState.getCapState("feature-probe")?.lastProbedAt ?? 0) > 0;
|
|
38771
|
+
}
|
|
38772
|
+
};
|
|
37499
38773
|
/**
|
|
37500
38774
|
* THE derivation. One crop rectangle, one presence verdict — see D52 for why
|
|
37501
38775
|
* this repo insists a derived value has exactly one implementation.
|
|
@@ -47482,4 +48756,4 @@ function enumerateInferenceDevices(hw) {
|
|
|
47482
48756
|
return out;
|
|
47483
48757
|
}
|
|
47484
48758
|
//#endregion
|
|
47485
|
-
export {
|
|
48759
|
+
export { deviceStateCapability as $, streamQualityLabel as $t, StorageLocationModeSchema as A, resolveCapMount as An, normalizeUnit as At, bareAddonId as B, resolveMethodAuth as Bt, RecordingStatusSchema as C, hydrateSchema as Cn, logDestinationCapability as Ct, STREAM_PROFILE_META as D, parseJsonObject as Dn, mayWriteToLocation as Dt, STORAGE_ACCESS_FALLBACK as E, nodePin as En, mayReadLocation as Et, addonSettingsCapability as F, ptzAutotrackCapability as Ft, coreBlockAddonId as G, settingsStoreCapability as Gt, buildStreamParamsConfigSchema as H, scopesAllowAddon as Ht, addonWidgetsCapability as I, reducePoints as It, dataStoreProviderCapability as J, storageCapability as Jt, coreBlockIdFromAddonId as K, siteZoneKey as Kt, alertsCapability as L, resolveBucketMs as Lt, StorageMigrationJobSchema as M, sleep as Mn, parseStreamParamsFormPatch as Mt, UserRecordSchema as N, EventCategory as Nn, platformProbeCapability as Nt, ScopedTokenSchema as O, parseJsonUnknown as On, metricsProviderCapability as Ot, addonPagesCapability as P, procedureAuthKey as Pt, deviceManagerCapability as Q, streamCatalogCapability as Qt, authProviderCapability as R, resolveContainerPrimaryChild as Rt, RUNTIME_DEFAULTS as S, expandCapMethods as Sn, locationIconKey as St, SOURCE_DEVICE_TYPES as T, isEvent as Tn, looseSchema as Tt, cameraGridLayoutCapability as U, scopesAllowDeviceCap as Ut, batteryCapability as V, runtimeStatePolicyFor as Vt, collectSecretConfigKeys as W, scoreRuntimes as Wt, decodeVectorBase64 as X, storageOccupancyCapability as Xt, declarationOwnerNodeId as Y, storageMigrationCapability as Yt, deriveBatteryPresence as Z, storageProviderCapability as Zt, LOG_LEVEL_RANK as _, asNumber as _n, isVoidInput as _t, BaseDevice as a, buildFfmpegArgs as an, evaluateSensorEdge as at, METHOD_ACCESS_MAP as b, emitDownForOwnedCaps as bn, lifecycleJobSchema as bt, CAP_NAMES_WITH_STATUS as c, DEVICE_SETTINGS_CONTRIBUTION_METHODS as cn, filesystemBrowseCapability as ct, CoreBlockSchema as d, DeviceRole as dn, isArrayOutputSchema as dt, userManagementCapability as en, deviceStatusCapability as et, DEFAULT_FIRST_SIGHTING_FRESHNESS_MS as f, DeviceType as fn, isCollectionArrayMethod as ft, FIRST_LEVEL_MACRO_CLASSES as g, asJsonObject as gn, isSourceCap as gt, DeviceStatusSchema as h, WELL_KNOWN_TAB_MAP as hn, isSameAddonId as ht, ApiKeyRecordSchema as i, errMsg as in, enumerateSchemaFields as it, StorageLocationTypeSchema as j, scopeKey as jn, objectInputDeclaresAddonId as jt, StorageCleanupJobSchema as k, readinessKey as kn, modeMayWrite as kt, CORE_BLOCKS_ADDON_ID as l, DEVICE_STATUS_METHOD as ln, foldSnapshotByFunction as lt, DeclaredDevices as m, ReadinessTimeoutError as mn, isObjectInput as mt, AUDIO_ANALYSIS_CAP_NAME as n, vectorStoreCapability as nn, enumerateInferenceDevices as nt, BatteryStatusSchema as o, BaseAddon as on, evictionPolicyOfLocation as ot, DETECTION_PIPELINE_CAP_NAME as p, ReadinessRegistry as pn, isLocationEnabled as pt, coreBlocksCapability as q, snapshotCapability as qt, AlertSchema as r, withLocationMode as rn, enumerateItemArrayFields as rt, CAMERA_SWITCH_ORDER as s, DATAPLANE_SECRET_HEADER as sn, extractNestedAddonId as st, ALL_CAPABILITY_DEFINITIONS as t, vectorDimFromBase64 as tn, doorbellCapability as tt, CORE_BLOCK_ADDON_PREFIX as u, DeviceFeature as un, gbToBytes as ut, LegacyStorageLocationDefaultSchema as v, asString as vn, kebabToCamel as vt, SOURCE_CAPS as w, isDeviceConfigCap as wn, logLevelAtMost as wt, REDACTED_SECRET as x, emitReadiness as xn, localNetworkCapability as xt, LocationIconIdSchema as y, createEvent as yn, legacyModeOf as yt, backupCapability as z, resolveLocationMode as zt };
|