@camstack/addon-pipeline 1.2.46 → 1.2.48
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/audio-analyzer/index.js +1 -1
- package/dist/audio-analyzer/index.mjs +1 -1
- package/dist/detection-pipeline/index.js +2 -2
- package/dist/detection-pipeline/index.mjs +2 -2
- package/dist/{dist-CPfFUIl9.js → dist-CksKE1Sn.js} +1399 -1285
- package/dist/{dist-CUj9mdhh.mjs → dist-DoTw0NM3.mjs} +1399 -1285
- package/dist/{event-loop-stall-monitor-md__iwWh.mjs → event-loop-stall-monitor-CtktCf3u.mjs} +1 -1
- package/dist/{event-loop-stall-monitor-BGbgcrEN.js → event-loop-stall-monitor-DzdwBr4X.js} +1 -1
- package/dist/motion-wasm/index.js +1 -1
- package/dist/motion-wasm/index.mjs +1 -1
- package/dist/pipeline-runner/index.js +3 -3
- package/dist/pipeline-runner/index.mjs +3 -3
- package/dist/recorder/index.js +1 -1
- package/dist/recorder/index.mjs +1 -1
- package/dist/session-decode/decode-worker-child.js +1 -1
- package/dist/session-decode/decode-worker-child.mjs +1 -1
- package/dist/stream-broker/_stub.js +2 -2
- package/dist/stream-broker/{_virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-aUWVCncO.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_stream_broker_widgets-BNISghj9.mjs} +2 -2
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Cj2xMupA.mjs +26 -0
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-Czrsozsg.mjs +26 -0
- package/dist/stream-broker/{hostInit-CRwWRwJH.mjs → hostInit-DqD1qq8w.mjs} +2 -2
- package/dist/stream-broker/index.js +671 -101
- package/dist/stream-broker/index.mjs +671 -101
- package/dist/stream-broker/remoteEntry.js +1 -1
- package/dist/{worker-protocol-CD0M9YD7.mjs → worker-protocol-4_4L1QOe.mjs} +1 -1
- package/dist/{worker-protocol-hdTR-F5q.js → worker-protocol-CuEcTi66.js} +1 -1
- package/embed-dist/assets/{MaskShapeCanvas-DI4BY7W2-C8eGXtf-.js → MaskShapeCanvas-DI4BY7W2-EcWZ5m8e.js} +1 -1
- package/embed-dist/assets/{MotionZonesSettings-NcxxQN8r-vvEcAgxS.js → MotionZonesSettings-NcxxQN8r-CfiyCD3z.js} +1 -1
- package/embed-dist/assets/{PrivacyMaskSettings-APgPLF7p-BLhx7VI-.js → PrivacyMaskSettings-APgPLF7p-ESPy4XXk.js} +1 -1
- package/embed-dist/assets/index-C3FKV1x-.js +112 -0
- package/embed-dist/index.html +1 -1
- package/package.json +1 -1
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-DN2e2GJL.mjs +0 -26
- package/dist/stream-broker/_virtual_mf___mfe_internal__addon_stream_broker_widgets__loadShare___mf_0_camstack_mf_1_ui_mf_2_library__loadShare__.js-2D_L2y3g.mjs +0 -26
- package/embed-dist/assets/index-DvOgpqcE.js +0 -112
|
@@ -7133,7 +7133,7 @@ function sleep(ms) {
|
|
|
7133
7133
|
return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
|
|
7134
7134
|
}
|
|
7135
7135
|
//#endregion
|
|
7136
|
-
//#region ../types/dist/
|
|
7136
|
+
//#region ../types/dist/canonical-hash-rO1sRmEK.mjs
|
|
7137
7137
|
var AUDIO_ENCODER_BY_CODEC = {
|
|
7138
7138
|
opus: "libopus",
|
|
7139
7139
|
aac: "aac",
|
|
@@ -7538,37 +7538,6 @@ function invocationFromEncodeProfile(input) {
|
|
|
7538
7538
|
...input.audioSidecar !== void 0 ? { audioSidecar: input.audioSidecar } : {}
|
|
7539
7539
|
};
|
|
7540
7540
|
}
|
|
7541
|
-
/**
|
|
7542
|
-
* Deterministic SHA-256 hash of an arbitrary serialisable value. The
|
|
7543
|
-
* canonical form sorts object keys alphabetically at every depth so two
|
|
7544
|
-
* structurally-equal inputs with different key insertion orders produce
|
|
7545
|
-
* the same hash. Returns a 64-char lowercase hex digest.
|
|
7546
|
-
*
|
|
7547
|
-
* Used by export adapters (Alexa, HAP) to short-circuit re-discovery /
|
|
7548
|
-
* accessory-rebuild work when the upstream shape is byte-identical to
|
|
7549
|
-
* the last applied state — preventing user-visible "re-discovery"
|
|
7550
|
-
* notifications on every addon-runner respawn. Each respawn re-fires
|
|
7551
|
-
* `DeviceBindingsChanged` for every cap registration, which without
|
|
7552
|
-
* this guard would propagate redundant pushes.
|
|
7553
|
-
*
|
|
7554
|
-
* Note: this is a SYMPTOMATIC fix layered on top of the binding-change
|
|
7555
|
-
* subscription. The proper fix is a single "device ready" lifecycle
|
|
7556
|
-
* barrier so exports react only when the full cap set has landed —
|
|
7557
|
-
* tracked separately for post-HA-integration work.
|
|
7558
|
-
*/
|
|
7559
|
-
function canonicalHash(value) {
|
|
7560
|
-
const canonical = JSON.stringify(value, replaceWithSortedKeys);
|
|
7561
|
-
return createHash("sha256").update(canonical ?? "").digest("hex");
|
|
7562
|
-
}
|
|
7563
|
-
function replaceWithSortedKeys(_key, value) {
|
|
7564
|
-
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
7565
|
-
const obj = value;
|
|
7566
|
-
const out = {};
|
|
7567
|
-
for (const k of Object.keys(obj).toSorted()) out[k] = obj[k];
|
|
7568
|
-
return out;
|
|
7569
|
-
}
|
|
7570
|
-
return value;
|
|
7571
|
-
}
|
|
7572
7541
|
var DEFAULT_MAX_UNIT_BYTES = 16 * 1024 * 1024;
|
|
7573
7542
|
/** Header size for a normal box, and for one carrying a 64-bit `largesize`. */
|
|
7574
7543
|
var BOX_HEADER_BYTES = 8;
|
|
@@ -7741,6 +7710,37 @@ var Fmp4BoxSplitter = class {
|
|
|
7741
7710
|
return [];
|
|
7742
7711
|
}
|
|
7743
7712
|
};
|
|
7713
|
+
/**
|
|
7714
|
+
* Deterministic SHA-256 hash of an arbitrary serialisable value. The
|
|
7715
|
+
* canonical form sorts object keys alphabetically at every depth so two
|
|
7716
|
+
* structurally-equal inputs with different key insertion orders produce
|
|
7717
|
+
* the same hash. Returns a 64-char lowercase hex digest.
|
|
7718
|
+
*
|
|
7719
|
+
* Used by export adapters (Alexa, HAP) to short-circuit re-discovery /
|
|
7720
|
+
* accessory-rebuild work when the upstream shape is byte-identical to
|
|
7721
|
+
* the last applied state — preventing user-visible "re-discovery"
|
|
7722
|
+
* notifications on every addon-runner respawn. Each respawn re-fires
|
|
7723
|
+
* `DeviceBindingsChanged` for every cap registration, which without
|
|
7724
|
+
* this guard would propagate redundant pushes.
|
|
7725
|
+
*
|
|
7726
|
+
* Note: this is a SYMPTOMATIC fix layered on top of the binding-change
|
|
7727
|
+
* subscription. The proper fix is a single "device ready" lifecycle
|
|
7728
|
+
* barrier so exports react only when the full cap set has landed —
|
|
7729
|
+
* tracked separately for post-HA-integration work.
|
|
7730
|
+
*/
|
|
7731
|
+
function canonicalHash(value) {
|
|
7732
|
+
const canonical = JSON.stringify(value, replaceWithSortedKeys);
|
|
7733
|
+
return createHash("sha256").update(canonical ?? "").digest("hex");
|
|
7734
|
+
}
|
|
7735
|
+
function replaceWithSortedKeys(_key, value) {
|
|
7736
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
7737
|
+
const obj = value;
|
|
7738
|
+
const out = {};
|
|
7739
|
+
for (const k of Object.keys(obj).toSorted()) out[k] = obj[k];
|
|
7740
|
+
return out;
|
|
7741
|
+
}
|
|
7742
|
+
return value;
|
|
7743
|
+
}
|
|
7744
7744
|
//#endregion
|
|
7745
7745
|
//#region ../types/dist/err-msg-IQTHeDzc.mjs
|
|
7746
7746
|
/**
|
|
@@ -10017,6 +10017,69 @@ var StreamFormatSchema = _enum([
|
|
|
10017
10017
|
"mjpeg",
|
|
10018
10018
|
"rtsp"
|
|
10019
10019
|
]);
|
|
10020
|
+
/** A container `produceEventMedia` can emit. */
|
|
10021
|
+
var EventMediaKindSchema = _enum(["mp4", "gif"]);
|
|
10022
|
+
/**
|
|
10023
|
+
* One produced artifact, referenced by HANDLE.
|
|
10024
|
+
*
|
|
10025
|
+
* Never inline bytes: a produced clip is 200 KB–5 MB and every consumer of this
|
|
10026
|
+
* method is in another runner ([D9](../../../../docs/decisions/adr-0009.md),
|
|
10027
|
+
* [D18](../../../../docs/decisions/adr-0018.md) — cross-process media is fetched
|
|
10028
|
+
* on demand, compressed, by handle). `bytes` is here so a caller can decide
|
|
10029
|
+
* whether it wants the fetch at all.
|
|
10030
|
+
*/
|
|
10031
|
+
var EventMediaArtifactSchema = object({
|
|
10032
|
+
kind: EventMediaKindSchema,
|
|
10033
|
+
/** Opaque, single-camera, short-lived. Redeem with `fetchEventMedia`. */
|
|
10034
|
+
handle: string(),
|
|
10035
|
+
/**
|
|
10036
|
+
* The node holding the bytes — the ROUTING key for `fetchEventMedia`.
|
|
10037
|
+
*
|
|
10038
|
+
* `stream-broker` is a singleton cap and an unpinned call never leaves the
|
|
10039
|
+
* hub, so a handle produced on an agent's broker would be redeemed against
|
|
10040
|
+
* the hub's store and come back `null`. Same contract, same field name and
|
|
10041
|
+
* the same reason as `FrameHandleSchema.nodeId`: the producer stamps where it
|
|
10042
|
+
* lives and the consumer pins to it.
|
|
10043
|
+
*/
|
|
10044
|
+
nodeId: string(),
|
|
10045
|
+
mime: string(),
|
|
10046
|
+
bytes: number().int(),
|
|
10047
|
+
width: number().int(),
|
|
10048
|
+
height: number().int()
|
|
10049
|
+
});
|
|
10050
|
+
/**
|
|
10051
|
+
* What a production actually covered — the answer to the only question an
|
|
10052
|
+
* operator asks about a notification clip.
|
|
10053
|
+
*
|
|
10054
|
+
* `fromTs`/`toTs` are WALL CLOCK, derived from the ring's own packet timeline,
|
|
10055
|
+
* so a caller can state "this clip starts 4.1 s before the event" instead of
|
|
10056
|
+
* inferring it from a duration. A production whose `fromTs` is later than the
|
|
10057
|
+
* event is a production with no pre-roll, and that is exactly the defect this
|
|
10058
|
+
* method exists to make visible rather than plausible.
|
|
10059
|
+
*/
|
|
10060
|
+
var EventMediaCoverageSchema = object({
|
|
10061
|
+
fromTs: number(),
|
|
10062
|
+
toTs: number(),
|
|
10063
|
+
/** Encoded packets in the muxed window. */
|
|
10064
|
+
packets: number().int()
|
|
10065
|
+
});
|
|
10066
|
+
/**
|
|
10067
|
+
* The result of ONE cut, in every container the caller asked for.
|
|
10068
|
+
*
|
|
10069
|
+
* Every artifact in `media` came out of the SAME window of the SAME rendition —
|
|
10070
|
+
* that is the whole reason this is one method rather than one call per format.
|
|
10071
|
+
* A consumer attaching a gif and a video can no longer show two different
|
|
10072
|
+
* moments, because it never chose two sources.
|
|
10073
|
+
*/
|
|
10074
|
+
var EventMediaProductionSchema = object({
|
|
10075
|
+
media: array(EventMediaArtifactSchema).readonly(),
|
|
10076
|
+
coverage: EventMediaCoverageSchema,
|
|
10077
|
+
/** The rendition actually cut from — what the default or the fallback chose. */
|
|
10078
|
+
profile: CamProfileSchema,
|
|
10079
|
+
/** `copy` = the camera's own H.264, untouched. `encode` = re-encoded (H.265
|
|
10080
|
+
* source, a downscale, or a playback rate other than 1). */
|
|
10081
|
+
video: _enum(["copy", "encode"])
|
|
10082
|
+
});
|
|
10020
10083
|
var RtspRestreamEntrySchema = object({
|
|
10021
10084
|
brokerId: string(),
|
|
10022
10085
|
url: string(),
|
|
@@ -10446,6 +10509,88 @@ var streamBrokerCapability = {
|
|
|
10446
10509
|
kind: "mutation",
|
|
10447
10510
|
auth: "admin"
|
|
10448
10511
|
}),
|
|
10512
|
+
/**
|
|
10513
|
+
* THE production point for event media — one window, every container.
|
|
10514
|
+
*
|
|
10515
|
+
* `renderPreBufferClip` renders ONE format per call, so a consumer wanting a
|
|
10516
|
+
* gif and a video made two calls and got two windows. On 2026-08-08 the
|
|
10517
|
+
* operator received exactly that: a gif around a doorbell press and an mp4
|
|
10518
|
+
* of the six seconds after it. This method cuts once and derives the rest,
|
|
10519
|
+
* so "the attachments agree" is a property of the service instead of a
|
|
10520
|
+
* discipline every consumer has to keep.
|
|
10521
|
+
*
|
|
10522
|
+
* It is also the only place that owns the packets. The broker already
|
|
10523
|
+
* retains an AnnexB ring per camera (`setClipRetention`, D32) — the
|
|
10524
|
+
* prebuffer — so a production reaches BACKWARDS from the event with no
|
|
10525
|
+
* process to spawn and nothing kept warm. It waits out `postSeconds`, muxes
|
|
10526
|
+
* the window once (COPYING the camera's own H.264 whenever nothing is being
|
|
10527
|
+
* changed), derives any gif from that same mux, and returns HANDLES.
|
|
10528
|
+
*
|
|
10529
|
+
* Intended for every future consumer, not just notifications: a clips API, a
|
|
10530
|
+
* retrain export and an event export all want the same window of the same
|
|
10531
|
+
* camera and must not each grow their own renderer.
|
|
10532
|
+
*/
|
|
10533
|
+
produceEventMedia: method(object({
|
|
10534
|
+
deviceId: number(),
|
|
10535
|
+
/** Absent = the largest H.264 rendition at or below 1080p, which is
|
|
10536
|
+
* also the one that can be copied. Falls back to whatever the ring
|
|
10537
|
+
* actually retained, and the answer says which. */
|
|
10538
|
+
profile: CamProfileSchema.optional(),
|
|
10539
|
+
aroundMs: number(),
|
|
10540
|
+
preSeconds: number().min(0).max(20).default(4),
|
|
10541
|
+
postSeconds: number().min(0).max(20).default(6),
|
|
10542
|
+
kinds: array(EventMediaKindSchema).min(1).default(["mp4"]),
|
|
10543
|
+
/** GIF geometry. The video keeps the source's own. */
|
|
10544
|
+
gifMaxWidth: number().int().min(120).max(1280).default(640),
|
|
10545
|
+
/**
|
|
10546
|
+
* The gif's own PLAYBACK rate in frames per second — what the finished
|
|
10547
|
+
* gif runs at, not how many source frames feed it. The decimation that
|
|
10548
|
+
* feeds it samples `gifFps / gifSpeed` source frames per second, so at
|
|
10549
|
+
* the defaults a 12 fps gif is built out of 3 source frames a second.
|
|
10550
|
+
*/
|
|
10551
|
+
gifFps: number().int().min(1).max(15).default(12),
|
|
10552
|
+
/**
|
|
10553
|
+
* How fast the GIF plays against real time, independent of `speed`.
|
|
10554
|
+
*
|
|
10555
|
+
* 4× by default, by operator request: a notification gif is glanced at
|
|
10556
|
+
* on a lock screen, so a ~12 s window has to be over in ~3 s. It stays
|
|
10557
|
+
* a separate knob from `speed` even though both now default to 4 —
|
|
10558
|
+
* a caller wanting a real-time video and a fast gif must not have to
|
|
10559
|
+
* choose.
|
|
10560
|
+
*/
|
|
10561
|
+
gifSpeed: number().min(1).max(8).default(4),
|
|
10562
|
+
/**
|
|
10563
|
+
* Playback rate of the VIDEO. Also 4× by default, by operator decision.
|
|
10564
|
+
*
|
|
10565
|
+
* `1` is real time and is the ONLY value that allows the copy branch —
|
|
10566
|
+
* anything else forces `libx264` over the window. That was priced
|
|
10567
|
+
* before it was chosen: a per-event burst measured at 0.23 s and 254 KB
|
|
10568
|
+
* on a real 615 720p cut, against 922 KB for the copy it replaces. A
|
|
10569
|
+
* re-encode is capped at 720p (`EVENT_CLIP_ENCODE_MAX_WIDTH`), because
|
|
10570
|
+
* once the decode is forced the width stops being free.
|
|
10571
|
+
*/
|
|
10572
|
+
speed: number().min(1).max(8).default(4)
|
|
10573
|
+
}), EventMediaProductionSchema, {
|
|
10574
|
+
kind: "mutation",
|
|
10575
|
+
auth: "admin"
|
|
10576
|
+
}),
|
|
10577
|
+
/**
|
|
10578
|
+
* Redeem a {@link EventMediaArtifactSchema} handle for its bytes.
|
|
10579
|
+
*
|
|
10580
|
+
* Separate from the production on purpose (D18): the producing runner keeps
|
|
10581
|
+
* the artifact for a few minutes, and a consumer that only wanted the
|
|
10582
|
+
* coverage never moves a megabyte. `null` once it has expired — a handle is
|
|
10583
|
+
* short-lived by design and a caller that waited too long must see that
|
|
10584
|
+
* rather than a zero-length file.
|
|
10585
|
+
*/
|
|
10586
|
+
fetchEventMedia: method(object({ handle: string() }), object({
|
|
10587
|
+
base64: string(),
|
|
10588
|
+
mime: string(),
|
|
10589
|
+
bytes: number().int()
|
|
10590
|
+
}).nullable(), {
|
|
10591
|
+
kind: "mutation",
|
|
10592
|
+
auth: "admin"
|
|
10593
|
+
}),
|
|
10449
10594
|
listAllCameraStreams: method(_void(), array(CameraStreamSchema).readonly()),
|
|
10450
10595
|
listAllProfileSlots: method(_void(), array(ProfileSlotSchema).readonly()),
|
|
10451
10596
|
getBrokerStats: method(object({ brokerId: string() }), BrokerStatsSchema),
|
|
@@ -10875,25 +11020,6 @@ var cameraStreamsCapability = {
|
|
|
10875
11020
|
lastChangedAt: number()
|
|
10876
11021
|
})
|
|
10877
11022
|
};
|
|
10878
|
-
/**
|
|
10879
|
-
* core-blocks — user-authored TypeScript, stored in the kernel and executed in
|
|
10880
|
-
* its own process.
|
|
10881
|
-
*
|
|
10882
|
-
* Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
|
|
10883
|
-
*
|
|
10884
|
-
* The first use is **owning devices without being a device provider**: a block
|
|
10885
|
-
* declares devices under a system or custom integration and drives their state,
|
|
10886
|
-
* with the same `ctx` an addon gets. Automations come later; nothing here
|
|
10887
|
-
* models a trigger.
|
|
10888
|
-
*
|
|
10889
|
-
* **Stated plainly, because it does not change by being true:** a block has an
|
|
10890
|
-
* addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
|
|
10891
|
-
* with no review step. What makes that survivable is not a sandbox, it is
|
|
10892
|
-
* PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
|
|
10893
|
-
* so a block that throws or never returns is marked `failed` and visible
|
|
10894
|
-
* instead of taking the hub with it (D6). Every method here is admin-only, and
|
|
10895
|
-
* must stay so.
|
|
10896
|
-
*/
|
|
10897
11023
|
/** Where a block runs. The operator chooses — a block driving a device on an
|
|
10898
11024
|
* agent is the reason placement is not fixed to the hub. */
|
|
10899
11025
|
var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
|
|
@@ -10965,6 +11091,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
10965
11091
|
}), object({ block: CoreBlockSchema }), {
|
|
10966
11092
|
kind: "mutation",
|
|
10967
11093
|
auth: "admin"
|
|
11094
|
+
}), method(object({ blockId: string() }), object({ block: CoreBlockSchema }), {
|
|
11095
|
+
kind: "mutation",
|
|
11096
|
+
auth: "admin"
|
|
10968
11097
|
}), method(object({ code: string() }), CoreBlockCompileResultSchema, {
|
|
10969
11098
|
kind: "mutation",
|
|
10970
11099
|
auth: "admin"
|
|
@@ -11696,713 +11825,118 @@ var ExposeInputSchema = object({
|
|
|
11696
11825
|
});
|
|
11697
11826
|
var UnexposeInputSchema = object({ deviceId: string() });
|
|
11698
11827
|
method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), { kind: "mutation" }), method(UnexposeInputSchema, _void(), { kind: "mutation" });
|
|
11828
|
+
var ProviderStatusSchema = object({
|
|
11829
|
+
connected: boolean(),
|
|
11830
|
+
deviceCount: number(),
|
|
11831
|
+
error: string().optional()
|
|
11832
|
+
});
|
|
11833
|
+
object({
|
|
11834
|
+
externalId: string(),
|
|
11835
|
+
name: string(),
|
|
11836
|
+
type: string(),
|
|
11837
|
+
metadata: record(string(), unknown()).optional()
|
|
11838
|
+
});
|
|
11699
11839
|
/**
|
|
11700
|
-
*
|
|
11701
|
-
*
|
|
11702
|
-
*
|
|
11703
|
-
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
11704
|
-
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
11705
|
-
* work a single author-supplied expression can request, so a hostile or
|
|
11706
|
-
* accidental pathological string can never spend unbounded CPU/memory.
|
|
11840
|
+
* Candidate handed back from discovery and accepted by
|
|
11841
|
+
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
11842
|
+
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
11707
11843
|
*/
|
|
11708
|
-
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
/**
|
|
11714
|
-
*
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11844
|
+
var DiscoveryCandidateSchema = object({
|
|
11845
|
+
stableId: string(),
|
|
11846
|
+
type: _enum(DeviceType),
|
|
11847
|
+
suggestedName: string(),
|
|
11848
|
+
prefilledConfig: record(string(), unknown()),
|
|
11849
|
+
/**
|
|
11850
|
+
* Optional upstream-system identity (HA entity_id, vendor MAC, …).
|
|
11851
|
+
* Discovery pre-populates this for systems that know the upstream
|
|
11852
|
+
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
11853
|
+
* flows live through the cap STATUS SLICE after adoption.
|
|
11854
|
+
*/
|
|
11855
|
+
sourceInfo: SourceInfoSchema.optional()
|
|
11856
|
+
});
|
|
11721
11857
|
/**
|
|
11722
|
-
*
|
|
11723
|
-
*
|
|
11724
|
-
*
|
|
11858
|
+
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
11859
|
+
* Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
|
|
11860
|
+
* tRPC layer can pass it through without reshaping.
|
|
11725
11861
|
*/
|
|
11726
|
-
|
|
11727
|
-
|
|
11728
|
-
|
|
11729
|
-
|
|
11730
|
-
|
|
11731
|
-
|
|
11732
|
-
|
|
11733
|
-
|
|
11734
|
-
|
|
11735
|
-
|
|
11736
|
-
/**
|
|
11737
|
-
*
|
|
11738
|
-
|
|
11739
|
-
|
|
11740
|
-
|
|
11741
|
-
|
|
11742
|
-
}
|
|
11743
|
-
};
|
|
11862
|
+
var DeviceSummarySchema = object({
|
|
11863
|
+
id: number(),
|
|
11864
|
+
stableId: string(),
|
|
11865
|
+
addonId: string(),
|
|
11866
|
+
type: string(),
|
|
11867
|
+
name: string(),
|
|
11868
|
+
parentDeviceId: number().nullable(),
|
|
11869
|
+
online: boolean(),
|
|
11870
|
+
features: array(string()),
|
|
11871
|
+
config: record(string(), unknown()),
|
|
11872
|
+
/** Optional upstream-system identity (dispatch key + system tag).
|
|
11873
|
+
* See `SourceInfo`. Present when the device has a non-synthetic
|
|
11874
|
+
* source identifier (HA entities, vendor MAC, …); omitted when the
|
|
11875
|
+
* synthetic backfill is in effect. */
|
|
11876
|
+
sourceInfo: SourceInfoSchema.optional()
|
|
11877
|
+
});
|
|
11744
11878
|
/**
|
|
11745
|
-
*
|
|
11746
|
-
*
|
|
11747
|
-
*
|
|
11748
|
-
*
|
|
11749
|
-
*
|
|
11750
|
-
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
11751
|
-
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
11752
|
-
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
11753
|
-
* (there is no `Object.prototype` in the chain), so those names are not
|
|
11754
|
-
* callable — they are simply "unknown function" at parse time.
|
|
11755
|
-
*
|
|
11756
|
-
* Every numeric argument is validated as a finite number and every numeric
|
|
11757
|
-
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
11758
|
-
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
11759
|
-
* closed rather than emitting a garbage value.
|
|
11879
|
+
* Result of a live field test (e.g. probing an RTSP URL during device
|
|
11880
|
+
* creation). Matches the UI-side `FieldProbeResult` in
|
|
11881
|
+
* `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
|
|
11882
|
+
* returned `labels` as chips next to the input.
|
|
11760
11883
|
*/
|
|
11761
|
-
|
|
11762
|
-
|
|
11763
|
-
|
|
11764
|
-
|
|
11765
|
-
|
|
11766
|
-
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
11767
|
-
return value;
|
|
11768
|
-
}
|
|
11769
|
-
function finiteResult(value, name) {
|
|
11770
|
-
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
11771
|
-
return value;
|
|
11772
|
-
}
|
|
11773
|
-
function allFiniteNumbers(args, name) {
|
|
11774
|
-
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
11775
|
-
}
|
|
11776
|
-
var INF = Number.POSITIVE_INFINITY;
|
|
11777
|
-
var table = {
|
|
11778
|
-
min: {
|
|
11779
|
-
minArgs: 1,
|
|
11780
|
-
maxArgs: INF,
|
|
11781
|
-
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
11782
|
-
},
|
|
11783
|
-
max: {
|
|
11784
|
-
minArgs: 1,
|
|
11785
|
-
maxArgs: INF,
|
|
11786
|
-
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
11787
|
-
},
|
|
11788
|
-
abs: {
|
|
11789
|
-
minArgs: 1,
|
|
11790
|
-
maxArgs: 1,
|
|
11791
|
-
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
11792
|
-
},
|
|
11793
|
-
floor: {
|
|
11794
|
-
minArgs: 1,
|
|
11795
|
-
maxArgs: 1,
|
|
11796
|
-
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
11797
|
-
},
|
|
11798
|
-
ceil: {
|
|
11799
|
-
minArgs: 1,
|
|
11800
|
-
maxArgs: 1,
|
|
11801
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
11802
|
-
},
|
|
11803
|
-
sqrt: {
|
|
11804
|
-
minArgs: 1,
|
|
11805
|
-
maxArgs: 1,
|
|
11806
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
11807
|
-
},
|
|
11808
|
-
round: {
|
|
11809
|
-
minArgs: 1,
|
|
11810
|
-
maxArgs: 2,
|
|
11811
|
-
apply: (args) => {
|
|
11812
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
11813
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
11814
|
-
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
11815
|
-
const factor = 10 ** digits;
|
|
11816
|
-
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
11817
|
-
}
|
|
11818
|
-
},
|
|
11819
|
-
pow: {
|
|
11820
|
-
minArgs: 2,
|
|
11821
|
-
maxArgs: 2,
|
|
11822
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
11823
|
-
},
|
|
11824
|
-
clamp: {
|
|
11825
|
-
minArgs: 3,
|
|
11826
|
-
maxArgs: 3,
|
|
11827
|
-
apply: (args) => {
|
|
11828
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
11829
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
11830
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
11831
|
-
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
11832
|
-
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
11833
|
-
}
|
|
11834
|
-
},
|
|
11835
|
-
avg: {
|
|
11836
|
-
minArgs: 1,
|
|
11837
|
-
maxArgs: INF,
|
|
11838
|
-
apply: (args) => {
|
|
11839
|
-
const nums = allFiniteNumbers(args, "avg");
|
|
11840
|
-
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
11841
|
-
}
|
|
11842
|
-
},
|
|
11843
|
-
sum: {
|
|
11844
|
-
minArgs: 1,
|
|
11845
|
-
maxArgs: INF,
|
|
11846
|
-
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
11847
|
-
},
|
|
11848
|
-
coalesce: {
|
|
11849
|
-
minArgs: 1,
|
|
11850
|
-
maxArgs: INF,
|
|
11851
|
-
apply: (args) => {
|
|
11852
|
-
for (const a of args) if (a !== null) return a;
|
|
11853
|
-
return null;
|
|
11854
|
-
}
|
|
11855
|
-
},
|
|
11856
|
-
age: {
|
|
11857
|
-
minArgs: 2,
|
|
11858
|
-
maxArgs: 2,
|
|
11859
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
11860
|
-
},
|
|
11861
|
-
convert: {
|
|
11862
|
-
minArgs: 3,
|
|
11863
|
-
maxArgs: 3,
|
|
11864
|
-
apply: (args, hooks) => {
|
|
11865
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
11866
|
-
const from = asString$1(args[1], "convert", 1).trim();
|
|
11867
|
-
const to = asString$1(args[2], "convert", 2).trim();
|
|
11868
|
-
if (hooks.convert) {
|
|
11869
|
-
const out = hooks.convert(x, from, to);
|
|
11870
|
-
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
11871
|
-
return finiteResult(out, "convert");
|
|
11872
|
-
}
|
|
11873
|
-
if (from === to) return x;
|
|
11874
|
-
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
11875
|
-
}
|
|
11876
|
-
}
|
|
11877
|
-
};
|
|
11878
|
-
Object.freeze(Object.assign(Object.create(null), table));
|
|
11879
|
-
/** The set of valid builtin names — used by the parser to reject unknown
|
|
11880
|
-
* callees at parse time (immediate author feedback). */
|
|
11881
|
-
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
11882
|
-
/**
|
|
11883
|
-
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
11884
|
-
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
11885
|
-
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
11886
|
-
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
11887
|
-
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
11888
|
-
* is a parse error with a source position, so member access / assignment /
|
|
11889
|
-
* template literals are lexically impossible.
|
|
11890
|
-
*/
|
|
11891
|
-
var KEYWORDS = new Set([
|
|
11892
|
-
"true",
|
|
11893
|
-
"false",
|
|
11894
|
-
"null"
|
|
11895
|
-
]);
|
|
11896
|
-
function isDigit(ch) {
|
|
11897
|
-
return ch >= "0" && ch <= "9";
|
|
11898
|
-
}
|
|
11899
|
-
function isIdentStart(ch) {
|
|
11900
|
-
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
11901
|
-
}
|
|
11902
|
-
function isIdentPart(ch) {
|
|
11903
|
-
return isIdentStart(ch) || isDigit(ch);
|
|
11904
|
-
}
|
|
11905
|
-
function isWhitespace(ch) {
|
|
11906
|
-
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
11907
|
-
}
|
|
11908
|
-
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
11909
|
-
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
11910
|
-
* string. */
|
|
11911
|
-
function tokenize(source) {
|
|
11912
|
-
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
11913
|
-
const tokens = [];
|
|
11914
|
-
let i = 0;
|
|
11915
|
-
const n = source.length;
|
|
11916
|
-
while (i < n) {
|
|
11917
|
-
const ch = source[i];
|
|
11918
|
-
if (isWhitespace(ch)) {
|
|
11919
|
-
i += 1;
|
|
11920
|
-
continue;
|
|
11921
|
-
}
|
|
11922
|
-
if (isDigit(ch)) {
|
|
11923
|
-
const start = i;
|
|
11924
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
11925
|
-
if (i < n && source[i] === ".") {
|
|
11926
|
-
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
11927
|
-
i += 1;
|
|
11928
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
11929
|
-
}
|
|
11930
|
-
const text = source.slice(start, i);
|
|
11931
|
-
const value = Number(text);
|
|
11932
|
-
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
11933
|
-
tokens.push({
|
|
11934
|
-
type: "number",
|
|
11935
|
-
value,
|
|
11936
|
-
pos: start
|
|
11937
|
-
});
|
|
11938
|
-
continue;
|
|
11939
|
-
}
|
|
11940
|
-
if (ch === "'" || ch === "\"") {
|
|
11941
|
-
const quote = ch;
|
|
11942
|
-
const start = i;
|
|
11943
|
-
i += 1;
|
|
11944
|
-
let out = "";
|
|
11945
|
-
let closed = false;
|
|
11946
|
-
while (i < n) {
|
|
11947
|
-
const c = source[i];
|
|
11948
|
-
if (c === "\\") {
|
|
11949
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
11950
|
-
if (next === "\\" || next === "'" || next === "\"") {
|
|
11951
|
-
out += next;
|
|
11952
|
-
i += 2;
|
|
11953
|
-
continue;
|
|
11954
|
-
}
|
|
11955
|
-
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
11956
|
-
}
|
|
11957
|
-
if (c === quote) {
|
|
11958
|
-
closed = true;
|
|
11959
|
-
i += 1;
|
|
11960
|
-
break;
|
|
11961
|
-
}
|
|
11962
|
-
out += c;
|
|
11963
|
-
i += 1;
|
|
11964
|
-
}
|
|
11965
|
-
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
11966
|
-
tokens.push({
|
|
11967
|
-
type: "string",
|
|
11968
|
-
value: out,
|
|
11969
|
-
pos: start
|
|
11970
|
-
});
|
|
11971
|
-
continue;
|
|
11972
|
-
}
|
|
11973
|
-
if (isIdentStart(ch)) {
|
|
11974
|
-
const start = i;
|
|
11975
|
-
while (i < n && isIdentPart(source[i])) i += 1;
|
|
11976
|
-
const text = source.slice(start, i);
|
|
11977
|
-
if (KEYWORDS.has(text)) tokens.push({
|
|
11978
|
-
type: "keyword",
|
|
11979
|
-
keyword: keywordOf(text),
|
|
11980
|
-
pos: start
|
|
11981
|
-
});
|
|
11982
|
-
else tokens.push({
|
|
11983
|
-
type: "identifier",
|
|
11984
|
-
name: text,
|
|
11985
|
-
pos: start
|
|
11986
|
-
});
|
|
11987
|
-
continue;
|
|
11988
|
-
}
|
|
11989
|
-
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
11990
|
-
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
11991
|
-
tokens.push({
|
|
11992
|
-
type: "punct",
|
|
11993
|
-
punct: two,
|
|
11994
|
-
pos: i
|
|
11995
|
-
});
|
|
11996
|
-
i += 2;
|
|
11997
|
-
continue;
|
|
11998
|
-
}
|
|
11999
|
-
if (isSinglePunct(ch)) {
|
|
12000
|
-
tokens.push({
|
|
12001
|
-
type: "punct",
|
|
12002
|
-
punct: ch,
|
|
12003
|
-
pos: i
|
|
12004
|
-
});
|
|
12005
|
-
i += 1;
|
|
12006
|
-
continue;
|
|
12007
|
-
}
|
|
12008
|
-
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
12009
|
-
}
|
|
12010
|
-
tokens.push({
|
|
12011
|
-
type: "eof",
|
|
12012
|
-
pos: n
|
|
12013
|
-
});
|
|
12014
|
-
return tokens;
|
|
12015
|
-
}
|
|
12016
|
-
function keywordOf(text) {
|
|
12017
|
-
if (text === "true") return "true";
|
|
12018
|
-
if (text === "false") return "false";
|
|
12019
|
-
return "null";
|
|
12020
|
-
}
|
|
12021
|
-
function isSinglePunct(ch) {
|
|
12022
|
-
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
12023
|
-
}
|
|
11884
|
+
var FieldProbeResultSchema = object({
|
|
11885
|
+
status: _enum(["ok", "error"]),
|
|
11886
|
+
labels: array(string()).optional(),
|
|
11887
|
+
error: string().optional()
|
|
11888
|
+
});
|
|
12024
11889
|
/**
|
|
12025
|
-
*
|
|
12026
|
-
*
|
|
12027
|
-
*
|
|
12028
|
-
*
|
|
12029
|
-
*
|
|
12030
|
-
*
|
|
12031
|
-
* function is rejected immediately (author feedback) and a persisted expression
|
|
12032
|
-
* that references a since-removed builtin degrades at read.
|
|
12033
|
-
*
|
|
12034
|
-
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
12035
|
-
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
11890
|
+
* The output of `getChildCreationSchema` is a UI schema tree. We store
|
|
11891
|
+
* it as `unknown` at the capability layer — the router just passes it
|
|
11892
|
+
* through and the admin UI renders it via `FormBuilder`. The actual
|
|
11893
|
+
* type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
|
|
11894
|
+
* but we deliberately avoid a Zod mirror because the union is large and
|
|
11895
|
+
* not meant for runtime validation at this seam.
|
|
12036
11896
|
*/
|
|
12037
|
-
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
|
|
12042
|
-
|
|
12043
|
-
"
|
|
12044
|
-
"
|
|
12045
|
-
|
|
12046
|
-
"
|
|
12047
|
-
"
|
|
12048
|
-
|
|
12049
|
-
|
|
12050
|
-
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
12054
|
-
|
|
12055
|
-
|
|
12056
|
-
|
|
12057
|
-
|
|
12058
|
-
|
|
12059
|
-
|
|
12060
|
-
|
|
12061
|
-
|
|
12062
|
-
|
|
12063
|
-
identifiers = /* @__PURE__ */ new Set();
|
|
12064
|
-
callees = /* @__PURE__ */ new Set();
|
|
12065
|
-
constructor(tokens) {
|
|
12066
|
-
this.tokens = tokens;
|
|
12067
|
-
}
|
|
12068
|
-
parse() {
|
|
12069
|
-
const ast = this.parseTernary();
|
|
12070
|
-
const tok = this.peek();
|
|
12071
|
-
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
12072
|
-
return {
|
|
12073
|
-
ast,
|
|
12074
|
-
identifiers: this.identifiers,
|
|
12075
|
-
callees: this.callees,
|
|
12076
|
-
nodeCount: this.nodeCount
|
|
12077
|
-
};
|
|
12078
|
-
}
|
|
12079
|
-
peek() {
|
|
12080
|
-
return this.tokens[this.pos];
|
|
12081
|
-
}
|
|
12082
|
-
next() {
|
|
12083
|
-
return this.tokens[this.pos++];
|
|
12084
|
-
}
|
|
12085
|
-
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
12086
|
-
expectPunct(punct) {
|
|
12087
|
-
const tok = this.peek();
|
|
12088
|
-
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
12089
|
-
this.pos += 1;
|
|
12090
|
-
}
|
|
12091
|
-
matchPunct(punct) {
|
|
12092
|
-
const tok = this.peek();
|
|
12093
|
-
if (tok.type === "punct" && tok.punct === punct) {
|
|
12094
|
-
this.pos += 1;
|
|
12095
|
-
return true;
|
|
12096
|
-
}
|
|
12097
|
-
return false;
|
|
12098
|
-
}
|
|
12099
|
-
countNode() {
|
|
12100
|
-
this.nodeCount += 1;
|
|
12101
|
-
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
12102
|
-
}
|
|
12103
|
-
parseTernary() {
|
|
12104
|
-
const test = this.parseBinary(1);
|
|
12105
|
-
if (this.matchPunct("?")) {
|
|
12106
|
-
const consequent = this.parseTernary();
|
|
12107
|
-
this.expectPunct(":");
|
|
12108
|
-
const alternate = this.parseTernary();
|
|
12109
|
-
this.countNode();
|
|
12110
|
-
return {
|
|
12111
|
-
kind: "conditional",
|
|
12112
|
-
test,
|
|
12113
|
-
consequent,
|
|
12114
|
-
alternate
|
|
12115
|
-
};
|
|
12116
|
-
}
|
|
12117
|
-
return test;
|
|
12118
|
-
}
|
|
12119
|
-
parseBinary(minPrec) {
|
|
12120
|
-
let left = this.parseUnary();
|
|
12121
|
-
for (;;) {
|
|
12122
|
-
const tok = this.peek();
|
|
12123
|
-
if (tok.type !== "punct") break;
|
|
12124
|
-
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
12125
|
-
if (prec === void 0 || prec < minPrec) break;
|
|
12126
|
-
const op = tok.punct;
|
|
12127
|
-
this.pos += 1;
|
|
12128
|
-
const right = this.parseBinary(prec + 1);
|
|
12129
|
-
this.countNode();
|
|
12130
|
-
if (isLogicalOp(op)) left = {
|
|
12131
|
-
kind: "logical",
|
|
12132
|
-
op,
|
|
12133
|
-
left,
|
|
12134
|
-
right
|
|
12135
|
-
};
|
|
12136
|
-
else if (isBinaryOp(op)) left = {
|
|
12137
|
-
kind: "binary",
|
|
12138
|
-
op,
|
|
12139
|
-
left,
|
|
12140
|
-
right
|
|
12141
|
-
};
|
|
12142
|
-
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
12143
|
-
}
|
|
12144
|
-
return left;
|
|
12145
|
-
}
|
|
12146
|
-
parseUnary() {
|
|
12147
|
-
const tok = this.peek();
|
|
12148
|
-
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
12149
|
-
const op = tok.punct;
|
|
12150
|
-
this.pos += 1;
|
|
12151
|
-
const operand = this.parseUnary();
|
|
12152
|
-
this.countNode();
|
|
12153
|
-
return {
|
|
12154
|
-
kind: "unary",
|
|
12155
|
-
op,
|
|
12156
|
-
operand
|
|
12157
|
-
};
|
|
12158
|
-
}
|
|
12159
|
-
return this.parsePrimary();
|
|
12160
|
-
}
|
|
12161
|
-
parsePrimary() {
|
|
12162
|
-
const tok = this.next();
|
|
12163
|
-
switch (tok.type) {
|
|
12164
|
-
case "number":
|
|
12165
|
-
this.countNode();
|
|
12166
|
-
return {
|
|
12167
|
-
kind: "literal",
|
|
12168
|
-
value: tok.value
|
|
12169
|
-
};
|
|
12170
|
-
case "string":
|
|
12171
|
-
this.countNode();
|
|
12172
|
-
return {
|
|
12173
|
-
kind: "literal",
|
|
12174
|
-
value: tok.value
|
|
12175
|
-
};
|
|
12176
|
-
case "keyword":
|
|
12177
|
-
this.countNode();
|
|
12178
|
-
return {
|
|
12179
|
-
kind: "literal",
|
|
12180
|
-
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
12181
|
-
};
|
|
12182
|
-
case "identifier": {
|
|
12183
|
-
const nextTok = this.peek();
|
|
12184
|
-
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
12185
|
-
this.identifiers.add(tok.name);
|
|
12186
|
-
this.countNode();
|
|
12187
|
-
return {
|
|
12188
|
-
kind: "identifier",
|
|
12189
|
-
name: tok.name
|
|
12190
|
-
};
|
|
12191
|
-
}
|
|
12192
|
-
case "punct":
|
|
12193
|
-
if (tok.punct === "(") {
|
|
12194
|
-
const inner = this.parseTernary();
|
|
12195
|
-
this.expectPunct(")");
|
|
12196
|
-
return inner;
|
|
12197
|
-
}
|
|
12198
|
-
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
12199
|
-
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
12200
|
-
}
|
|
12201
|
-
}
|
|
12202
|
-
parseCall(callee, pos) {
|
|
12203
|
-
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
12204
|
-
this.expectPunct("(");
|
|
12205
|
-
const args = [];
|
|
12206
|
-
if (!this.matchPunct(")")) for (;;) {
|
|
12207
|
-
args.push(this.parseTernary());
|
|
12208
|
-
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
12209
|
-
if (this.matchPunct(",")) continue;
|
|
12210
|
-
this.expectPunct(")");
|
|
12211
|
-
break;
|
|
12212
|
-
}
|
|
12213
|
-
this.callees.add(callee);
|
|
12214
|
-
this.countNode();
|
|
12215
|
-
return {
|
|
12216
|
-
kind: "call",
|
|
12217
|
-
callee,
|
|
12218
|
-
args
|
|
12219
|
-
};
|
|
12220
|
-
}
|
|
12221
|
-
};
|
|
12222
|
-
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
12223
|
-
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
12224
|
-
function parseExpression(source) {
|
|
12225
|
-
return new Parser(tokenize(source)).parse();
|
|
12226
|
-
}
|
|
11897
|
+
var CreationSchemaOutputSchema = unknown();
|
|
11898
|
+
method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), ProviderStatusSchema), method(_void(), array(object({
|
|
11899
|
+
id: string(),
|
|
11900
|
+
name: string(),
|
|
11901
|
+
type: string()
|
|
11902
|
+
}))), method(object({}), boolean()), method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
11903
|
+
kind: "mutation",
|
|
11904
|
+
auth: "admin"
|
|
11905
|
+
}), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
11906
|
+
kind: "mutation",
|
|
11907
|
+
auth: "admin"
|
|
11908
|
+
}), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
|
|
11909
|
+
type: _enum(DeviceType),
|
|
11910
|
+
config: record(string(), unknown())
|
|
11911
|
+
}), DeviceSummarySchema, {
|
|
11912
|
+
kind: "mutation",
|
|
11913
|
+
auth: "admin"
|
|
11914
|
+
}), method(object({
|
|
11915
|
+
type: _enum(DeviceType),
|
|
11916
|
+
key: string(),
|
|
11917
|
+
value: unknown(),
|
|
11918
|
+
formValues: record(string(), unknown()).optional()
|
|
11919
|
+
}), FieldProbeResultSchema, {
|
|
11920
|
+
kind: "mutation",
|
|
11921
|
+
auth: "admin"
|
|
11922
|
+
});
|
|
12227
11923
|
/**
|
|
12228
|
-
*
|
|
12229
|
-
*
|
|
12230
|
-
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
12231
|
-
* one per read on a hot resolve path.
|
|
11924
|
+
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
11925
|
+
* live registry access, and all management operations into a single tRPC surface.
|
|
12232
11926
|
*
|
|
12233
|
-
*
|
|
12234
|
-
*
|
|
12235
|
-
*
|
|
12236
|
-
|
|
12237
|
-
|
|
12238
|
-
|
|
12239
|
-
|
|
12240
|
-
|
|
12241
|
-
|
|
12242
|
-
|
|
12243
|
-
|
|
12244
|
-
|
|
12245
|
-
|
|
12246
|
-
try {
|
|
12247
|
-
result = {
|
|
12248
|
-
ok: true,
|
|
12249
|
-
parsed: parseExpression(source)
|
|
12250
|
-
};
|
|
12251
|
-
} catch (err) {
|
|
12252
|
-
result = {
|
|
12253
|
-
ok: false,
|
|
12254
|
-
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
12255
|
-
};
|
|
12256
|
-
}
|
|
12257
|
-
cache.set(source, result);
|
|
12258
|
-
if (cache.size > 256) {
|
|
12259
|
-
const oldest = cache.keys().next().value;
|
|
12260
|
-
if (oldest !== void 0) cache.delete(oldest);
|
|
12261
|
-
}
|
|
12262
|
-
return result;
|
|
12263
|
-
}
|
|
12264
|
-
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
12265
|
-
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
12266
|
-
function compileExpressionSafe(source) {
|
|
12267
|
-
return getCached(source);
|
|
12268
|
-
}
|
|
12269
|
-
Object.freeze({});
|
|
12270
|
-
/**
|
|
12271
|
-
* Author-time validation. Returns `null` when the source is valid, else a
|
|
12272
|
-
* human-readable error message. Checks: the expression compiles; binding count
|
|
12273
|
-
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
12274
|
-
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
12275
|
-
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
12276
|
-
*/
|
|
12277
|
-
function validateExpressionSource(src) {
|
|
12278
|
-
const names = Object.keys(src.bindings);
|
|
12279
|
-
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
12280
|
-
for (const name of names) {
|
|
12281
|
-
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
12282
|
-
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
12283
|
-
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
12284
|
-
}
|
|
12285
|
-
const compiled = compileExpressionSafe(src.expr);
|
|
12286
|
-
if (!compiled.ok) return compiled.error;
|
|
12287
|
-
const bound = new Set(names);
|
|
12288
|
-
for (const id of compiled.parsed.identifiers) {
|
|
12289
|
-
if (id === "now") continue;
|
|
12290
|
-
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
12291
|
-
}
|
|
12292
|
-
return null;
|
|
12293
|
-
}
|
|
12294
|
-
var ProviderStatusSchema = object({
|
|
12295
|
-
connected: boolean(),
|
|
12296
|
-
deviceCount: number(),
|
|
12297
|
-
error: string().optional()
|
|
12298
|
-
});
|
|
12299
|
-
object({
|
|
12300
|
-
externalId: string(),
|
|
12301
|
-
name: string(),
|
|
12302
|
-
type: string(),
|
|
12303
|
-
metadata: record(string(), unknown()).optional()
|
|
12304
|
-
});
|
|
12305
|
-
/**
|
|
12306
|
-
* Candidate handed back from discovery and accepted by
|
|
12307
|
-
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
12308
|
-
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
12309
|
-
*/
|
|
12310
|
-
var DiscoveryCandidateSchema = object({
|
|
12311
|
-
stableId: string(),
|
|
12312
|
-
type: _enum(DeviceType),
|
|
12313
|
-
suggestedName: string(),
|
|
12314
|
-
prefilledConfig: record(string(), unknown()),
|
|
12315
|
-
/**
|
|
12316
|
-
* Optional upstream-system identity (HA entity_id, vendor MAC, …).
|
|
12317
|
-
* Discovery pre-populates this for systems that know the upstream
|
|
12318
|
-
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
12319
|
-
* flows live through the cap STATUS SLICE after adoption.
|
|
12320
|
-
*/
|
|
12321
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
12322
|
-
});
|
|
12323
|
-
/**
|
|
12324
|
-
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
12325
|
-
* Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
|
|
12326
|
-
* tRPC layer can pass it through without reshaping.
|
|
12327
|
-
*/
|
|
12328
|
-
var DeviceSummarySchema = object({
|
|
12329
|
-
id: number(),
|
|
12330
|
-
stableId: string(),
|
|
12331
|
-
addonId: string(),
|
|
12332
|
-
type: string(),
|
|
12333
|
-
name: string(),
|
|
12334
|
-
parentDeviceId: number().nullable(),
|
|
12335
|
-
online: boolean(),
|
|
12336
|
-
features: array(string()),
|
|
12337
|
-
config: record(string(), unknown()),
|
|
12338
|
-
/** Optional upstream-system identity (dispatch key + system tag).
|
|
12339
|
-
* See `SourceInfo`. Present when the device has a non-synthetic
|
|
12340
|
-
* source identifier (HA entities, vendor MAC, …); omitted when the
|
|
12341
|
-
* synthetic backfill is in effect. */
|
|
12342
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
12343
|
-
});
|
|
12344
|
-
/**
|
|
12345
|
-
* Result of a live field test (e.g. probing an RTSP URL during device
|
|
12346
|
-
* creation). Matches the UI-side `FieldProbeResult` in
|
|
12347
|
-
* `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
|
|
12348
|
-
* returned `labels` as chips next to the input.
|
|
12349
|
-
*/
|
|
12350
|
-
var FieldProbeResultSchema = object({
|
|
12351
|
-
status: _enum(["ok", "error"]),
|
|
12352
|
-
labels: array(string()).optional(),
|
|
12353
|
-
error: string().optional()
|
|
12354
|
-
});
|
|
12355
|
-
/**
|
|
12356
|
-
* The output of `getChildCreationSchema` is a UI schema tree. We store
|
|
12357
|
-
* it as `unknown` at the capability layer — the router just passes it
|
|
12358
|
-
* through and the admin UI renders it via `FormBuilder`. The actual
|
|
12359
|
-
* type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
|
|
12360
|
-
* but we deliberately avoid a Zod mirror because the union is large and
|
|
12361
|
-
* not meant for runtime validation at this seam.
|
|
12362
|
-
*/
|
|
12363
|
-
var CreationSchemaOutputSchema = unknown();
|
|
12364
|
-
method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), ProviderStatusSchema), method(_void(), array(object({
|
|
12365
|
-
id: string(),
|
|
12366
|
-
name: string(),
|
|
12367
|
-
type: string()
|
|
12368
|
-
}))), method(object({}), boolean()), method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
12369
|
-
kind: "mutation",
|
|
12370
|
-
auth: "admin"
|
|
12371
|
-
}), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
12372
|
-
kind: "mutation",
|
|
12373
|
-
auth: "admin"
|
|
12374
|
-
}), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
|
|
12375
|
-
type: _enum(DeviceType),
|
|
12376
|
-
config: record(string(), unknown())
|
|
12377
|
-
}), DeviceSummarySchema, {
|
|
12378
|
-
kind: "mutation",
|
|
12379
|
-
auth: "admin"
|
|
12380
|
-
}), method(object({
|
|
12381
|
-
type: _enum(DeviceType),
|
|
12382
|
-
key: string(),
|
|
12383
|
-
value: unknown(),
|
|
12384
|
-
formValues: record(string(), unknown()).optional()
|
|
12385
|
-
}), FieldProbeResultSchema, {
|
|
12386
|
-
kind: "mutation",
|
|
12387
|
-
auth: "admin"
|
|
12388
|
-
});
|
|
12389
|
-
/**
|
|
12390
|
-
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
12391
|
-
* live registry access, and all management operations into a single tRPC surface.
|
|
12392
|
-
*
|
|
12393
|
-
* Replaces:
|
|
12394
|
-
* - `device-persistence` capability (persistence methods absorbed here)
|
|
12395
|
-
* - `device-management.router.ts` (deleted in Phase 2)
|
|
12396
|
-
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
12397
|
-
*
|
|
12398
|
-
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
12399
|
-
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
12400
|
-
* - No nodeId routing needed — this is a pure hub singleton.
|
|
12401
|
-
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
12402
|
-
* - No shadow registry or cross-node aggregation required.
|
|
12403
|
-
*
|
|
12404
|
-
* Forked workers register devices back to the hub via `ctx.devices`
|
|
12405
|
-
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
11927
|
+
* Replaces:
|
|
11928
|
+
* - `device-persistence` capability (persistence methods absorbed here)
|
|
11929
|
+
* - `device-management.router.ts` (deleted in Phase 2)
|
|
11930
|
+
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
11931
|
+
*
|
|
11932
|
+
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
11933
|
+
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
11934
|
+
* - No nodeId routing needed — this is a pure hub singleton.
|
|
11935
|
+
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
11936
|
+
* - No shadow registry or cross-node aggregation required.
|
|
11937
|
+
*
|
|
11938
|
+
* Forked workers register devices back to the hub via `ctx.devices`
|
|
11939
|
+
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
12406
11940
|
*/
|
|
12407
11941
|
/** One child-placement directive on a container's `childLayout`. Structurally
|
|
12408
11942
|
* identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
|
|
@@ -12415,92 +11949,6 @@ var ChildLayoutEntrySchema = object({
|
|
|
12415
11949
|
order: number().optional(),
|
|
12416
11950
|
collapsed: boolean().optional()
|
|
12417
11951
|
});
|
|
12418
|
-
/** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
|
|
12419
|
-
* `device-management.ts`. Source is a union: a FIELD source copies a sibling
|
|
12420
|
-
* accessory's status field (`kind` optional/absent for wire compat); a
|
|
12421
|
-
* LITERAL source carries a per-device constant (no sibling is read); a
|
|
12422
|
-
* GLOBAL source (P2e) copies ANY device's status field, addressed by the
|
|
12423
|
-
* source device's full re-sync-stable `stableId`. */
|
|
12424
|
-
var DeviceLinkFieldSourceSchema = object({
|
|
12425
|
-
kind: literal("field").optional(),
|
|
12426
|
-
sourceKey: string(),
|
|
12427
|
-
cap: string(),
|
|
12428
|
-
fieldPath: string()
|
|
12429
|
-
});
|
|
12430
|
-
var DeviceLinkLiteralSourceSchema = object({
|
|
12431
|
-
kind: literal("literal"),
|
|
12432
|
-
value: union([
|
|
12433
|
-
string(),
|
|
12434
|
-
number(),
|
|
12435
|
-
boolean(),
|
|
12436
|
-
_null()
|
|
12437
|
-
])
|
|
12438
|
-
});
|
|
12439
|
-
var DeviceLinkGlobalSourceSchema = object({
|
|
12440
|
-
kind: literal("global"),
|
|
12441
|
-
sourceStableId: string(),
|
|
12442
|
-
cap: string(),
|
|
12443
|
-
fieldPath: string()
|
|
12444
|
-
});
|
|
12445
|
-
/** Expression source (Stage X): compute the target field from N named bindings
|
|
12446
|
-
* via the safe expression engine. Bindings are field | literal | global — never
|
|
12447
|
-
* another expression (no nesting). The `superRefine` runs the SAME author-time
|
|
12448
|
-
* validation as `validateExpressionSource` (compiles the expr, checks binding
|
|
12449
|
-
* names + identifier coverage) so every wire boundary that parses a DeviceLink
|
|
12450
|
-
* (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
|
|
12451
|
-
* Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
|
|
12452
|
-
var DeviceLinkExpressionSourceSchema = object({
|
|
12453
|
-
kind: literal("expression"),
|
|
12454
|
-
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
12455
|
-
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), union([
|
|
12456
|
-
DeviceLinkFieldSourceSchema,
|
|
12457
|
-
DeviceLinkLiteralSourceSchema,
|
|
12458
|
-
DeviceLinkGlobalSourceSchema
|
|
12459
|
-
]))
|
|
12460
|
-
}).superRefine((src, ctx) => {
|
|
12461
|
-
const err = validateExpressionSource(src);
|
|
12462
|
-
if (err !== null) ctx.addIssue({
|
|
12463
|
-
code: "custom",
|
|
12464
|
-
message: err,
|
|
12465
|
-
path: ["expr"]
|
|
12466
|
-
});
|
|
12467
|
-
});
|
|
12468
|
-
var DeviceLinkSchema = object({
|
|
12469
|
-
id: string(),
|
|
12470
|
-
source: union([
|
|
12471
|
-
DeviceLinkFieldSourceSchema,
|
|
12472
|
-
DeviceLinkLiteralSourceSchema,
|
|
12473
|
-
DeviceLinkGlobalSourceSchema,
|
|
12474
|
-
DeviceLinkExpressionSourceSchema
|
|
12475
|
-
]),
|
|
12476
|
-
target: object({
|
|
12477
|
-
cap: string(),
|
|
12478
|
-
fieldPath: string(),
|
|
12479
|
-
itemKey: string().optional()
|
|
12480
|
-
}),
|
|
12481
|
-
transform: discriminatedUnion("kind", [
|
|
12482
|
-
object({ kind: literal("identity") }),
|
|
12483
|
-
object({
|
|
12484
|
-
kind: literal("enum-map"),
|
|
12485
|
-
mapping: record(string(), union([
|
|
12486
|
-
string(),
|
|
12487
|
-
number(),
|
|
12488
|
-
boolean()
|
|
12489
|
-
])),
|
|
12490
|
-
fallback: union([
|
|
12491
|
-
string(),
|
|
12492
|
-
number(),
|
|
12493
|
-
boolean()
|
|
12494
|
-
]).optional()
|
|
12495
|
-
}),
|
|
12496
|
-
object({
|
|
12497
|
-
kind: literal("linear"),
|
|
12498
|
-
scale: number(),
|
|
12499
|
-
offset: number(),
|
|
12500
|
-
clamp: tuple([number(), number()]).readonly().optional()
|
|
12501
|
-
})
|
|
12502
|
-
]).optional()
|
|
12503
|
-
});
|
|
12504
11952
|
/** Cap-wire shape of a per-cap display refinement — mirrors
|
|
12505
11953
|
* `DeviceCapDisplayOverride` in `device-management.ts`. */
|
|
12506
11954
|
var DeviceCapDisplayOverrideSchema = object({
|
|
@@ -12580,8 +12028,6 @@ var DeviceInfoSchema = object({
|
|
|
12580
12028
|
* named accordion sections (with optional intra-section order). See
|
|
12581
12029
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
12582
12030
|
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
12583
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
12584
|
-
deviceLinks: array(DeviceLinkSchema).readonly().optional(),
|
|
12585
12031
|
/** Operator-authored per-device display override. See `DeviceMeta.display`. */
|
|
12586
12032
|
display: DeviceDisplayOverrideSchema.optional()
|
|
12587
12033
|
});
|
|
@@ -12590,7 +12036,7 @@ var ConfigEntrySchema = object({
|
|
|
12590
12036
|
value: unknown(),
|
|
12591
12037
|
description: string().optional()
|
|
12592
12038
|
});
|
|
12593
|
-
var
|
|
12039
|
+
var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
|
|
12594
12040
|
/** One resolved linked device — the compact projection consumers need. */
|
|
12595
12041
|
var LinkedDeviceSchema = object({
|
|
12596
12042
|
deviceId: number(),
|
|
@@ -12653,8 +12099,6 @@ var DeviceMetaSchema = object({
|
|
|
12653
12099
|
* accordion sections (with optional intra-section order). See
|
|
12654
12100
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
12655
12101
|
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
12656
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
12657
|
-
deviceLinks: array(DeviceLinkSchema).readonly().optional(),
|
|
12658
12102
|
/** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
|
|
12659
12103
|
* Optional: only present for accessory children that carry a known role. */
|
|
12660
12104
|
role: string().nullable().optional(),
|
|
@@ -12747,12 +12191,6 @@ method(object({
|
|
|
12747
12191
|
}), _void(), {
|
|
12748
12192
|
kind: "mutation",
|
|
12749
12193
|
auth: "admin"
|
|
12750
|
-
}), method(object({
|
|
12751
|
-
deviceId: number(),
|
|
12752
|
-
deviceLinks: array(DeviceLinkSchema).readonly()
|
|
12753
|
-
}), _void(), {
|
|
12754
|
-
kind: "mutation",
|
|
12755
|
-
auth: "admin"
|
|
12756
12194
|
}), method(object({
|
|
12757
12195
|
deviceId: number(),
|
|
12758
12196
|
display: DeviceDisplayOverrideSchema.nullable()
|
|
@@ -12834,7 +12272,7 @@ method(object({
|
|
|
12834
12272
|
* shipping 293 rows to find 12. */
|
|
12835
12273
|
isCamera: boolean().optional()
|
|
12836
12274
|
}), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
|
|
12837
|
-
mode:
|
|
12275
|
+
mode: LinkedDevicesModeSchema,
|
|
12838
12276
|
devices: array(LinkedDeviceSchema)
|
|
12839
12277
|
})), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
|
|
12840
12278
|
deviceId: number(),
|
|
@@ -12867,11 +12305,7 @@ method(object({
|
|
|
12867
12305
|
deviceId: number(),
|
|
12868
12306
|
entries: array(object({
|
|
12869
12307
|
capName: string(),
|
|
12870
|
-
kind: _enum([
|
|
12871
|
-
"native",
|
|
12872
|
-
"wrapped",
|
|
12873
|
-
"linked"
|
|
12874
|
-
]),
|
|
12308
|
+
kind: _enum(["native", "wrapped"]),
|
|
12875
12309
|
providerAddonId: string(),
|
|
12876
12310
|
providerNodeId: string(),
|
|
12877
12311
|
nativeAddonId: string()
|
|
@@ -12880,11 +12314,7 @@ method(object({
|
|
|
12880
12314
|
deviceId: number(),
|
|
12881
12315
|
entries: array(object({
|
|
12882
12316
|
capName: string(),
|
|
12883
|
-
kind: _enum([
|
|
12884
|
-
"native",
|
|
12885
|
-
"wrapped",
|
|
12886
|
-
"linked"
|
|
12887
|
-
]),
|
|
12317
|
+
kind: _enum(["native", "wrapped"]),
|
|
12888
12318
|
providerAddonId: string(),
|
|
12889
12319
|
providerNodeId: string(),
|
|
12890
12320
|
nativeAddonId: string()
|
|
@@ -13693,6 +13123,10 @@ var motionDetectionCapability = {
|
|
|
13693
13123
|
mode: "singleton",
|
|
13694
13124
|
kind: "wrapper",
|
|
13695
13125
|
defaultActive: true,
|
|
13126
|
+
/** Frame-differencing motion analysis over a video stream — only a camera
|
|
13127
|
+
* produces the frames. Read by the `defaultActive` auto-bind in
|
|
13128
|
+
* `device-manager.getBindings` to decide which devices it may claim. */
|
|
13129
|
+
deviceTypes: [DeviceType.Camera],
|
|
13696
13130
|
exposesDeviceSettings: true,
|
|
13697
13131
|
methods: {
|
|
13698
13132
|
analyze: method(object({
|
|
@@ -16215,6 +15649,18 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
16215
15649
|
* redirect_uri that does not start with one of these. Required —
|
|
16216
15650
|
* an empty list means the integration can never complete linking. */
|
|
16217
15651
|
allowedRedirectPrefixes: array(string()).min(1),
|
|
15652
|
+
/** Paths accepted as a `redirect_uri` when the host is PRIVATE — loopback,
|
|
15653
|
+
* RFC1918, CGNAT (100.64/10, Tailscale), link-local, IPv6 ULA, or an
|
|
15654
|
+
* `.local` / `.internal` / `.ts.net` name. Exists for self-hosted clients
|
|
15655
|
+
* whose address the hub cannot know in advance (a Home Assistant at
|
|
15656
|
+
* `http://<lan-ip>:8123/auth/external/callback`). The PATH must match
|
|
15657
|
+
* exactly; a public host never satisfies this branch, so it is not a
|
|
15658
|
+
* wildcard prefix by another name. */
|
|
15659
|
+
allowedPrivateHostPaths: array(string()).optional(),
|
|
15660
|
+
/** When true this is a PUBLIC client (source is published, no secret can be
|
|
15661
|
+
* protected) and PKCE is mandatory: `/authorize` refuses without an S256
|
|
15662
|
+
* `code_challenge`, `/token` refuses without the matching `code_verifier`. */
|
|
15663
|
+
requiresPkce: boolean().optional(),
|
|
16218
15664
|
/** Optional public origin (no trailing slash) that this integration's
|
|
16219
15665
|
* issued codes/tokens should carry as the `hubUrl` claim — typically the
|
|
16220
15666
|
* operator-selected external-access endpoint resolved by the addon. When
|
|
@@ -16365,7 +15811,7 @@ var TrackEnvelopeSchema = object({
|
|
|
16365
15811
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
16366
15812
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
16367
15813
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
16368
|
-
* zonesVisited, bestEventId, envelope) and returns `positions` /
|
|
15814
|
+
* zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
|
|
16369
15815
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
16370
15816
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
16371
15817
|
* (`getObjectEvents` et al.).
|
|
@@ -16602,6 +16048,24 @@ var TrackSchema = object({
|
|
|
16602
16048
|
* Populated from the persisted envelope columns on historical reads;
|
|
16603
16049
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
16604
16050
|
envelope: TrackEnvelopeSchema.optional(),
|
|
16051
|
+
/**
|
|
16052
|
+
* A face DETECTOR found a face on this track — nothing more. It says the
|
|
16053
|
+
* detail plane produced a `face` detail; it does NOT say the face was
|
|
16054
|
+
* embedded, matched, above `minFacePx`, or that the recognizer was even
|
|
16055
|
+
* enabled. Set once and never cleared.
|
|
16056
|
+
*
|
|
16057
|
+
* **This exists so "face present but not recognised" is expressible.** A
|
|
16058
|
+
* recognised identity lands in `subLabel` (attributed to the face chain via
|
|
16059
|
+
* `subLabelMeta.stepId`), so before this field a track with an unmatched face
|
|
16060
|
+
* and a track with no face at all were byte-identical on the wire and no
|
|
16061
|
+
* surface could tell them apart. The read is `hasFace === true && subLabel
|
|
16062
|
+
* === undefined`.
|
|
16063
|
+
*
|
|
16064
|
+
* **Absent ≠ false.** Every row written before the column existed omits it,
|
|
16065
|
+
* and so does every server that predates the field — a consumer must test
|
|
16066
|
+
* `=== true` and render nothing otherwise, never infer "no face".
|
|
16067
|
+
*/
|
|
16068
|
+
hasFace: boolean().optional(),
|
|
16605
16069
|
...TrackFlagFields,
|
|
16606
16070
|
...TrackRetrainFields
|
|
16607
16071
|
});
|
|
@@ -19510,6 +18974,10 @@ var SsoBridgeClaimsSchema = object({
|
|
|
19510
18974
|
integrationId: string().optional(),
|
|
19511
18975
|
/** JWT ID — unique per issued code; consumed-set enforces single-use. */
|
|
19512
18976
|
jti: string().optional(),
|
|
18977
|
+
/** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
|
|
18978
|
+
* client. Its PRESENCE is what makes the verifier mandatory at exchange,
|
|
18979
|
+
* so the requirement travels with the code and not with mutable config. */
|
|
18980
|
+
codeChallenge: string().optional(),
|
|
19513
18981
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
19514
18982
|
* tokens so the verify path can check the session is not revoked. */
|
|
19515
18983
|
sessionId: string().optional()
|
|
@@ -20077,7 +19545,7 @@ var ClipPlaybackSchema = object({
|
|
|
20077
19545
|
playbackEndpoints: array(string()).optional(),
|
|
20078
19546
|
token: string().optional()
|
|
20079
19547
|
});
|
|
20080
|
-
method(object({
|
|
19548
|
+
DeviceType.Camera, method(object({
|
|
20081
19549
|
deviceId: number(),
|
|
20082
19550
|
since: number(),
|
|
20083
19551
|
until: number(),
|
|
@@ -25333,13 +24801,18 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
|
|
|
25333
24801
|
username: string(),
|
|
25334
24802
|
scopes: array(TokenScopeSchema),
|
|
25335
24803
|
redirectUri: string(),
|
|
25336
|
-
hubUrl: string()
|
|
24804
|
+
hubUrl: string(),
|
|
24805
|
+
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
24806
|
+
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
24807
|
+
codeChallenge: string().optional()
|
|
25337
24808
|
}), object({ code: string() }), {
|
|
25338
24809
|
kind: "mutation",
|
|
25339
24810
|
access: "create"
|
|
25340
24811
|
}), method(object({
|
|
25341
24812
|
code: string(),
|
|
25342
|
-
redirectUri: string()
|
|
24813
|
+
redirectUri: string(),
|
|
24814
|
+
/** PKCE verifier. REQUIRED when the code carries a challenge. */
|
|
24815
|
+
codeVerifier: string().optional()
|
|
25343
24816
|
}), object({
|
|
25344
24817
|
accessToken: string(),
|
|
25345
24818
|
refreshToken: string(),
|
|
@@ -25566,190 +25039,819 @@ object({
|
|
|
25566
25039
|
});
|
|
25567
25040
|
DeviceType.Weather;
|
|
25568
25041
|
/**
|
|
25569
|
-
* Per-zone occupancy aggregation produced by the analytics frame
|
|
25570
|
-
* processor on every inference result. Covers the full combinatorial
|
|
25571
|
-
* matrix the operator UI needs: total objects everywhere, total
|
|
25572
|
-
* objects per zone, single class everywhere, single class per zone,
|
|
25573
|
-
* objects outside any zone.
|
|
25574
|
-
*
|
|
25575
|
-
* Counts are derived from the analytics tracker (tracked detections
|
|
25576
|
-
* with stable trackIds), not raw detector hits — this filters out
|
|
25577
|
-
* one-off detector flickers and gives counts that match what the user
|
|
25578
|
-
* sees on the live overlay.
|
|
25042
|
+
* Per-zone occupancy aggregation produced by the analytics frame
|
|
25043
|
+
* processor on every inference result. Covers the full combinatorial
|
|
25044
|
+
* matrix the operator UI needs: total objects everywhere, total
|
|
25045
|
+
* objects per zone, single class everywhere, single class per zone,
|
|
25046
|
+
* objects outside any zone.
|
|
25047
|
+
*
|
|
25048
|
+
* Counts are derived from the analytics tracker (tracked detections
|
|
25049
|
+
* with stable trackIds), not raw detector hits — this filters out
|
|
25050
|
+
* one-off detector flickers and gives counts that match what the user
|
|
25051
|
+
* sees on the live overlay.
|
|
25052
|
+
*
|
|
25053
|
+
* Overlap policy: a detection that intersects two zones counts in
|
|
25054
|
+
* BOTH zones' `byClass` and `totalObjects` (count-in-each). The
|
|
25055
|
+
* `frame` aggregate de-duplicates trivially since it's frame-wide.
|
|
25056
|
+
* `unzoned` counts only detections that landed in zero zones.
|
|
25057
|
+
*/
|
|
25058
|
+
var PerScopeBreakdownSchema = object({
|
|
25059
|
+
/** Total tracked objects in this scope (frame / zone / unzoned). */
|
|
25060
|
+
totalObjects: number().int().nonnegative(),
|
|
25061
|
+
/** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
|
|
25062
|
+
byClass: record(string(), number().int().nonnegative())
|
|
25063
|
+
});
|
|
25064
|
+
var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
|
|
25065
|
+
zoneId: string(),
|
|
25066
|
+
zoneName: string(),
|
|
25067
|
+
/** TrackIds of objects currently inside this zone — for cross-reference
|
|
25068
|
+
* with the per-track detail panel and live overlay. */
|
|
25069
|
+
trackIds: array(string()).readonly()
|
|
25070
|
+
});
|
|
25071
|
+
/**
|
|
25072
|
+
* A parked ("stationary") object surfaced alongside occupancy — an object that
|
|
25073
|
+
* settled and stopped moving. It is NO LONGER a tracked object (the tracker was
|
|
25074
|
+
* told to forget it so it stops re-spawning tracks/events), but it IS still
|
|
25075
|
+
* physically present, so it keeps counting toward `frame` occupancy and is
|
|
25076
|
+
* listed here so the UI can show it in a dedicated "Stationary" section instead
|
|
25077
|
+
* of flooding the live event feed.
|
|
25078
|
+
*/
|
|
25079
|
+
var StationaryObjectSchema = object({
|
|
25080
|
+
id: string(),
|
|
25081
|
+
className: string(),
|
|
25082
|
+
bbox: object({
|
|
25083
|
+
x: number(),
|
|
25084
|
+
y: number(),
|
|
25085
|
+
w: number(),
|
|
25086
|
+
h: number()
|
|
25087
|
+
}),
|
|
25088
|
+
frameWidth: number().int().nonnegative(),
|
|
25089
|
+
frameHeight: number().int().nonnegative(),
|
|
25090
|
+
/** When the source track was first seen. */
|
|
25091
|
+
firstSeenAt: number().int(),
|
|
25092
|
+
/** When the object was recognised as parked (promotion time). */
|
|
25093
|
+
becameStationaryAt: number().int(),
|
|
25094
|
+
/** Last frame a detection confirmed the object is still there. */
|
|
25095
|
+
lastConfirmedAt: number().int(),
|
|
25096
|
+
/** Enrichment label carried from the source track (identity / plate). */
|
|
25097
|
+
label: string().optional(),
|
|
25098
|
+
/** Native-resolution key-frame media key for the parked object's best image. */
|
|
25099
|
+
keyFrameMediaKey: string().optional()
|
|
25100
|
+
});
|
|
25101
|
+
var CameraOccupancySnapshotSchema = object({
|
|
25102
|
+
/** Frame timestamp of the inference result that produced this snapshot. */
|
|
25103
|
+
ts: number().int(),
|
|
25104
|
+
/** Frame width/height in pixels — let the UI normalize bbox coords. */
|
|
25105
|
+
frameWidth: number().int().nonnegative(),
|
|
25106
|
+
frameHeight: number().int().nonnegative(),
|
|
25107
|
+
/** Per-zone breakdown — one entry per defined zone (user + onboard). */
|
|
25108
|
+
zones: array(ZoneScopeBreakdownSchema).readonly(),
|
|
25109
|
+
/** Frame-wide aggregate (everywhere, regardless of zone membership).
|
|
25110
|
+
* INCLUDES currently-confirmed stationary objects (they are still present). */
|
|
25111
|
+
frame: PerScopeBreakdownSchema,
|
|
25112
|
+
/** Detections that landed outside every zone. Empty when no zones defined. */
|
|
25113
|
+
unzoned: PerScopeBreakdownSchema,
|
|
25114
|
+
/** Parked objects on this camera (additive — absent on legacy snapshots).
|
|
25115
|
+
* Surfaced separately so the UI shows them in a dedicated section rather
|
|
25116
|
+
* than as repeated tracks/events. */
|
|
25117
|
+
stationaryObjects: array(StationaryObjectSchema).readonly().optional()
|
|
25118
|
+
});
|
|
25119
|
+
/**
|
|
25120
|
+
* Time-series resolution. The history methods return one bucket per
|
|
25121
|
+
* step over the requested range. Smaller resolutions cost more
|
|
25122
|
+
* memory + bandwidth; bound to discrete steps so caller cannot ask
|
|
25123
|
+
* for arbitrary fractional buckets.
|
|
25124
|
+
*/
|
|
25125
|
+
var HistoryResolutionEnum = _enum([
|
|
25126
|
+
"minute",
|
|
25127
|
+
"5min",
|
|
25128
|
+
"hour"
|
|
25129
|
+
]);
|
|
25130
|
+
var HistoryRangeSchema = object({
|
|
25131
|
+
/** Range start (epoch ms, inclusive). */
|
|
25132
|
+
from: number().int(),
|
|
25133
|
+
/** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
|
|
25134
|
+
to: number().int(),
|
|
25135
|
+
resolution: HistoryResolutionEnum
|
|
25136
|
+
});
|
|
25137
|
+
var HistoryPointSchema = object({
|
|
25138
|
+
/** Bucket midpoint (epoch ms). */
|
|
25139
|
+
ts: number().int(),
|
|
25140
|
+
/** Object count averaged over the bucket (rounded to nearest integer). */
|
|
25141
|
+
count: number().int().nonnegative()
|
|
25142
|
+
});
|
|
25143
|
+
DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
|
|
25144
|
+
deviceId: number(),
|
|
25145
|
+
zoneId: string(),
|
|
25146
|
+
className: string().optional()
|
|
25147
|
+
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
|
|
25148
|
+
deviceId: number(),
|
|
25149
|
+
className: string().optional()
|
|
25150
|
+
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
|
|
25151
|
+
deviceId: number(),
|
|
25152
|
+
className: string().optional()
|
|
25153
|
+
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly());
|
|
25154
|
+
/**
|
|
25155
|
+
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
25156
|
+
* cap so a single CRUD surface backs every consumer; each stage has
|
|
25157
|
+
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
25158
|
+
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
25159
|
+
*
|
|
25160
|
+
* Extend the enum here when a new gating consumer comes online (audio
|
|
25161
|
+
* gating, alert filtering, …) — no other surface needs to change.
|
|
25162
|
+
*/
|
|
25163
|
+
var ZoneRuleStageEnum = _enum([
|
|
25164
|
+
"motion",
|
|
25165
|
+
"detection",
|
|
25166
|
+
"package"
|
|
25167
|
+
]);
|
|
25168
|
+
DeviceType.Camera, method(object({
|
|
25169
|
+
deviceId: number(),
|
|
25170
|
+
stage: ZoneRuleStageEnum
|
|
25171
|
+
}), array(ZoneRuleSchema).readonly()), method(object({
|
|
25172
|
+
deviceId: number(),
|
|
25173
|
+
stage: ZoneRuleStageEnum,
|
|
25174
|
+
rules: array(ZoneRuleSchema).readonly()
|
|
25175
|
+
}), _void(), {
|
|
25176
|
+
kind: "mutation",
|
|
25177
|
+
auth: "admin"
|
|
25178
|
+
}), object({
|
|
25179
|
+
motion: array(ZoneRuleSchema).readonly(),
|
|
25180
|
+
detection: array(ZoneRuleSchema).readonly(),
|
|
25181
|
+
package: array(ZoneRuleSchema).readonly()
|
|
25182
|
+
});
|
|
25183
|
+
var HF_BASE_URL = `https://huggingface.co/camstack/camstack-models/resolve/main`;
|
|
25184
|
+
/**
|
|
25185
|
+
* Accessory device helpers — shared across drivers.
|
|
25186
|
+
*
|
|
25187
|
+
* Many vendor-specific drivers register accessory child devices on
|
|
25188
|
+
* top of a parent (Reolink: siren / floodlight / PIR / autotrack /
|
|
25189
|
+
* chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
|
|
25190
|
+
* driver picks the right `DeviceType` + `DeviceRole` explicitly when
|
|
25191
|
+
* spawning, builds a name derived from the parent, and produces a
|
|
25192
|
+
* stableId tied to the parent so boot-restore can reconstruct the
|
|
25193
|
+
* relationship.
|
|
25194
|
+
*
|
|
25195
|
+
* Centralised `(kind → DeviceType)` mapping was dropped on purpose:
|
|
25196
|
+
* drivers may reasonably disagree on the right type for an accessory
|
|
25197
|
+
* (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
|
|
25198
|
+
* read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
|
|
25199
|
+
* one canonical mapping was over-prescriptive and added a layer of
|
|
25200
|
+
* indirection without saving meaningful code at call sites — the
|
|
25201
|
+
* driver knows its own hardware best.
|
|
25202
|
+
*/
|
|
25203
|
+
/**
|
|
25204
|
+
* Subset of `DeviceRole` values that drivers register as child
|
|
25205
|
+
* accessories of a parent device. Sourced verbatim from `DeviceRole`
|
|
25206
|
+
* — `AccessoryKind` is the alias drivers use when building accessory
|
|
25207
|
+
* children, so the call site reads as
|
|
25208
|
+
* `accessoryStableId(parent, AccessoryKind.Siren)` rather than
|
|
25209
|
+
* `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
|
|
25210
|
+
* any role works, including non-accessory ones like Doorbell).
|
|
25211
|
+
*/
|
|
25212
|
+
var AccessoryKind = {
|
|
25213
|
+
Siren: DeviceRole.Siren,
|
|
25214
|
+
Floodlight: DeviceRole.Floodlight,
|
|
25215
|
+
Spotlight: DeviceRole.Spotlight,
|
|
25216
|
+
PirSensor: DeviceRole.PirSensor,
|
|
25217
|
+
Chime: DeviceRole.Chime,
|
|
25218
|
+
Autotrack: DeviceRole.Autotrack,
|
|
25219
|
+
Nightvision: DeviceRole.Nightvision,
|
|
25220
|
+
PrivacyMask: DeviceRole.PrivacyMask
|
|
25221
|
+
};
|
|
25222
|
+
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
25223
|
+
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
|
|
25224
|
+
new Set(Object.values(DeviceType));
|
|
25225
|
+
DeviceFeature.BatteryOperated;
|
|
25226
|
+
/**
|
|
25227
|
+
* Error types for the safe expression engine. Two distinct classes so callers
|
|
25228
|
+
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
25229
|
+
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
25230
|
+
*/
|
|
25231
|
+
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
25232
|
+
* the failure is anchored to a character (author-facing inline feedback). */
|
|
25233
|
+
var ExpressionParseError = class extends Error {
|
|
25234
|
+
position;
|
|
25235
|
+
constructor(message, position) {
|
|
25236
|
+
super(message);
|
|
25237
|
+
this.name = "ExpressionParseError";
|
|
25238
|
+
this.position = position;
|
|
25239
|
+
}
|
|
25240
|
+
};
|
|
25241
|
+
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
25242
|
+
* result, unknown builtin, step-budget exceeded). */
|
|
25243
|
+
var ExpressionEvalError = class extends Error {
|
|
25244
|
+
constructor(message) {
|
|
25245
|
+
super(message);
|
|
25246
|
+
this.name = "ExpressionEvalError";
|
|
25247
|
+
}
|
|
25248
|
+
};
|
|
25249
|
+
/**
|
|
25250
|
+
* Frozen, null-prototype builtin function table for the expression engine
|
|
25251
|
+
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
25252
|
+
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
25253
|
+
* own-property check against it.
|
|
25254
|
+
*
|
|
25255
|
+
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
25256
|
+
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
25257
|
+
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
25258
|
+
* (there is no `Object.prototype` in the chain), so those names are not
|
|
25259
|
+
* callable — they are simply "unknown function" at parse time.
|
|
25260
|
+
*
|
|
25261
|
+
* Every numeric argument is validated as a finite number and every numeric
|
|
25262
|
+
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
25263
|
+
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
25264
|
+
* closed rather than emitting a garbage value.
|
|
25265
|
+
*/
|
|
25266
|
+
function asFiniteNumber(value, name, index) {
|
|
25267
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
25268
|
+
return value;
|
|
25269
|
+
}
|
|
25270
|
+
function asString$1(value, name, index) {
|
|
25271
|
+
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
25272
|
+
return value;
|
|
25273
|
+
}
|
|
25274
|
+
function finiteResult(value, name) {
|
|
25275
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
25276
|
+
return value;
|
|
25277
|
+
}
|
|
25278
|
+
function allFiniteNumbers(args, name) {
|
|
25279
|
+
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
25280
|
+
}
|
|
25281
|
+
var INF = Number.POSITIVE_INFINITY;
|
|
25282
|
+
var table = {
|
|
25283
|
+
min: {
|
|
25284
|
+
minArgs: 1,
|
|
25285
|
+
maxArgs: INF,
|
|
25286
|
+
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
25287
|
+
},
|
|
25288
|
+
max: {
|
|
25289
|
+
minArgs: 1,
|
|
25290
|
+
maxArgs: INF,
|
|
25291
|
+
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
25292
|
+
},
|
|
25293
|
+
abs: {
|
|
25294
|
+
minArgs: 1,
|
|
25295
|
+
maxArgs: 1,
|
|
25296
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
25297
|
+
},
|
|
25298
|
+
floor: {
|
|
25299
|
+
minArgs: 1,
|
|
25300
|
+
maxArgs: 1,
|
|
25301
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
25302
|
+
},
|
|
25303
|
+
ceil: {
|
|
25304
|
+
minArgs: 1,
|
|
25305
|
+
maxArgs: 1,
|
|
25306
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
25307
|
+
},
|
|
25308
|
+
sqrt: {
|
|
25309
|
+
minArgs: 1,
|
|
25310
|
+
maxArgs: 1,
|
|
25311
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
25312
|
+
},
|
|
25313
|
+
round: {
|
|
25314
|
+
minArgs: 1,
|
|
25315
|
+
maxArgs: 2,
|
|
25316
|
+
apply: (args) => {
|
|
25317
|
+
const x = asFiniteNumber(args[0], "round", 0);
|
|
25318
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
25319
|
+
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
25320
|
+
const factor = 10 ** digits;
|
|
25321
|
+
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
25322
|
+
}
|
|
25323
|
+
},
|
|
25324
|
+
pow: {
|
|
25325
|
+
minArgs: 2,
|
|
25326
|
+
maxArgs: 2,
|
|
25327
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
25328
|
+
},
|
|
25329
|
+
clamp: {
|
|
25330
|
+
minArgs: 3,
|
|
25331
|
+
maxArgs: 3,
|
|
25332
|
+
apply: (args) => {
|
|
25333
|
+
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
25334
|
+
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
25335
|
+
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
25336
|
+
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
25337
|
+
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
25338
|
+
}
|
|
25339
|
+
},
|
|
25340
|
+
avg: {
|
|
25341
|
+
minArgs: 1,
|
|
25342
|
+
maxArgs: INF,
|
|
25343
|
+
apply: (args) => {
|
|
25344
|
+
const nums = allFiniteNumbers(args, "avg");
|
|
25345
|
+
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
25346
|
+
}
|
|
25347
|
+
},
|
|
25348
|
+
sum: {
|
|
25349
|
+
minArgs: 1,
|
|
25350
|
+
maxArgs: INF,
|
|
25351
|
+
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
25352
|
+
},
|
|
25353
|
+
coalesce: {
|
|
25354
|
+
minArgs: 1,
|
|
25355
|
+
maxArgs: INF,
|
|
25356
|
+
apply: (args) => {
|
|
25357
|
+
for (const a of args) if (a !== null) return a;
|
|
25358
|
+
return null;
|
|
25359
|
+
}
|
|
25360
|
+
},
|
|
25361
|
+
age: {
|
|
25362
|
+
minArgs: 2,
|
|
25363
|
+
maxArgs: 2,
|
|
25364
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
25365
|
+
},
|
|
25366
|
+
convert: {
|
|
25367
|
+
minArgs: 3,
|
|
25368
|
+
maxArgs: 3,
|
|
25369
|
+
apply: (args, hooks) => {
|
|
25370
|
+
const x = asFiniteNumber(args[0], "convert", 0);
|
|
25371
|
+
const from = asString$1(args[1], "convert", 1).trim();
|
|
25372
|
+
const to = asString$1(args[2], "convert", 2).trim();
|
|
25373
|
+
if (hooks.convert) {
|
|
25374
|
+
const out = hooks.convert(x, from, to);
|
|
25375
|
+
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
25376
|
+
return finiteResult(out, "convert");
|
|
25377
|
+
}
|
|
25378
|
+
if (from === to) return x;
|
|
25379
|
+
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
25380
|
+
}
|
|
25381
|
+
}
|
|
25382
|
+
};
|
|
25383
|
+
Object.freeze(Object.assign(Object.create(null), table));
|
|
25384
|
+
/** The set of valid builtin names — used by the parser to reject unknown
|
|
25385
|
+
* callees at parse time (immediate author feedback). */
|
|
25386
|
+
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
25387
|
+
/**
|
|
25388
|
+
* Resource-bound constants for the safe expression engine.
|
|
25389
|
+
*
|
|
25390
|
+
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
25391
|
+
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
25392
|
+
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
25393
|
+
* work a single author-supplied expression can request, so a hostile or
|
|
25394
|
+
* accidental pathological string can never spend unbounded CPU/memory.
|
|
25395
|
+
*/
|
|
25396
|
+
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
25397
|
+
* rejected without allocation. */
|
|
25398
|
+
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
25399
|
+
/** A legal binding / identifier name. */
|
|
25400
|
+
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
25401
|
+
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
25402
|
+
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
25403
|
+
var RESERVED_BINDING_NAMES = new Set([
|
|
25404
|
+
"now",
|
|
25405
|
+
"true",
|
|
25406
|
+
"false",
|
|
25407
|
+
"null"
|
|
25408
|
+
]);
|
|
25409
|
+
/**
|
|
25410
|
+
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
25411
|
+
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
25412
|
+
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
25413
|
+
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
25414
|
+
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
25415
|
+
* is a parse error with a source position, so member access / assignment /
|
|
25416
|
+
* template literals are lexically impossible.
|
|
25417
|
+
*/
|
|
25418
|
+
var KEYWORDS = new Set([
|
|
25419
|
+
"true",
|
|
25420
|
+
"false",
|
|
25421
|
+
"null"
|
|
25422
|
+
]);
|
|
25423
|
+
function isDigit(ch) {
|
|
25424
|
+
return ch >= "0" && ch <= "9";
|
|
25425
|
+
}
|
|
25426
|
+
function isIdentStart(ch) {
|
|
25427
|
+
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
25428
|
+
}
|
|
25429
|
+
function isIdentPart(ch) {
|
|
25430
|
+
return isIdentStart(ch) || isDigit(ch);
|
|
25431
|
+
}
|
|
25432
|
+
function isWhitespace(ch) {
|
|
25433
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
25434
|
+
}
|
|
25435
|
+
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
25436
|
+
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
25437
|
+
* string. */
|
|
25438
|
+
function tokenize(source) {
|
|
25439
|
+
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
25440
|
+
const tokens = [];
|
|
25441
|
+
let i = 0;
|
|
25442
|
+
const n = source.length;
|
|
25443
|
+
while (i < n) {
|
|
25444
|
+
const ch = source[i];
|
|
25445
|
+
if (isWhitespace(ch)) {
|
|
25446
|
+
i += 1;
|
|
25447
|
+
continue;
|
|
25448
|
+
}
|
|
25449
|
+
if (isDigit(ch)) {
|
|
25450
|
+
const start = i;
|
|
25451
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
25452
|
+
if (i < n && source[i] === ".") {
|
|
25453
|
+
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
25454
|
+
i += 1;
|
|
25455
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
25456
|
+
}
|
|
25457
|
+
const text = source.slice(start, i);
|
|
25458
|
+
const value = Number(text);
|
|
25459
|
+
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
25460
|
+
tokens.push({
|
|
25461
|
+
type: "number",
|
|
25462
|
+
value,
|
|
25463
|
+
pos: start
|
|
25464
|
+
});
|
|
25465
|
+
continue;
|
|
25466
|
+
}
|
|
25467
|
+
if (ch === "'" || ch === "\"") {
|
|
25468
|
+
const quote = ch;
|
|
25469
|
+
const start = i;
|
|
25470
|
+
i += 1;
|
|
25471
|
+
let out = "";
|
|
25472
|
+
let closed = false;
|
|
25473
|
+
while (i < n) {
|
|
25474
|
+
const c = source[i];
|
|
25475
|
+
if (c === "\\") {
|
|
25476
|
+
const next = i + 1 < n ? source[i + 1] : "";
|
|
25477
|
+
if (next === "\\" || next === "'" || next === "\"") {
|
|
25478
|
+
out += next;
|
|
25479
|
+
i += 2;
|
|
25480
|
+
continue;
|
|
25481
|
+
}
|
|
25482
|
+
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
25483
|
+
}
|
|
25484
|
+
if (c === quote) {
|
|
25485
|
+
closed = true;
|
|
25486
|
+
i += 1;
|
|
25487
|
+
break;
|
|
25488
|
+
}
|
|
25489
|
+
out += c;
|
|
25490
|
+
i += 1;
|
|
25491
|
+
}
|
|
25492
|
+
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
25493
|
+
tokens.push({
|
|
25494
|
+
type: "string",
|
|
25495
|
+
value: out,
|
|
25496
|
+
pos: start
|
|
25497
|
+
});
|
|
25498
|
+
continue;
|
|
25499
|
+
}
|
|
25500
|
+
if (isIdentStart(ch)) {
|
|
25501
|
+
const start = i;
|
|
25502
|
+
while (i < n && isIdentPart(source[i])) i += 1;
|
|
25503
|
+
const text = source.slice(start, i);
|
|
25504
|
+
if (KEYWORDS.has(text)) tokens.push({
|
|
25505
|
+
type: "keyword",
|
|
25506
|
+
keyword: keywordOf(text),
|
|
25507
|
+
pos: start
|
|
25508
|
+
});
|
|
25509
|
+
else tokens.push({
|
|
25510
|
+
type: "identifier",
|
|
25511
|
+
name: text,
|
|
25512
|
+
pos: start
|
|
25513
|
+
});
|
|
25514
|
+
continue;
|
|
25515
|
+
}
|
|
25516
|
+
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
25517
|
+
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
25518
|
+
tokens.push({
|
|
25519
|
+
type: "punct",
|
|
25520
|
+
punct: two,
|
|
25521
|
+
pos: i
|
|
25522
|
+
});
|
|
25523
|
+
i += 2;
|
|
25524
|
+
continue;
|
|
25525
|
+
}
|
|
25526
|
+
if (isSinglePunct(ch)) {
|
|
25527
|
+
tokens.push({
|
|
25528
|
+
type: "punct",
|
|
25529
|
+
punct: ch,
|
|
25530
|
+
pos: i
|
|
25531
|
+
});
|
|
25532
|
+
i += 1;
|
|
25533
|
+
continue;
|
|
25534
|
+
}
|
|
25535
|
+
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
25536
|
+
}
|
|
25537
|
+
tokens.push({
|
|
25538
|
+
type: "eof",
|
|
25539
|
+
pos: n
|
|
25540
|
+
});
|
|
25541
|
+
return tokens;
|
|
25542
|
+
}
|
|
25543
|
+
function keywordOf(text) {
|
|
25544
|
+
if (text === "true") return "true";
|
|
25545
|
+
if (text === "false") return "false";
|
|
25546
|
+
return "null";
|
|
25547
|
+
}
|
|
25548
|
+
function isSinglePunct(ch) {
|
|
25549
|
+
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
25550
|
+
}
|
|
25551
|
+
/**
|
|
25552
|
+
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
25553
|
+
*
|
|
25554
|
+
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
25555
|
+
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
25556
|
+
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
25557
|
+
* string validated against the builtin table at parse time, so an unknown
|
|
25558
|
+
* function is rejected immediately (author feedback) and a persisted expression
|
|
25559
|
+
* that references a since-removed builtin degrades at read.
|
|
25560
|
+
*
|
|
25561
|
+
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
25562
|
+
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
25563
|
+
*/
|
|
25564
|
+
/** Binary/logical operator precedence (higher binds tighter). */
|
|
25565
|
+
var BINARY_PRECEDENCE = {
|
|
25566
|
+
"||": 1,
|
|
25567
|
+
"&&": 2,
|
|
25568
|
+
"==": 3,
|
|
25569
|
+
"!=": 3,
|
|
25570
|
+
"<": 4,
|
|
25571
|
+
"<=": 4,
|
|
25572
|
+
">": 4,
|
|
25573
|
+
">=": 4,
|
|
25574
|
+
"+": 5,
|
|
25575
|
+
"-": 5,
|
|
25576
|
+
"*": 6,
|
|
25577
|
+
"/": 6,
|
|
25578
|
+
"%": 6
|
|
25579
|
+
};
|
|
25580
|
+
function isLogicalOp(op) {
|
|
25581
|
+
return op === "&&" || op === "||";
|
|
25582
|
+
}
|
|
25583
|
+
function isBinaryOp(op) {
|
|
25584
|
+
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
25585
|
+
}
|
|
25586
|
+
var Parser = class {
|
|
25587
|
+
tokens;
|
|
25588
|
+
pos = 0;
|
|
25589
|
+
nodeCount = 0;
|
|
25590
|
+
identifiers = /* @__PURE__ */ new Set();
|
|
25591
|
+
callees = /* @__PURE__ */ new Set();
|
|
25592
|
+
constructor(tokens) {
|
|
25593
|
+
this.tokens = tokens;
|
|
25594
|
+
}
|
|
25595
|
+
parse() {
|
|
25596
|
+
const ast = this.parseTernary();
|
|
25597
|
+
const tok = this.peek();
|
|
25598
|
+
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
25599
|
+
return {
|
|
25600
|
+
ast,
|
|
25601
|
+
identifiers: this.identifiers,
|
|
25602
|
+
callees: this.callees,
|
|
25603
|
+
nodeCount: this.nodeCount
|
|
25604
|
+
};
|
|
25605
|
+
}
|
|
25606
|
+
peek() {
|
|
25607
|
+
return this.tokens[this.pos];
|
|
25608
|
+
}
|
|
25609
|
+
next() {
|
|
25610
|
+
return this.tokens[this.pos++];
|
|
25611
|
+
}
|
|
25612
|
+
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
25613
|
+
expectPunct(punct) {
|
|
25614
|
+
const tok = this.peek();
|
|
25615
|
+
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
25616
|
+
this.pos += 1;
|
|
25617
|
+
}
|
|
25618
|
+
matchPunct(punct) {
|
|
25619
|
+
const tok = this.peek();
|
|
25620
|
+
if (tok.type === "punct" && tok.punct === punct) {
|
|
25621
|
+
this.pos += 1;
|
|
25622
|
+
return true;
|
|
25623
|
+
}
|
|
25624
|
+
return false;
|
|
25625
|
+
}
|
|
25626
|
+
countNode() {
|
|
25627
|
+
this.nodeCount += 1;
|
|
25628
|
+
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
25629
|
+
}
|
|
25630
|
+
parseTernary() {
|
|
25631
|
+
const test = this.parseBinary(1);
|
|
25632
|
+
if (this.matchPunct("?")) {
|
|
25633
|
+
const consequent = this.parseTernary();
|
|
25634
|
+
this.expectPunct(":");
|
|
25635
|
+
const alternate = this.parseTernary();
|
|
25636
|
+
this.countNode();
|
|
25637
|
+
return {
|
|
25638
|
+
kind: "conditional",
|
|
25639
|
+
test,
|
|
25640
|
+
consequent,
|
|
25641
|
+
alternate
|
|
25642
|
+
};
|
|
25643
|
+
}
|
|
25644
|
+
return test;
|
|
25645
|
+
}
|
|
25646
|
+
parseBinary(minPrec) {
|
|
25647
|
+
let left = this.parseUnary();
|
|
25648
|
+
for (;;) {
|
|
25649
|
+
const tok = this.peek();
|
|
25650
|
+
if (tok.type !== "punct") break;
|
|
25651
|
+
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
25652
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
25653
|
+
const op = tok.punct;
|
|
25654
|
+
this.pos += 1;
|
|
25655
|
+
const right = this.parseBinary(prec + 1);
|
|
25656
|
+
this.countNode();
|
|
25657
|
+
if (isLogicalOp(op)) left = {
|
|
25658
|
+
kind: "logical",
|
|
25659
|
+
op,
|
|
25660
|
+
left,
|
|
25661
|
+
right
|
|
25662
|
+
};
|
|
25663
|
+
else if (isBinaryOp(op)) left = {
|
|
25664
|
+
kind: "binary",
|
|
25665
|
+
op,
|
|
25666
|
+
left,
|
|
25667
|
+
right
|
|
25668
|
+
};
|
|
25669
|
+
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
25670
|
+
}
|
|
25671
|
+
return left;
|
|
25672
|
+
}
|
|
25673
|
+
parseUnary() {
|
|
25674
|
+
const tok = this.peek();
|
|
25675
|
+
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
25676
|
+
const op = tok.punct;
|
|
25677
|
+
this.pos += 1;
|
|
25678
|
+
const operand = this.parseUnary();
|
|
25679
|
+
this.countNode();
|
|
25680
|
+
return {
|
|
25681
|
+
kind: "unary",
|
|
25682
|
+
op,
|
|
25683
|
+
operand
|
|
25684
|
+
};
|
|
25685
|
+
}
|
|
25686
|
+
return this.parsePrimary();
|
|
25687
|
+
}
|
|
25688
|
+
parsePrimary() {
|
|
25689
|
+
const tok = this.next();
|
|
25690
|
+
switch (tok.type) {
|
|
25691
|
+
case "number":
|
|
25692
|
+
this.countNode();
|
|
25693
|
+
return {
|
|
25694
|
+
kind: "literal",
|
|
25695
|
+
value: tok.value
|
|
25696
|
+
};
|
|
25697
|
+
case "string":
|
|
25698
|
+
this.countNode();
|
|
25699
|
+
return {
|
|
25700
|
+
kind: "literal",
|
|
25701
|
+
value: tok.value
|
|
25702
|
+
};
|
|
25703
|
+
case "keyword":
|
|
25704
|
+
this.countNode();
|
|
25705
|
+
return {
|
|
25706
|
+
kind: "literal",
|
|
25707
|
+
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
25708
|
+
};
|
|
25709
|
+
case "identifier": {
|
|
25710
|
+
const nextTok = this.peek();
|
|
25711
|
+
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
25712
|
+
this.identifiers.add(tok.name);
|
|
25713
|
+
this.countNode();
|
|
25714
|
+
return {
|
|
25715
|
+
kind: "identifier",
|
|
25716
|
+
name: tok.name
|
|
25717
|
+
};
|
|
25718
|
+
}
|
|
25719
|
+
case "punct":
|
|
25720
|
+
if (tok.punct === "(") {
|
|
25721
|
+
const inner = this.parseTernary();
|
|
25722
|
+
this.expectPunct(")");
|
|
25723
|
+
return inner;
|
|
25724
|
+
}
|
|
25725
|
+
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
25726
|
+
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
25727
|
+
}
|
|
25728
|
+
}
|
|
25729
|
+
parseCall(callee, pos) {
|
|
25730
|
+
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
25731
|
+
this.expectPunct("(");
|
|
25732
|
+
const args = [];
|
|
25733
|
+
if (!this.matchPunct(")")) for (;;) {
|
|
25734
|
+
args.push(this.parseTernary());
|
|
25735
|
+
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
25736
|
+
if (this.matchPunct(",")) continue;
|
|
25737
|
+
this.expectPunct(")");
|
|
25738
|
+
break;
|
|
25739
|
+
}
|
|
25740
|
+
this.callees.add(callee);
|
|
25741
|
+
this.countNode();
|
|
25742
|
+
return {
|
|
25743
|
+
kind: "call",
|
|
25744
|
+
callee,
|
|
25745
|
+
args
|
|
25746
|
+
};
|
|
25747
|
+
}
|
|
25748
|
+
};
|
|
25749
|
+
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
25750
|
+
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
25751
|
+
function parseExpression(source) {
|
|
25752
|
+
return new Parser(tokenize(source)).parse();
|
|
25753
|
+
}
|
|
25754
|
+
/**
|
|
25755
|
+
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
25756
|
+
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
25757
|
+
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
25758
|
+
* one per read on a hot resolve path.
|
|
25579
25759
|
*
|
|
25580
|
-
*
|
|
25581
|
-
*
|
|
25582
|
-
*
|
|
25583
|
-
* `unzoned` counts only detections that landed in zero zones.
|
|
25760
|
+
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
25761
|
+
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
25762
|
+
* callers is safe and maximises hit rate.
|
|
25584
25763
|
*/
|
|
25585
|
-
var
|
|
25586
|
-
|
|
25587
|
-
|
|
25588
|
-
|
|
25589
|
-
|
|
25590
|
-
|
|
25591
|
-
|
|
25592
|
-
|
|
25593
|
-
|
|
25594
|
-
|
|
25595
|
-
|
|
25596
|
-
|
|
25597
|
-
|
|
25764
|
+
var cache = /* @__PURE__ */ new Map();
|
|
25765
|
+
function getCached(source) {
|
|
25766
|
+
const hit = cache.get(source);
|
|
25767
|
+
if (hit !== void 0) {
|
|
25768
|
+
cache.delete(source);
|
|
25769
|
+
cache.set(source, hit);
|
|
25770
|
+
return hit;
|
|
25771
|
+
}
|
|
25772
|
+
let result;
|
|
25773
|
+
try {
|
|
25774
|
+
result = {
|
|
25775
|
+
ok: true,
|
|
25776
|
+
parsed: parseExpression(source)
|
|
25777
|
+
};
|
|
25778
|
+
} catch (err) {
|
|
25779
|
+
result = {
|
|
25780
|
+
ok: false,
|
|
25781
|
+
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
25782
|
+
};
|
|
25783
|
+
}
|
|
25784
|
+
cache.set(source, result);
|
|
25785
|
+
if (cache.size > 256) {
|
|
25786
|
+
const oldest = cache.keys().next().value;
|
|
25787
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
25788
|
+
}
|
|
25789
|
+
return result;
|
|
25790
|
+
}
|
|
25791
|
+
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
25792
|
+
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
25793
|
+
function compileExpressionSafe(source) {
|
|
25794
|
+
return getCached(source);
|
|
25795
|
+
}
|
|
25796
|
+
Object.freeze({});
|
|
25598
25797
|
/**
|
|
25599
|
-
*
|
|
25600
|
-
*
|
|
25601
|
-
*
|
|
25602
|
-
*
|
|
25603
|
-
*
|
|
25604
|
-
* of flooding the live event feed.
|
|
25798
|
+
* Author-time validation. Returns `null` when the source is valid, else a
|
|
25799
|
+
* human-readable error message. Checks: the expression compiles; binding count
|
|
25800
|
+
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
25801
|
+
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
25802
|
+
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
25605
25803
|
*/
|
|
25606
|
-
|
|
25607
|
-
|
|
25608
|
-
|
|
25609
|
-
|
|
25610
|
-
|
|
25611
|
-
|
|
25612
|
-
|
|
25613
|
-
|
|
25804
|
+
function validateExpressionSource(src) {
|
|
25805
|
+
const names = Object.keys(src.bindings);
|
|
25806
|
+
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
25807
|
+
for (const name of names) {
|
|
25808
|
+
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
25809
|
+
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
25810
|
+
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
25811
|
+
}
|
|
25812
|
+
const compiled = compileExpressionSafe(src.expr);
|
|
25813
|
+
if (!compiled.ok) return compiled.error;
|
|
25814
|
+
const bound = new Set(names);
|
|
25815
|
+
for (const id of compiled.parsed.identifiers) {
|
|
25816
|
+
if (id === "now") continue;
|
|
25817
|
+
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
25818
|
+
}
|
|
25819
|
+
return null;
|
|
25820
|
+
}
|
|
25821
|
+
var ExpressionBindingSourceSchema = union([
|
|
25822
|
+
object({
|
|
25823
|
+
kind: literal("field").optional(),
|
|
25824
|
+
sourceKey: string(),
|
|
25825
|
+
cap: string(),
|
|
25826
|
+
fieldPath: string()
|
|
25614
25827
|
}),
|
|
25615
|
-
|
|
25616
|
-
|
|
25617
|
-
|
|
25618
|
-
|
|
25619
|
-
|
|
25620
|
-
|
|
25621
|
-
|
|
25622
|
-
|
|
25623
|
-
|
|
25624
|
-
|
|
25625
|
-
|
|
25626
|
-
|
|
25627
|
-
|
|
25628
|
-
|
|
25629
|
-
|
|
25630
|
-
ts: number().int(),
|
|
25631
|
-
/** Frame width/height in pixels — let the UI normalize bbox coords. */
|
|
25632
|
-
frameWidth: number().int().nonnegative(),
|
|
25633
|
-
frameHeight: number().int().nonnegative(),
|
|
25634
|
-
/** Per-zone breakdown — one entry per defined zone (user + onboard). */
|
|
25635
|
-
zones: array(ZoneScopeBreakdownSchema).readonly(),
|
|
25636
|
-
/** Frame-wide aggregate (everywhere, regardless of zone membership).
|
|
25637
|
-
* INCLUDES currently-confirmed stationary objects (they are still present). */
|
|
25638
|
-
frame: PerScopeBreakdownSchema,
|
|
25639
|
-
/** Detections that landed outside every zone. Empty when no zones defined. */
|
|
25640
|
-
unzoned: PerScopeBreakdownSchema,
|
|
25641
|
-
/** Parked objects on this camera (additive — absent on legacy snapshots).
|
|
25642
|
-
* Surfaced separately so the UI shows them in a dedicated section rather
|
|
25643
|
-
* than as repeated tracks/events. */
|
|
25644
|
-
stationaryObjects: array(StationaryObjectSchema).readonly().optional()
|
|
25645
|
-
});
|
|
25646
|
-
/**
|
|
25647
|
-
* Time-series resolution. The history methods return one bucket per
|
|
25648
|
-
* step over the requested range. Smaller resolutions cost more
|
|
25649
|
-
* memory + bandwidth; bound to discrete steps so caller cannot ask
|
|
25650
|
-
* for arbitrary fractional buckets.
|
|
25651
|
-
*/
|
|
25652
|
-
var HistoryResolutionEnum = _enum([
|
|
25653
|
-
"minute",
|
|
25654
|
-
"5min",
|
|
25655
|
-
"hour"
|
|
25656
|
-
]);
|
|
25657
|
-
var HistoryRangeSchema = object({
|
|
25658
|
-
/** Range start (epoch ms, inclusive). */
|
|
25659
|
-
from: number().int(),
|
|
25660
|
-
/** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
|
|
25661
|
-
to: number().int(),
|
|
25662
|
-
resolution: HistoryResolutionEnum
|
|
25663
|
-
});
|
|
25664
|
-
var HistoryPointSchema = object({
|
|
25665
|
-
/** Bucket midpoint (epoch ms). */
|
|
25666
|
-
ts: number().int(),
|
|
25667
|
-
/** Object count averaged over the bucket (rounded to nearest integer). */
|
|
25668
|
-
count: number().int().nonnegative()
|
|
25669
|
-
});
|
|
25670
|
-
DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
|
|
25671
|
-
deviceId: number(),
|
|
25672
|
-
zoneId: string(),
|
|
25673
|
-
className: string().optional()
|
|
25674
|
-
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
|
|
25675
|
-
deviceId: number(),
|
|
25676
|
-
className: string().optional()
|
|
25677
|
-
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
|
|
25678
|
-
deviceId: number(),
|
|
25679
|
-
className: string().optional()
|
|
25680
|
-
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly());
|
|
25681
|
-
/**
|
|
25682
|
-
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
25683
|
-
* cap so a single CRUD surface backs every consumer; each stage has
|
|
25684
|
-
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
25685
|
-
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
25686
|
-
*
|
|
25687
|
-
* Extend the enum here when a new gating consumer comes online (audio
|
|
25688
|
-
* gating, alert filtering, …) — no other surface needs to change.
|
|
25689
|
-
*/
|
|
25690
|
-
var ZoneRuleStageEnum = _enum([
|
|
25691
|
-
"motion",
|
|
25692
|
-
"detection",
|
|
25693
|
-
"package"
|
|
25828
|
+
object({
|
|
25829
|
+
kind: literal("literal"),
|
|
25830
|
+
value: union([
|
|
25831
|
+
string(),
|
|
25832
|
+
number(),
|
|
25833
|
+
boolean(),
|
|
25834
|
+
_null()
|
|
25835
|
+
])
|
|
25836
|
+
}),
|
|
25837
|
+
object({
|
|
25838
|
+
kind: literal("global"),
|
|
25839
|
+
sourceStableId: string(),
|
|
25840
|
+
cap: string(),
|
|
25841
|
+
fieldPath: string()
|
|
25842
|
+
})
|
|
25694
25843
|
]);
|
|
25695
|
-
|
|
25696
|
-
|
|
25697
|
-
|
|
25698
|
-
})
|
|
25699
|
-
|
|
25700
|
-
|
|
25701
|
-
|
|
25702
|
-
|
|
25703
|
-
|
|
25704
|
-
|
|
25705
|
-
})
|
|
25706
|
-
motion: array(ZoneRuleSchema).readonly(),
|
|
25707
|
-
detection: array(ZoneRuleSchema).readonly(),
|
|
25708
|
-
package: array(ZoneRuleSchema).readonly()
|
|
25709
|
-
});
|
|
25710
|
-
var HF_BASE_URL = `https://huggingface.co/camstack/camstack-models/resolve/main`;
|
|
25711
|
-
/**
|
|
25712
|
-
* Accessory device helpers — shared across drivers.
|
|
25713
|
-
*
|
|
25714
|
-
* Many vendor-specific drivers register accessory child devices on
|
|
25715
|
-
* top of a parent (Reolink: siren / floodlight / PIR / autotrack /
|
|
25716
|
-
* chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
|
|
25717
|
-
* driver picks the right `DeviceType` + `DeviceRole` explicitly when
|
|
25718
|
-
* spawning, builds a name derived from the parent, and produces a
|
|
25719
|
-
* stableId tied to the parent so boot-restore can reconstruct the
|
|
25720
|
-
* relationship.
|
|
25721
|
-
*
|
|
25722
|
-
* Centralised `(kind → DeviceType)` mapping was dropped on purpose:
|
|
25723
|
-
* drivers may reasonably disagree on the right type for an accessory
|
|
25724
|
-
* (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
|
|
25725
|
-
* read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
|
|
25726
|
-
* one canonical mapping was over-prescriptive and added a layer of
|
|
25727
|
-
* indirection without saving meaningful code at call sites — the
|
|
25728
|
-
* driver knows its own hardware best.
|
|
25729
|
-
*/
|
|
25730
|
-
/**
|
|
25731
|
-
* Subset of `DeviceRole` values that drivers register as child
|
|
25732
|
-
* accessories of a parent device. Sourced verbatim from `DeviceRole`
|
|
25733
|
-
* — `AccessoryKind` is the alias drivers use when building accessory
|
|
25734
|
-
* children, so the call site reads as
|
|
25735
|
-
* `accessoryStableId(parent, AccessoryKind.Siren)` rather than
|
|
25736
|
-
* `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
|
|
25737
|
-
* any role works, including non-accessory ones like Doorbell).
|
|
25738
|
-
*/
|
|
25739
|
-
var AccessoryKind = {
|
|
25740
|
-
Siren: DeviceRole.Siren,
|
|
25741
|
-
Floodlight: DeviceRole.Floodlight,
|
|
25742
|
-
Spotlight: DeviceRole.Spotlight,
|
|
25743
|
-
PirSensor: DeviceRole.PirSensor,
|
|
25744
|
-
Chime: DeviceRole.Chime,
|
|
25745
|
-
Autotrack: DeviceRole.Autotrack,
|
|
25746
|
-
Nightvision: DeviceRole.Nightvision,
|
|
25747
|
-
PrivacyMask: DeviceRole.PrivacyMask
|
|
25748
|
-
};
|
|
25749
|
-
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
25750
|
-
DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder;
|
|
25751
|
-
new Set(Object.values(DeviceType));
|
|
25752
|
-
DeviceFeature.BatteryOperated;
|
|
25844
|
+
object({
|
|
25845
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
25846
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
25847
|
+
}).superRefine((src, ctx) => {
|
|
25848
|
+
const err = validateExpressionSource(src);
|
|
25849
|
+
if (err !== null) ctx.addIssue({
|
|
25850
|
+
code: "custom",
|
|
25851
|
+
message: err,
|
|
25852
|
+
path: ["expr"]
|
|
25853
|
+
});
|
|
25854
|
+
});
|
|
25753
25855
|
Object.freeze({
|
|
25754
25856
|
"accessories.setChildHidden": {
|
|
25755
25857
|
capName: "accessories",
|
|
@@ -26573,6 +26675,12 @@ Object.freeze({
|
|
|
26573
26675
|
addonId: null,
|
|
26574
26676
|
access: "view"
|
|
26575
26677
|
},
|
|
26678
|
+
"coreBlocks.restart": {
|
|
26679
|
+
capName: "core-blocks",
|
|
26680
|
+
capScope: "system",
|
|
26681
|
+
addonId: null,
|
|
26682
|
+
access: "create"
|
|
26683
|
+
},
|
|
26576
26684
|
"coreBlocks.setEnabled": {
|
|
26577
26685
|
capName: "core-blocks",
|
|
26578
26686
|
capScope: "system",
|
|
@@ -27209,12 +27317,6 @@ Object.freeze({
|
|
|
27209
27317
|
addonId: null,
|
|
27210
27318
|
access: "create"
|
|
27211
27319
|
},
|
|
27212
|
-
"deviceManager.setDeviceLinks": {
|
|
27213
|
-
capName: "device-manager",
|
|
27214
|
-
capScope: "system",
|
|
27215
|
-
addonId: null,
|
|
27216
|
-
access: "create"
|
|
27217
|
-
},
|
|
27218
27320
|
"deviceManager.setDisabled": {
|
|
27219
27321
|
capName: "device-manager",
|
|
27220
27322
|
capScope: "system",
|
|
@@ -30359,6 +30461,12 @@ Object.freeze({
|
|
|
30359
30461
|
addonId: null,
|
|
30360
30462
|
access: "create"
|
|
30361
30463
|
},
|
|
30464
|
+
"streamBroker.fetchEventMedia": {
|
|
30465
|
+
capName: "stream-broker",
|
|
30466
|
+
capScope: "system",
|
|
30467
|
+
addonId: null,
|
|
30468
|
+
access: "create"
|
|
30469
|
+
},
|
|
30362
30470
|
"streamBroker.getAllRtspEntries": {
|
|
30363
30471
|
capName: "stream-broker",
|
|
30364
30472
|
capScope: "system",
|
|
@@ -30443,6 +30551,12 @@ Object.freeze({
|
|
|
30443
30551
|
addonId: null,
|
|
30444
30552
|
access: "create"
|
|
30445
30553
|
},
|
|
30554
|
+
"streamBroker.produceEventMedia": {
|
|
30555
|
+
capName: "stream-broker",
|
|
30556
|
+
capScope: "system",
|
|
30557
|
+
addonId: null,
|
|
30558
|
+
access: "create"
|
|
30559
|
+
},
|
|
30446
30560
|
"streamBroker.publishCameraStream": {
|
|
30447
30561
|
capName: "stream-broker",
|
|
30448
30562
|
capScope: "system",
|
|
@@ -31183,249 +31297,62 @@ var DeviceIdsField = array(number()).min(1);
|
|
|
31183
31297
|
var CadenceSecField = number().int().min(2).max(3600);
|
|
31184
31298
|
var FramerateField = number().int().min(1).max(60);
|
|
31185
31299
|
var TargetsField = array(NcRuleTargetSchema).min(1);
|
|
31186
|
-
var PriorityField = number().int().min(1).max(5);
|
|
31187
|
-
/**
|
|
31188
|
-
* Client-supplied timelapse-rule fields. The server stamps id / createdBy /
|
|
31189
|
-
* createdAt / updatedAt / ownerUserId / lastGeneratedAt — none of them appear
|
|
31190
|
-
* here (see the ownership note above).
|
|
31191
|
-
*/
|
|
31192
|
-
var TimelapseRuleInputSchema = object({
|
|
31193
|
-
name: NameField,
|
|
31194
|
-
enabled: boolean().default(true),
|
|
31195
|
-
/** Cameras sampled by this rule — one scratch dir + one artifact per device. */
|
|
31196
|
-
deviceIds: DeviceIdsField,
|
|
31197
|
-
/**
|
|
31198
|
-
* Activation window(s). REQUIRED (unlike `NcRule`, where an absent schedule
|
|
31199
|
-
* means "always active"): a timelapse is defined by its window boundaries —
|
|
31200
|
-
* open clears the scratch, close assembles and delivers.
|
|
31201
|
-
*/
|
|
31202
|
-
schedule: NcScheduleSchema,
|
|
31203
|
-
/** Force-snapshot cadence inside the window, seconds (predecessor parity). */
|
|
31204
|
-
cadenceSec: CadenceSecField.default(15),
|
|
31205
|
-
/** Output frames per second of the assembled mp4 (predecessor parity). */
|
|
31206
|
-
framerate: FramerateField.default(10),
|
|
31207
|
-
/** `notification-output` targets the finished video/thumbnail is sent to. */
|
|
31208
|
-
targets: TargetsField,
|
|
31209
|
-
template: TimelapseTemplateSchema.optional(),
|
|
31210
|
-
/** Canonical notification priority ordinal (1..5); per-target overridable. */
|
|
31211
|
-
priority: PriorityField.default(3)
|
|
31212
|
-
});
|
|
31213
|
-
object({
|
|
31214
|
-
name: NameField.optional(),
|
|
31215
|
-
enabled: boolean().optional(),
|
|
31216
|
-
deviceIds: DeviceIdsField.optional(),
|
|
31217
|
-
schedule: NcScheduleSchema.optional(),
|
|
31218
|
-
cadenceSec: CadenceSecField.optional(),
|
|
31219
|
-
framerate: FramerateField.optional(),
|
|
31220
|
-
targets: TargetsField.optional(),
|
|
31221
|
-
template: TimelapseTemplateSchema.nullable().optional(),
|
|
31222
|
-
priority: PriorityField.optional()
|
|
31223
|
-
});
|
|
31224
|
-
TimelapseRuleInputSchema.extend({
|
|
31225
|
-
id: string(),
|
|
31226
|
-
/**
|
|
31227
|
-
* Ownership/visibility key. Absent = admin/global rule (visible to all).
|
|
31228
|
-
* Present = personal rule owned by this userId. Server-stamped from the
|
|
31229
|
-
* resolved caller; never trusted from a client payload.
|
|
31230
|
-
*/
|
|
31231
|
-
ownerUserId: string().optional(),
|
|
31232
|
-
/**
|
|
31233
|
-
* Epoch-ms of the last successful generation — the 1-hour re-generation
|
|
31234
|
-
* guard's durable state (predecessor parity). Absent = never generated.
|
|
31235
|
-
*/
|
|
31236
|
-
lastGeneratedAt: number().optional(),
|
|
31237
|
-
/** userId of the caller who created the rule (server-stamped). */
|
|
31238
|
-
createdBy: string(),
|
|
31239
|
-
createdAt: number(),
|
|
31240
|
-
updatedAt: number()
|
|
31241
|
-
});
|
|
31242
|
-
function hfModelUrl(repo, path) {
|
|
31243
|
-
return `https://huggingface.co/${repo}/resolve/main/${path}`;
|
|
31244
|
-
}
|
|
31245
|
-
/**
|
|
31246
|
-
* Names that, when used as URL query parameters, almost certainly carry
|
|
31247
|
-
* a secret. Matching is case-insensitive and anchored to the full param
|
|
31248
|
-
* name to avoid false positives like `user_id` (an entity reference, not
|
|
31249
|
-
* a credential) or `topic=password-reset` (a value happens to contain
|
|
31250
|
-
* the substring "password" but the param name is unrelated).
|
|
31251
|
-
*
|
|
31252
|
-
* Add new patterns here as we encounter cameras/APIs that leak through
|
|
31253
|
-
* the existing list.
|
|
31254
|
-
*/
|
|
31255
|
-
var CREDENTIAL_PARAM_PATTERNS = [
|
|
31256
|
-
/^user$/i,
|
|
31257
|
-
/^usr$/i,
|
|
31258
|
-
/^username$/i,
|
|
31259
|
-
/^password$/i,
|
|
31260
|
-
/^pwd$/i,
|
|
31261
|
-
/^pass$/i,
|
|
31262
|
-
/^passwd$/i,
|
|
31263
|
-
/^secret$/i,
|
|
31264
|
-
/^token$/i,
|
|
31265
|
-
/^auth$/i,
|
|
31266
|
-
/^auth[_-]?token$/i,
|
|
31267
|
-
/^bearer$/i,
|
|
31268
|
-
/^api[_-]?key$/i,
|
|
31269
|
-
/^access[_-]?token$/i,
|
|
31270
|
-
/^refresh[_-]?token$/i
|
|
31271
|
-
];
|
|
31272
|
-
function isCredentialParam(name) {
|
|
31273
|
-
return CREDENTIAL_PARAM_PATTERNS.some((re) => re.test(name));
|
|
31274
|
-
}
|
|
31275
|
-
/**
|
|
31276
|
-
* Redact secrets we may surface in logs or telemetry:
|
|
31277
|
-
* 1. `userinfo` (`user:password@host`) — the classic RTSP shape.
|
|
31278
|
-
* 2. Query-string credential params (`?user=...&password=...`) — the
|
|
31279
|
-
* Reolink RTMP shape and several HTTP-pushed-stream APIs.
|
|
31280
|
-
*
|
|
31281
|
-
* Returns the original string verbatim when the input is not a parseable
|
|
31282
|
-
* URL — a log helper must never throw. Param values are replaced with
|
|
31283
|
-
* `***` so an operator reading logs still sees WHICH credential was
|
|
31284
|
-
* present (useful for debugging "wrong password" issues) without
|
|
31285
|
-
* exposing the value.
|
|
31286
|
-
*
|
|
31287
|
-
* The credential-name match is case-insensitive but anchored to the
|
|
31288
|
-
* whole param name; values like `user_id=42` or `topic=password-reset`
|
|
31289
|
-
* are NOT masked (those are entity references, not credentials).
|
|
31290
|
-
*/
|
|
31291
|
-
function maskUrlCredentials(rawUrl) {
|
|
31292
|
-
try {
|
|
31293
|
-
const u = new URL(rawUrl);
|
|
31294
|
-
let touched = false;
|
|
31295
|
-
if (u.username || u.password) {
|
|
31296
|
-
u.username = "";
|
|
31297
|
-
u.password = "";
|
|
31298
|
-
touched = true;
|
|
31299
|
-
}
|
|
31300
|
-
for (const key of Array.from(u.searchParams.keys())) if (isCredentialParam(key)) {
|
|
31301
|
-
u.searchParams.set(key, "***");
|
|
31302
|
-
touched = true;
|
|
31303
|
-
}
|
|
31304
|
-
return touched ? u.toString() : rawUrl;
|
|
31305
|
-
} catch {
|
|
31306
|
-
return rawUrl;
|
|
31307
|
-
}
|
|
31308
|
-
}
|
|
31309
|
-
/**
|
|
31310
|
-
* Fixed-capacity ring buffer. When full, push() overwrites the oldest entry.
|
|
31311
|
-
* drain() returns up to maxCount items in FIFO order and removes them.
|
|
31312
|
-
*/
|
|
31313
|
-
var RingBuffer = class {
|
|
31314
|
-
capacity;
|
|
31315
|
-
items;
|
|
31316
|
-
head = 0;
|
|
31317
|
-
tail = 0;
|
|
31318
|
-
count = 0;
|
|
31319
|
-
constructor(capacity) {
|
|
31320
|
-
this.capacity = capacity;
|
|
31321
|
-
this.items = Array.from({ length: capacity });
|
|
31322
|
-
}
|
|
31323
|
-
get size() {
|
|
31324
|
-
return this.count;
|
|
31325
|
-
}
|
|
31326
|
-
push(item) {
|
|
31327
|
-
this.items[this.tail] = item;
|
|
31328
|
-
this.tail = (this.tail + 1) % this.capacity;
|
|
31329
|
-
if (this.count < this.capacity) this.count++;
|
|
31330
|
-
else this.head = (this.head + 1) % this.capacity;
|
|
31331
|
-
}
|
|
31332
|
-
drain(maxCount) {
|
|
31333
|
-
const result = [];
|
|
31334
|
-
const n = Math.min(maxCount, this.count);
|
|
31335
|
-
for (let i = 0; i < n; i++) {
|
|
31336
|
-
result.push(this.items[this.head]);
|
|
31337
|
-
this.items[this.head] = void 0;
|
|
31338
|
-
this.head = (this.head + 1) % this.capacity;
|
|
31339
|
-
}
|
|
31340
|
-
this.count -= n;
|
|
31341
|
-
return result;
|
|
31342
|
-
}
|
|
31343
|
-
};
|
|
31344
|
-
/**
|
|
31345
|
-
* DEVICE-POOL backend → model format — the SSOT for the multi-device inference
|
|
31346
|
-
* DESCRIPTOR path (Python per-device pools), distinct from {@link BACKEND_TO_FORMAT}
|
|
31347
|
-
* above which is the legacy NODE runtime map (where `coreml → onnx` because
|
|
31348
|
-
* onnxruntime-node's CoreML EP loads `.onnx`). Here a device pool runs the
|
|
31349
|
-
* backend NATIVELY, so `coreml → coreml` and `edgetpu → tflite`. Consumed by
|
|
31350
|
-
* `resolveDeviceEngine` (addon-pipeline) and the orchestrator's
|
|
31351
|
-
* `parseDeviceKey`/`getNodeInferenceDevices` — the previously-duplicated
|
|
31352
|
-
* `BACKEND_FORMAT` maps (R3/node-F2). `cpu`/`cuda` floor to onnx.
|
|
31353
|
-
*/
|
|
31354
|
-
var DEVICE_BACKEND_TO_FORMAT = {
|
|
31355
|
-
openvino: "openvino",
|
|
31356
|
-
edgetpu: "tflite",
|
|
31357
|
-
coreml: "coreml",
|
|
31358
|
-
onnx: "onnx",
|
|
31359
|
-
cpu: "onnx",
|
|
31360
|
-
cuda: "onnx"
|
|
31361
|
-
};
|
|
31362
|
-
/**
|
|
31363
|
-
* Evaluate `rules` against `items`. Returns the items partitioned
|
|
31364
|
-
* into `passed` (kept by the gate) and `excluded` (dropped). When
|
|
31365
|
-
* `rules` is empty or `zones` is empty everything passes — the gate
|
|
31366
|
-
* is "off" without explicit configuration.
|
|
31367
|
-
*
|
|
31368
|
-
* `getCenter` returns the item's normalised (0–1) center point.
|
|
31369
|
-
* `getClassName` is optional — when omitted, every rule's
|
|
31370
|
-
* `classFilter` is treated as matching all classes (motion-stage
|
|
31371
|
-
* semantics). Provide it for object detections so per-class rules
|
|
31372
|
-
* narrow correctly.
|
|
31373
|
-
*/
|
|
31374
|
-
function evaluateZoneRules(items, zones, rules, getCenter, getClassName) {
|
|
31375
|
-
if (rules.length === 0 || zones.length === 0) return {
|
|
31376
|
-
passed: items,
|
|
31377
|
-
excluded: []
|
|
31378
|
-
};
|
|
31379
|
-
const activeRules = rules.filter((r) => r.enabled !== false);
|
|
31380
|
-
const includeRules = activeRules.filter((r) => r.mode === "include");
|
|
31381
|
-
const excludeRules = activeRules.filter((r) => r.mode === "exclude");
|
|
31382
|
-
const whitelistMode = includeRules.length > 0;
|
|
31383
|
-
const zonesById = new Map(zones.map((z) => [z.id, z]));
|
|
31384
|
-
const passed = [];
|
|
31385
|
-
const excluded = [];
|
|
31386
|
-
for (const item of items) {
|
|
31387
|
-
const center = getCenter(item);
|
|
31388
|
-
const className = getClassName?.(item);
|
|
31389
|
-
const inInclude = includeRules.some((r) => ruleApplies(r, center, className, zonesById));
|
|
31390
|
-
const inExclude = excludeRules.some((r) => ruleApplies(r, center, className, zonesById));
|
|
31391
|
-
if (whitelistMode) if (inInclude && !inExclude) passed.push(item);
|
|
31392
|
-
else excluded.push(item);
|
|
31393
|
-
else if (inExclude) excluded.push(item);
|
|
31394
|
-
else passed.push(item);
|
|
31395
|
-
}
|
|
31396
|
-
return {
|
|
31397
|
-
passed,
|
|
31398
|
-
excluded
|
|
31399
|
-
};
|
|
31400
|
-
}
|
|
31401
|
-
function ruleApplies(rule, center, className, zonesById) {
|
|
31402
|
-
if (rule.classFilter && rule.classFilter.length > 0) {
|
|
31403
|
-
if (className !== void 0 && !rule.classFilter.includes(className)) return false;
|
|
31404
|
-
}
|
|
31405
|
-
for (const zoneId of rule.zoneIds) {
|
|
31406
|
-
const zone = zonesById.get(zoneId);
|
|
31407
|
-
if (!zone) continue;
|
|
31408
|
-
if (pointInPolygon(center, zone.polygon)) return true;
|
|
31409
|
-
}
|
|
31410
|
-
return false;
|
|
31411
|
-
}
|
|
31300
|
+
var PriorityField = number().int().min(1).max(5);
|
|
31412
31301
|
/**
|
|
31413
|
-
*
|
|
31414
|
-
*
|
|
31415
|
-
*
|
|
31416
|
-
* the y-edge crossing).
|
|
31302
|
+
* Client-supplied timelapse-rule fields. The server stamps id / createdBy /
|
|
31303
|
+
* createdAt / updatedAt / ownerUserId / lastGeneratedAt — none of them appear
|
|
31304
|
+
* here (see the ownership note above).
|
|
31417
31305
|
*/
|
|
31418
|
-
|
|
31419
|
-
|
|
31420
|
-
|
|
31421
|
-
|
|
31422
|
-
|
|
31423
|
-
|
|
31424
|
-
|
|
31425
|
-
|
|
31426
|
-
|
|
31427
|
-
|
|
31428
|
-
|
|
31306
|
+
var TimelapseRuleInputSchema = object({
|
|
31307
|
+
name: NameField,
|
|
31308
|
+
enabled: boolean().default(true),
|
|
31309
|
+
/** Cameras sampled by this rule — one scratch dir + one artifact per device. */
|
|
31310
|
+
deviceIds: DeviceIdsField,
|
|
31311
|
+
/**
|
|
31312
|
+
* Activation window(s). REQUIRED (unlike `NcRule`, where an absent schedule
|
|
31313
|
+
* means "always active"): a timelapse is defined by its window boundaries —
|
|
31314
|
+
* open clears the scratch, close assembles and delivers.
|
|
31315
|
+
*/
|
|
31316
|
+
schedule: NcScheduleSchema,
|
|
31317
|
+
/** Force-snapshot cadence inside the window, seconds (predecessor parity). */
|
|
31318
|
+
cadenceSec: CadenceSecField.default(15),
|
|
31319
|
+
/** Output frames per second of the assembled mp4 (predecessor parity). */
|
|
31320
|
+
framerate: FramerateField.default(10),
|
|
31321
|
+
/** `notification-output` targets the finished video/thumbnail is sent to. */
|
|
31322
|
+
targets: TargetsField,
|
|
31323
|
+
template: TimelapseTemplateSchema.optional(),
|
|
31324
|
+
/** Canonical notification priority ordinal (1..5); per-target overridable. */
|
|
31325
|
+
priority: PriorityField.default(3)
|
|
31326
|
+
});
|
|
31327
|
+
object({
|
|
31328
|
+
name: NameField.optional(),
|
|
31329
|
+
enabled: boolean().optional(),
|
|
31330
|
+
deviceIds: DeviceIdsField.optional(),
|
|
31331
|
+
schedule: NcScheduleSchema.optional(),
|
|
31332
|
+
cadenceSec: CadenceSecField.optional(),
|
|
31333
|
+
framerate: FramerateField.optional(),
|
|
31334
|
+
targets: TargetsField.optional(),
|
|
31335
|
+
template: TimelapseTemplateSchema.nullable().optional(),
|
|
31336
|
+
priority: PriorityField.optional()
|
|
31337
|
+
});
|
|
31338
|
+
TimelapseRuleInputSchema.extend({
|
|
31339
|
+
id: string(),
|
|
31340
|
+
/**
|
|
31341
|
+
* Ownership/visibility key. Absent = admin/global rule (visible to all).
|
|
31342
|
+
* Present = personal rule owned by this userId. Server-stamped from the
|
|
31343
|
+
* resolved caller; never trusted from a client payload.
|
|
31344
|
+
*/
|
|
31345
|
+
ownerUserId: string().optional(),
|
|
31346
|
+
/**
|
|
31347
|
+
* Epoch-ms of the last successful generation — the 1-hour re-generation
|
|
31348
|
+
* guard's durable state (predecessor parity). Absent = never generated.
|
|
31349
|
+
*/
|
|
31350
|
+
lastGeneratedAt: number().optional(),
|
|
31351
|
+
/** userId of the caller who created the rule (server-stamped). */
|
|
31352
|
+
createdBy: string(),
|
|
31353
|
+
createdAt: number(),
|
|
31354
|
+
updatedAt: number()
|
|
31355
|
+
});
|
|
31429
31356
|
var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
|
|
31430
31357
|
var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
|
|
31431
31358
|
/**
|
|
@@ -31744,6 +31671,193 @@ function pickNativeLeaseOverride(view) {
|
|
|
31744
31671
|
}
|
|
31745
31672
|
return readNativeLeaseOverride(flat);
|
|
31746
31673
|
}
|
|
31674
|
+
function hfModelUrl(repo, path) {
|
|
31675
|
+
return `https://huggingface.co/${repo}/resolve/main/${path}`;
|
|
31676
|
+
}
|
|
31677
|
+
/**
|
|
31678
|
+
* Names that, when used as URL query parameters, almost certainly carry
|
|
31679
|
+
* a secret. Matching is case-insensitive and anchored to the full param
|
|
31680
|
+
* name to avoid false positives like `user_id` (an entity reference, not
|
|
31681
|
+
* a credential) or `topic=password-reset` (a value happens to contain
|
|
31682
|
+
* the substring "password" but the param name is unrelated).
|
|
31683
|
+
*
|
|
31684
|
+
* Add new patterns here as we encounter cameras/APIs that leak through
|
|
31685
|
+
* the existing list.
|
|
31686
|
+
*/
|
|
31687
|
+
var CREDENTIAL_PARAM_PATTERNS = [
|
|
31688
|
+
/^user$/i,
|
|
31689
|
+
/^usr$/i,
|
|
31690
|
+
/^username$/i,
|
|
31691
|
+
/^password$/i,
|
|
31692
|
+
/^pwd$/i,
|
|
31693
|
+
/^pass$/i,
|
|
31694
|
+
/^passwd$/i,
|
|
31695
|
+
/^secret$/i,
|
|
31696
|
+
/^token$/i,
|
|
31697
|
+
/^auth$/i,
|
|
31698
|
+
/^auth[_-]?token$/i,
|
|
31699
|
+
/^bearer$/i,
|
|
31700
|
+
/^api[_-]?key$/i,
|
|
31701
|
+
/^access[_-]?token$/i,
|
|
31702
|
+
/^refresh[_-]?token$/i
|
|
31703
|
+
];
|
|
31704
|
+
function isCredentialParam(name) {
|
|
31705
|
+
return CREDENTIAL_PARAM_PATTERNS.some((re) => re.test(name));
|
|
31706
|
+
}
|
|
31707
|
+
/**
|
|
31708
|
+
* Redact secrets we may surface in logs or telemetry:
|
|
31709
|
+
* 1. `userinfo` (`user:password@host`) — the classic RTSP shape.
|
|
31710
|
+
* 2. Query-string credential params (`?user=...&password=...`) — the
|
|
31711
|
+
* Reolink RTMP shape and several HTTP-pushed-stream APIs.
|
|
31712
|
+
*
|
|
31713
|
+
* Returns the original string verbatim when the input is not a parseable
|
|
31714
|
+
* URL — a log helper must never throw. Param values are replaced with
|
|
31715
|
+
* `***` so an operator reading logs still sees WHICH credential was
|
|
31716
|
+
* present (useful for debugging "wrong password" issues) without
|
|
31717
|
+
* exposing the value.
|
|
31718
|
+
*
|
|
31719
|
+
* The credential-name match is case-insensitive but anchored to the
|
|
31720
|
+
* whole param name; values like `user_id=42` or `topic=password-reset`
|
|
31721
|
+
* are NOT masked (those are entity references, not credentials).
|
|
31722
|
+
*/
|
|
31723
|
+
function maskUrlCredentials(rawUrl) {
|
|
31724
|
+
try {
|
|
31725
|
+
const u = new URL(rawUrl);
|
|
31726
|
+
let touched = false;
|
|
31727
|
+
if (u.username || u.password) {
|
|
31728
|
+
u.username = "";
|
|
31729
|
+
u.password = "";
|
|
31730
|
+
touched = true;
|
|
31731
|
+
}
|
|
31732
|
+
for (const key of Array.from(u.searchParams.keys())) if (isCredentialParam(key)) {
|
|
31733
|
+
u.searchParams.set(key, "***");
|
|
31734
|
+
touched = true;
|
|
31735
|
+
}
|
|
31736
|
+
return touched ? u.toString() : rawUrl;
|
|
31737
|
+
} catch {
|
|
31738
|
+
return rawUrl;
|
|
31739
|
+
}
|
|
31740
|
+
}
|
|
31741
|
+
/**
|
|
31742
|
+
* Fixed-capacity ring buffer. When full, push() overwrites the oldest entry.
|
|
31743
|
+
* drain() returns up to maxCount items in FIFO order and removes them.
|
|
31744
|
+
*/
|
|
31745
|
+
var RingBuffer = class {
|
|
31746
|
+
capacity;
|
|
31747
|
+
items;
|
|
31748
|
+
head = 0;
|
|
31749
|
+
tail = 0;
|
|
31750
|
+
count = 0;
|
|
31751
|
+
constructor(capacity) {
|
|
31752
|
+
this.capacity = capacity;
|
|
31753
|
+
this.items = Array.from({ length: capacity });
|
|
31754
|
+
}
|
|
31755
|
+
get size() {
|
|
31756
|
+
return this.count;
|
|
31757
|
+
}
|
|
31758
|
+
push(item) {
|
|
31759
|
+
this.items[this.tail] = item;
|
|
31760
|
+
this.tail = (this.tail + 1) % this.capacity;
|
|
31761
|
+
if (this.count < this.capacity) this.count++;
|
|
31762
|
+
else this.head = (this.head + 1) % this.capacity;
|
|
31763
|
+
}
|
|
31764
|
+
drain(maxCount) {
|
|
31765
|
+
const result = [];
|
|
31766
|
+
const n = Math.min(maxCount, this.count);
|
|
31767
|
+
for (let i = 0; i < n; i++) {
|
|
31768
|
+
result.push(this.items[this.head]);
|
|
31769
|
+
this.items[this.head] = void 0;
|
|
31770
|
+
this.head = (this.head + 1) % this.capacity;
|
|
31771
|
+
}
|
|
31772
|
+
this.count -= n;
|
|
31773
|
+
return result;
|
|
31774
|
+
}
|
|
31775
|
+
};
|
|
31776
|
+
/**
|
|
31777
|
+
* DEVICE-POOL backend → model format — the SSOT for the multi-device inference
|
|
31778
|
+
* DESCRIPTOR path (Python per-device pools), distinct from {@link BACKEND_TO_FORMAT}
|
|
31779
|
+
* above which is the legacy NODE runtime map (where `coreml → onnx` because
|
|
31780
|
+
* onnxruntime-node's CoreML EP loads `.onnx`). Here a device pool runs the
|
|
31781
|
+
* backend NATIVELY, so `coreml → coreml` and `edgetpu → tflite`. Consumed by
|
|
31782
|
+
* `resolveDeviceEngine` (addon-pipeline) and the orchestrator's
|
|
31783
|
+
* `parseDeviceKey`/`getNodeInferenceDevices` — the previously-duplicated
|
|
31784
|
+
* `BACKEND_FORMAT` maps (R3/node-F2). `cpu`/`cuda` floor to onnx.
|
|
31785
|
+
*/
|
|
31786
|
+
var DEVICE_BACKEND_TO_FORMAT = {
|
|
31787
|
+
openvino: "openvino",
|
|
31788
|
+
edgetpu: "tflite",
|
|
31789
|
+
coreml: "coreml",
|
|
31790
|
+
onnx: "onnx",
|
|
31791
|
+
cpu: "onnx",
|
|
31792
|
+
cuda: "onnx"
|
|
31793
|
+
};
|
|
31794
|
+
/**
|
|
31795
|
+
* Evaluate `rules` against `items`. Returns the items partitioned
|
|
31796
|
+
* into `passed` (kept by the gate) and `excluded` (dropped). When
|
|
31797
|
+
* `rules` is empty or `zones` is empty everything passes — the gate
|
|
31798
|
+
* is "off" without explicit configuration.
|
|
31799
|
+
*
|
|
31800
|
+
* `getCenter` returns the item's normalised (0–1) center point.
|
|
31801
|
+
* `getClassName` is optional — when omitted, every rule's
|
|
31802
|
+
* `classFilter` is treated as matching all classes (motion-stage
|
|
31803
|
+
* semantics). Provide it for object detections so per-class rules
|
|
31804
|
+
* narrow correctly.
|
|
31805
|
+
*/
|
|
31806
|
+
function evaluateZoneRules(items, zones, rules, getCenter, getClassName) {
|
|
31807
|
+
if (rules.length === 0 || zones.length === 0) return {
|
|
31808
|
+
passed: items,
|
|
31809
|
+
excluded: []
|
|
31810
|
+
};
|
|
31811
|
+
const activeRules = rules.filter((r) => r.enabled !== false);
|
|
31812
|
+
const includeRules = activeRules.filter((r) => r.mode === "include");
|
|
31813
|
+
const excludeRules = activeRules.filter((r) => r.mode === "exclude");
|
|
31814
|
+
const whitelistMode = includeRules.length > 0;
|
|
31815
|
+
const zonesById = new Map(zones.map((z) => [z.id, z]));
|
|
31816
|
+
const passed = [];
|
|
31817
|
+
const excluded = [];
|
|
31818
|
+
for (const item of items) {
|
|
31819
|
+
const center = getCenter(item);
|
|
31820
|
+
const className = getClassName?.(item);
|
|
31821
|
+
const inInclude = includeRules.some((r) => ruleApplies(r, center, className, zonesById));
|
|
31822
|
+
const inExclude = excludeRules.some((r) => ruleApplies(r, center, className, zonesById));
|
|
31823
|
+
if (whitelistMode) if (inInclude && !inExclude) passed.push(item);
|
|
31824
|
+
else excluded.push(item);
|
|
31825
|
+
else if (inExclude) excluded.push(item);
|
|
31826
|
+
else passed.push(item);
|
|
31827
|
+
}
|
|
31828
|
+
return {
|
|
31829
|
+
passed,
|
|
31830
|
+
excluded
|
|
31831
|
+
};
|
|
31832
|
+
}
|
|
31833
|
+
function ruleApplies(rule, center, className, zonesById) {
|
|
31834
|
+
if (rule.classFilter && rule.classFilter.length > 0) {
|
|
31835
|
+
if (className !== void 0 && !rule.classFilter.includes(className)) return false;
|
|
31836
|
+
}
|
|
31837
|
+
for (const zoneId of rule.zoneIds) {
|
|
31838
|
+
const zone = zonesById.get(zoneId);
|
|
31839
|
+
if (!zone) continue;
|
|
31840
|
+
if (pointInPolygon(center, zone.polygon)) return true;
|
|
31841
|
+
}
|
|
31842
|
+
return false;
|
|
31843
|
+
}
|
|
31844
|
+
/**
|
|
31845
|
+
* Standard ray-casting point-in-polygon. Polygon vertices are
|
|
31846
|
+
* normalised (0–1); the test point is too — frame-resolution-
|
|
31847
|
+
* agnostic. Boundary points count as inside (treats `<` as `<=` on
|
|
31848
|
+
* the y-edge crossing).
|
|
31849
|
+
*/
|
|
31850
|
+
function pointInPolygon(point, polygon) {
|
|
31851
|
+
if (polygon.length < 3) return false;
|
|
31852
|
+
let inside = false;
|
|
31853
|
+
const { x, y } = point;
|
|
31854
|
+
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
|
|
31855
|
+
const a = polygon[i];
|
|
31856
|
+
const b = polygon[j];
|
|
31857
|
+
if (a.y > y !== b.y > y && x < (b.x - a.x) * (y - a.y) / (b.y - a.y || Number.EPSILON) + a.x) inside = !inside;
|
|
31858
|
+
}
|
|
31859
|
+
return inside;
|
|
31860
|
+
}
|
|
31747
31861
|
var AUTO = {
|
|
31748
31862
|
value: "auto",
|
|
31749
31863
|
label: "Auto"
|