@camstack/addon-admin-ui 1.2.35 → 1.2.37
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/assets/{AddLocationWizard-BpkZ7g8M.js → AddLocationWizard-ruAb2ySj.js} +1 -1
- package/dist/assets/AddonCollectionPage-y9PlVqw3.js +1 -0
- package/dist/assets/Addons-cAsBu2OQ.js +1 -0
- package/dist/assets/{AdminTabs-BzqgdZo8.js → AdminTabs-BVHcviOJ.js} +1 -1
- package/dist/assets/{AiProfiles-Du2Gut1p.js → AiProfiles-B6EskvNu.js} +1 -1
- package/dist/assets/{BrokerForm-en5xEKn5.js → BrokerForm-DIw2g_KM.js} +1 -1
- package/dist/assets/Build-sVmfeGFf.js +29 -0
- package/dist/assets/Capabilities-CTlKJVid.js +1 -0
- package/dist/assets/{CapabilityBadges-2-jFnJcJ.js → CapabilityBadges-B_ZC7k04.js} +1 -1
- package/dist/assets/Cluster-DkXZdWpm.js +1 -0
- package/dist/assets/{ClusterRecordingTab-BdEH4zMe.js → ClusterRecordingTab-U0cdF9UF.js} +1 -1
- package/dist/assets/{Dashboard-Bi7LNLdP.js → Dashboard-BCNcg--7.js} +1 -1
- package/dist/assets/Data-ZtVnR1Fe.js +1 -0
- package/dist/assets/DetectionIntelligence-BlZl9E8S.js +1 -0
- package/dist/assets/{DeviceAggregateSection-D8oQ4avR.js → DeviceAggregateSection-UxtJHE5B.js} +1 -1
- package/dist/assets/DeviceDetail-BbYvgUSY.js +2 -0
- package/dist/assets/{Devices-BmmGXj4K.js → Devices-DzLVWcFg.js} +1 -1
- package/dist/assets/{EmbedPlayerPage-CRkfP_Ng.js → EmbedPlayerPage-C5yT7D0W.js} +1 -1
- package/dist/assets/Events-BGJ5su4V.js +1 -0
- package/dist/assets/FormBuilder-C0N6NRfe.js +1 -0
- package/dist/assets/Identity-FI4f8ox5.js +1 -0
- package/dist/assets/IntegrationDetail-DxAOxN6t.js +1 -0
- package/dist/assets/Integrations-D5S4Yf1A.js +1 -0
- package/dist/assets/{Integrations-CsuR-wnp.js → Integrations-w3RuUxDV.js} +1 -1
- package/dist/assets/{Logs-D-v1pY1C.js → Logs-BfDIf7QZ.js} +1 -1
- package/dist/assets/{MotionZonesSettings-DSAuUczY.js → MotionZonesSettings-CHyWV5lm.js} +1 -1
- package/dist/assets/{MyAccess-CSduDxGh.js → MyAccess-76Y7QhMu.js} +1 -1
- package/dist/assets/Network-BAUdETHi.js +1 -0
- package/dist/assets/{NodeAddonsSettingsPanel-0BTA46bg.js → NodeAddonsSettingsPanel-CeWEr-Zg.js} +1 -1
- package/dist/assets/NodeInferenceDevices-CUinkSSN.js +1 -0
- package/dist/assets/Notifications-CK3PuxFQ.js +1 -0
- package/dist/assets/Pipeline-CTSS1aGu.js +1 -0
- package/dist/assets/Plates-Cns09Gg2.js +1 -0
- package/dist/assets/{PrivacyMaskSettings-CfGREDOs.js → PrivacyMaskSettings-C4ycA7Un.js} +1 -1
- package/dist/assets/{ProviderIcon-mSy1d917.js → ProviderIcon-DyK-gNEs.js} +1 -1
- package/dist/assets/Recordings-UAlZznSu.js +1 -0
- package/dist/assets/{ScanNetworkModal-DsEiGH4q.js → ScanNetworkModal-h3WtjMhA.js} +1 -1
- package/dist/assets/{Settings-_QzMAEQz.js → Settings-oupU5GDf.js} +1 -1
- package/dist/assets/{Showroom-BrqmuRQr.js → Showroom-HqqCUr23.js} +1 -1
- package/dist/assets/{Terminal-D6ADzUw6.js → Terminal-7B0qx8ho.js} +2 -2
- package/dist/assets/{_virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-D6JCOoeB.js → _virtual_mf-localSharedImportMap___mfe_internal__admin_ui_host-BnceOEA8.js} +1 -1
- package/dist/assets/authed-media-B8T1dsFh.js +1 -0
- package/dist/assets/{rotate-ccw-RsWJHCWz.js → boxes-BS6UzXs9.js} +1 -1
- package/dist/assets/{build-cluster-graph-DK8tHUpQ.js → build-cluster-graph-CO-MCBmD.js} +2 -2
- package/dist/assets/circle-check-big-DKZCFZff.js +1 -0
- package/dist/assets/clock-DqPUDB1Y.js +1 -0
- package/dist/assets/{copy-CiHOHvWK.js → copy-bKWchDUS.js} +1 -1
- package/dist/assets/debounced-invalidate-CoCA-chF.js +1 -0
- package/dist/assets/dist-B66gZe27.js +7 -0
- package/dist/assets/{dist-CD_UFqu_.js → dist-CXHmz2j1.js} +1 -1
- package/dist/assets/{download-BbJFh_GP.js → download-C3-lXLSJ.js} +1 -1
- package/dist/assets/{face-bbox-DFwfVPg5.js → face-bbox-Cb2eJK0t.js} +1 -1
- package/dist/assets/{hostInit-Gt7MZSrm.js → hostInit-DtkE57IB.js} +1 -1
- package/dist/assets/index-Bke_0boK.js +4 -0
- package/dist/assets/index-mdJi1UIz.css +2 -0
- package/dist/assets/{key-round-wDYpv-03.js → key-round-CSNhr2jN.js} +1 -1
- package/dist/assets/{mf-entry-bootstrap-0-eafb02b4.js → mf-entry-bootstrap-0-e0821d3f.js} +2 -2
- package/dist/assets/{pencil-CGcp1H_S.js → pencil-Dbscj0iB.js} +1 -1
- package/dist/assets/{player-overlays-DnLMvF2y.js → player-overlays-CCOENCr_.js} +1 -1
- package/dist/assets/plus-kjGKne3v.js +1 -0
- package/dist/assets/{radio-DejuKcx3.js → radio-BCrXf-T5.js} +1 -1
- package/dist/assets/{refresh-cw-CW_Ljqdp.js → refresh-cw-B50UNH29.js} +1 -1
- package/dist/assets/remoteEntry-DSjWOwlm.js +1 -0
- package/dist/assets/rotate-ccw-CnFi79eH.js +1 -0
- package/dist/assets/rotate-cw-BMi2-EKp.js +1 -0
- package/dist/assets/search-Cqbwip0Z.js +1 -0
- package/dist/assets/{send-CJWIa1yB.js → send-vIA2Sg01.js} +1 -1
- package/dist/assets/{server-DLCgy7fU.js → server-CVgGKwKo.js} +1 -1
- package/dist/assets/square-NIJNDIvZ.js +1 -0
- package/dist/assets/{square-check-big-C1eD-s2U.js → square-check-big-DTiBKI6p.js} +1 -1
- package/dist/assets/src-C3IwZqep.js +7 -0
- package/dist/assets/src-DiF3p-RR.js +67 -0
- package/dist/assets/{undo-2-B2bADvVZ.js → undo-2-buMXiXDJ.js} +1 -1
- package/dist/assets/{upload-DeJAhsSx.js → upload-YICZuTHD.js} +1 -1
- package/dist/assets/{useEventInvalidation-BuTU98fj.js → useEventInvalidation-CGT7WKQk.js} +1 -1
- package/dist/assets/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-CwEvP83T.js → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__admin_ui_host__remoteEntry-_hash_-CbdC118i.js} +2 -2
- package/dist/assets/{wifi-BuwizIJA.js → wifi-B6uMvniy.js} +1 -1
- package/dist/assets/{zap-CWuo4dzD.js → zap-O3h5iXhW.js} +1 -1
- package/dist/index.html +6 -6
- package/dist/server/addon.js +1361 -1283
- package/package.json +1 -1
- package/dist/assets/AddonCollectionPage-CqEMePgW.js +0 -1
- package/dist/assets/Addons-cCLSqWfM.js +0 -1
- package/dist/assets/Automation-CryfmheY.js +0 -29
- package/dist/assets/Capabilities-DOVICQ71.js +0 -1
- package/dist/assets/Cluster-DSXKolnm.js +0 -1
- package/dist/assets/Data-CTCmNwJC.js +0 -1
- package/dist/assets/DetectionIntelligence-3cLwl4I3.js +0 -1
- package/dist/assets/DeviceDetail-BcRXDqYg.js +0 -2
- package/dist/assets/Events-CRvWzt-T.js +0 -1
- package/dist/assets/FormBuilder-D36jBkTY.js +0 -1
- package/dist/assets/Identity-D6X1Vx5c.js +0 -1
- package/dist/assets/IntegrationDetail-Kd39JHzv.js +0 -1
- package/dist/assets/Integrations-BUaA-BHV.js +0 -1
- package/dist/assets/Network-D2GsfRiT.js +0 -1
- package/dist/assets/NodeInferenceDevices-FEzIpBGg.js +0 -1
- package/dist/assets/Notifications-C2xW5T2d.js +0 -1
- package/dist/assets/Pipeline-BAy-OSuJ.js +0 -1
- package/dist/assets/Plates-B-enbHRO.js +0 -1
- package/dist/assets/Recordings-CVuEjle9.js +0 -1
- package/dist/assets/Retrain-B38fDPkv.js +0 -1
- package/dist/assets/authed-media-XfBD4Ma-.js +0 -1
- package/dist/assets/circle-check-big-CmRUug-s.js +0 -1
- package/dist/assets/clock-CbQ_m2UA.js +0 -1
- package/dist/assets/debounced-invalidate-CS1Zql4g.js +0 -1
- package/dist/assets/dist-DyDQKWiO.js +0 -7
- package/dist/assets/image-off-CePvOw2Z.js +0 -1
- package/dist/assets/index-D4VDD87s.css +0 -2
- package/dist/assets/index-DzefosH4.js +0 -4
- package/dist/assets/plus-CWclgqCc.js +0 -1
- package/dist/assets/remoteEntry-DNdwEnrY.js +0 -1
- package/dist/assets/rotate-cw-Bj3NywrT.js +0 -1
- package/dist/assets/search-kZ6MJ1BT.js +0 -1
- package/dist/assets/square-I9BIO4a-.js +0 -1
- package/dist/assets/src-C2KiI31z.js +0 -67
- package/dist/assets/src-DREH726E.js +0 -7
package/dist/server/addon.js
CHANGED
|
@@ -8940,6 +8940,69 @@ var StreamFormatSchema = _enum([
|
|
|
8940
8940
|
"mjpeg",
|
|
8941
8941
|
"rtsp"
|
|
8942
8942
|
]);
|
|
8943
|
+
/** A container `produceEventMedia` can emit. */
|
|
8944
|
+
var EventMediaKindSchema = _enum(["mp4", "gif"]);
|
|
8945
|
+
/**
|
|
8946
|
+
* One produced artifact, referenced by HANDLE.
|
|
8947
|
+
*
|
|
8948
|
+
* Never inline bytes: a produced clip is 200 KB–5 MB and every consumer of this
|
|
8949
|
+
* method is in another runner ([D9](../../../../docs/decisions/adr-0009.md),
|
|
8950
|
+
* [D18](../../../../docs/decisions/adr-0018.md) — cross-process media is fetched
|
|
8951
|
+
* on demand, compressed, by handle). `bytes` is here so a caller can decide
|
|
8952
|
+
* whether it wants the fetch at all.
|
|
8953
|
+
*/
|
|
8954
|
+
var EventMediaArtifactSchema = object({
|
|
8955
|
+
kind: EventMediaKindSchema,
|
|
8956
|
+
/** Opaque, single-camera, short-lived. Redeem with `fetchEventMedia`. */
|
|
8957
|
+
handle: string(),
|
|
8958
|
+
/**
|
|
8959
|
+
* The node holding the bytes — the ROUTING key for `fetchEventMedia`.
|
|
8960
|
+
*
|
|
8961
|
+
* `stream-broker` is a singleton cap and an unpinned call never leaves the
|
|
8962
|
+
* hub, so a handle produced on an agent's broker would be redeemed against
|
|
8963
|
+
* the hub's store and come back `null`. Same contract, same field name and
|
|
8964
|
+
* the same reason as `FrameHandleSchema.nodeId`: the producer stamps where it
|
|
8965
|
+
* lives and the consumer pins to it.
|
|
8966
|
+
*/
|
|
8967
|
+
nodeId: string(),
|
|
8968
|
+
mime: string(),
|
|
8969
|
+
bytes: number().int(),
|
|
8970
|
+
width: number().int(),
|
|
8971
|
+
height: number().int()
|
|
8972
|
+
});
|
|
8973
|
+
/**
|
|
8974
|
+
* What a production actually covered — the answer to the only question an
|
|
8975
|
+
* operator asks about a notification clip.
|
|
8976
|
+
*
|
|
8977
|
+
* `fromTs`/`toTs` are WALL CLOCK, derived from the ring's own packet timeline,
|
|
8978
|
+
* so a caller can state "this clip starts 4.1 s before the event" instead of
|
|
8979
|
+
* inferring it from a duration. A production whose `fromTs` is later than the
|
|
8980
|
+
* event is a production with no pre-roll, and that is exactly the defect this
|
|
8981
|
+
* method exists to make visible rather than plausible.
|
|
8982
|
+
*/
|
|
8983
|
+
var EventMediaCoverageSchema = object({
|
|
8984
|
+
fromTs: number(),
|
|
8985
|
+
toTs: number(),
|
|
8986
|
+
/** Encoded packets in the muxed window. */
|
|
8987
|
+
packets: number().int()
|
|
8988
|
+
});
|
|
8989
|
+
/**
|
|
8990
|
+
* The result of ONE cut, in every container the caller asked for.
|
|
8991
|
+
*
|
|
8992
|
+
* Every artifact in `media` came out of the SAME window of the SAME rendition —
|
|
8993
|
+
* that is the whole reason this is one method rather than one call per format.
|
|
8994
|
+
* A consumer attaching a gif and a video can no longer show two different
|
|
8995
|
+
* moments, because it never chose two sources.
|
|
8996
|
+
*/
|
|
8997
|
+
var EventMediaProductionSchema = object({
|
|
8998
|
+
media: array(EventMediaArtifactSchema).readonly(),
|
|
8999
|
+
coverage: EventMediaCoverageSchema,
|
|
9000
|
+
/** The rendition actually cut from — what the default or the fallback chose. */
|
|
9001
|
+
profile: CamProfileSchema,
|
|
9002
|
+
/** `copy` = the camera's own H.264, untouched. `encode` = re-encoded (H.265
|
|
9003
|
+
* source, a downscale, or a playback rate other than 1). */
|
|
9004
|
+
video: _enum(["copy", "encode"])
|
|
9005
|
+
});
|
|
8943
9006
|
var RtspRestreamEntrySchema = object({
|
|
8944
9007
|
brokerId: string(),
|
|
8945
9008
|
url: string(),
|
|
@@ -9339,6 +9402,56 @@ method(object({
|
|
|
9339
9402
|
}), {
|
|
9340
9403
|
kind: "mutation",
|
|
9341
9404
|
auth: "admin"
|
|
9405
|
+
}), method(object({
|
|
9406
|
+
deviceId: number(),
|
|
9407
|
+
/** Absent = the largest H.264 rendition at or below 1080p, which is
|
|
9408
|
+
* also the one that can be copied. Falls back to whatever the ring
|
|
9409
|
+
* actually retained, and the answer says which. */
|
|
9410
|
+
profile: CamProfileSchema.optional(),
|
|
9411
|
+
aroundMs: number(),
|
|
9412
|
+
preSeconds: number().min(0).max(20).default(4),
|
|
9413
|
+
postSeconds: number().min(0).max(20).default(6),
|
|
9414
|
+
kinds: array(EventMediaKindSchema).min(1).default(["mp4"]),
|
|
9415
|
+
/** GIF geometry. The video keeps the source's own. */
|
|
9416
|
+
gifMaxWidth: number().int().min(120).max(1280).default(640),
|
|
9417
|
+
/**
|
|
9418
|
+
* The gif's own PLAYBACK rate in frames per second — what the finished
|
|
9419
|
+
* gif runs at, not how many source frames feed it. The decimation that
|
|
9420
|
+
* feeds it samples `gifFps / gifSpeed` source frames per second, so at
|
|
9421
|
+
* the defaults a 12 fps gif is built out of 3 source frames a second.
|
|
9422
|
+
*/
|
|
9423
|
+
gifFps: number().int().min(1).max(15).default(12),
|
|
9424
|
+
/**
|
|
9425
|
+
* How fast the GIF plays against real time, independent of `speed`.
|
|
9426
|
+
*
|
|
9427
|
+
* 4× by default, by operator request: a notification gif is glanced at
|
|
9428
|
+
* on a lock screen, so a ~12 s window has to be over in ~3 s. It stays
|
|
9429
|
+
* a separate knob from `speed` even though both now default to 4 —
|
|
9430
|
+
* a caller wanting a real-time video and a fast gif must not have to
|
|
9431
|
+
* choose.
|
|
9432
|
+
*/
|
|
9433
|
+
gifSpeed: number().min(1).max(8).default(4),
|
|
9434
|
+
/**
|
|
9435
|
+
* Playback rate of the VIDEO. Also 4× by default, by operator decision.
|
|
9436
|
+
*
|
|
9437
|
+
* `1` is real time and is the ONLY value that allows the copy branch —
|
|
9438
|
+
* anything else forces `libx264` over the window. That was priced
|
|
9439
|
+
* before it was chosen: a per-event burst measured at 0.23 s and 254 KB
|
|
9440
|
+
* on a real 615 720p cut, against 922 KB for the copy it replaces. A
|
|
9441
|
+
* re-encode is capped at 720p (`EVENT_CLIP_ENCODE_MAX_WIDTH`), because
|
|
9442
|
+
* once the decode is forced the width stops being free.
|
|
9443
|
+
*/
|
|
9444
|
+
speed: number().min(1).max(8).default(4)
|
|
9445
|
+
}), EventMediaProductionSchema, {
|
|
9446
|
+
kind: "mutation",
|
|
9447
|
+
auth: "admin"
|
|
9448
|
+
}), method(object({ handle: string() }), object({
|
|
9449
|
+
base64: string(),
|
|
9450
|
+
mime: string(),
|
|
9451
|
+
bytes: number().int()
|
|
9452
|
+
}).nullable(), {
|
|
9453
|
+
kind: "mutation",
|
|
9454
|
+
auth: "admin"
|
|
9342
9455
|
}), method(_void(), array(CameraStreamSchema).readonly()), method(_void(), array(ProfileSlotSchema).readonly()), method(object({ brokerId: string() }), BrokerStatsSchema), method(object({ brokerId: string() }), object({
|
|
9343
9456
|
probed: boolean(),
|
|
9344
9457
|
summary: string()
|
|
@@ -9531,25 +9644,6 @@ DeviceType.Camera, method(object({ deviceId: number().int().nonnegative() }), ar
|
|
|
9531
9644
|
}),
|
|
9532
9645
|
lastChangedAt: number()
|
|
9533
9646
|
});
|
|
9534
|
-
/**
|
|
9535
|
-
* core-blocks — user-authored TypeScript, stored in the kernel and executed in
|
|
9536
|
-
* its own process.
|
|
9537
|
-
*
|
|
9538
|
-
* Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
|
|
9539
|
-
*
|
|
9540
|
-
* The first use is **owning devices without being a device provider**: a block
|
|
9541
|
-
* declares devices under a system or custom integration and drives their state,
|
|
9542
|
-
* with the same `ctx` an addon gets. Automations come later; nothing here
|
|
9543
|
-
* models a trigger.
|
|
9544
|
-
*
|
|
9545
|
-
* **Stated plainly, because it does not change by being true:** a block has an
|
|
9546
|
-
* addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
|
|
9547
|
-
* with no review step. What makes that survivable is not a sandbox, it is
|
|
9548
|
-
* PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
|
|
9549
|
-
* so a block that throws or never returns is marked `failed` and visible
|
|
9550
|
-
* instead of taking the hub with it (D6). Every method here is admin-only, and
|
|
9551
|
-
* must stay so.
|
|
9552
|
-
*/
|
|
9553
9647
|
/** Where a block runs. The operator chooses — a block driving a device on an
|
|
9554
9648
|
* agent is the reason placement is not fixed to the hub. */
|
|
9555
9649
|
var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
|
|
@@ -9621,6 +9715,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
9621
9715
|
}), object({ block: CoreBlockSchema }), {
|
|
9622
9716
|
kind: "mutation",
|
|
9623
9717
|
auth: "admin"
|
|
9718
|
+
}), method(object({ blockId: string() }), object({ block: CoreBlockSchema }), {
|
|
9719
|
+
kind: "mutation",
|
|
9720
|
+
auth: "admin"
|
|
9624
9721
|
}), method(object({ code: string() }), CoreBlockCompileResultSchema, {
|
|
9625
9722
|
kind: "mutation",
|
|
9626
9723
|
auth: "admin"
|
|
@@ -10299,840 +10396,159 @@ var ExposeInputSchema = object({
|
|
|
10299
10396
|
});
|
|
10300
10397
|
var UnexposeInputSchema = object({ deviceId: string() });
|
|
10301
10398
|
method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), { kind: "mutation" }), method(UnexposeInputSchema, _void(), { kind: "mutation" });
|
|
10399
|
+
var ProviderStatusSchema = object({
|
|
10400
|
+
connected: boolean(),
|
|
10401
|
+
deviceCount: number(),
|
|
10402
|
+
error: string().optional()
|
|
10403
|
+
});
|
|
10404
|
+
object({
|
|
10405
|
+
externalId: string(),
|
|
10406
|
+
name: string(),
|
|
10407
|
+
type: string(),
|
|
10408
|
+
metadata: record(string(), unknown()).optional()
|
|
10409
|
+
});
|
|
10302
10410
|
/**
|
|
10303
|
-
*
|
|
10304
|
-
*
|
|
10305
|
-
*
|
|
10306
|
-
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
10307
|
-
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
10308
|
-
* work a single author-supplied expression can request, so a hostile or
|
|
10309
|
-
* accidental pathological string can never spend unbounded CPU/memory.
|
|
10411
|
+
* Candidate handed back from discovery and accepted by
|
|
10412
|
+
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
10413
|
+
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
10310
10414
|
*/
|
|
10311
|
-
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
10315
|
-
|
|
10316
|
-
/**
|
|
10317
|
-
*
|
|
10318
|
-
|
|
10319
|
-
|
|
10320
|
-
|
|
10321
|
-
|
|
10322
|
-
|
|
10323
|
-
|
|
10415
|
+
var DiscoveryCandidateSchema = object({
|
|
10416
|
+
stableId: string(),
|
|
10417
|
+
type: _enum(DeviceType),
|
|
10418
|
+
suggestedName: string(),
|
|
10419
|
+
prefilledConfig: record(string(), unknown()),
|
|
10420
|
+
/**
|
|
10421
|
+
* Optional upstream-system identity (HA entity_id, vendor MAC, …).
|
|
10422
|
+
* Discovery pre-populates this for systems that know the upstream
|
|
10423
|
+
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
10424
|
+
* flows live through the cap STATUS SLICE after adoption.
|
|
10425
|
+
*/
|
|
10426
|
+
sourceInfo: SourceInfoSchema.optional()
|
|
10427
|
+
});
|
|
10324
10428
|
/**
|
|
10325
|
-
*
|
|
10326
|
-
*
|
|
10327
|
-
*
|
|
10429
|
+
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
10430
|
+
* Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
|
|
10431
|
+
* tRPC layer can pass it through without reshaping.
|
|
10328
10432
|
*/
|
|
10329
|
-
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
|
|
10335
|
-
|
|
10336
|
-
|
|
10337
|
-
|
|
10338
|
-
|
|
10339
|
-
/**
|
|
10340
|
-
*
|
|
10341
|
-
|
|
10342
|
-
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
}
|
|
10346
|
-
};
|
|
10433
|
+
var DeviceSummarySchema = object({
|
|
10434
|
+
id: number(),
|
|
10435
|
+
stableId: string(),
|
|
10436
|
+
addonId: string(),
|
|
10437
|
+
type: string(),
|
|
10438
|
+
name: string(),
|
|
10439
|
+
parentDeviceId: number().nullable(),
|
|
10440
|
+
online: boolean(),
|
|
10441
|
+
features: array(string()),
|
|
10442
|
+
config: record(string(), unknown()),
|
|
10443
|
+
/** Optional upstream-system identity (dispatch key + system tag).
|
|
10444
|
+
* See `SourceInfo`. Present when the device has a non-synthetic
|
|
10445
|
+
* source identifier (HA entities, vendor MAC, …); omitted when the
|
|
10446
|
+
* synthetic backfill is in effect. */
|
|
10447
|
+
sourceInfo: SourceInfoSchema.optional()
|
|
10448
|
+
});
|
|
10347
10449
|
/**
|
|
10348
|
-
*
|
|
10349
|
-
*
|
|
10350
|
-
*
|
|
10351
|
-
*
|
|
10450
|
+
* Result of a live field test (e.g. probing an RTSP URL during device
|
|
10451
|
+
* creation). Matches the UI-side `FieldProbeResult` in
|
|
10452
|
+
* `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
|
|
10453
|
+
* returned `labels` as chips next to the input.
|
|
10454
|
+
*/
|
|
10455
|
+
var FieldProbeResultSchema = object({
|
|
10456
|
+
status: _enum(["ok", "error"]),
|
|
10457
|
+
labels: array(string()).optional(),
|
|
10458
|
+
error: string().optional()
|
|
10459
|
+
});
|
|
10460
|
+
/**
|
|
10461
|
+
* The output of `getChildCreationSchema` is a UI schema tree. We store
|
|
10462
|
+
* it as `unknown` at the capability layer — the router just passes it
|
|
10463
|
+
* through and the admin UI renders it via `FormBuilder`. The actual
|
|
10464
|
+
* type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
|
|
10465
|
+
* but we deliberately avoid a Zod mirror because the union is large and
|
|
10466
|
+
* not meant for runtime validation at this seam.
|
|
10467
|
+
*/
|
|
10468
|
+
var CreationSchemaOutputSchema = unknown();
|
|
10469
|
+
method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), ProviderStatusSchema), method(_void(), array(object({
|
|
10470
|
+
id: string(),
|
|
10471
|
+
name: string(),
|
|
10472
|
+
type: string()
|
|
10473
|
+
}))), method(object({}), boolean()), method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
10474
|
+
kind: "mutation",
|
|
10475
|
+
auth: "admin"
|
|
10476
|
+
}), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
10477
|
+
kind: "mutation",
|
|
10478
|
+
auth: "admin"
|
|
10479
|
+
}), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
|
|
10480
|
+
type: _enum(DeviceType),
|
|
10481
|
+
config: record(string(), unknown())
|
|
10482
|
+
}), DeviceSummarySchema, {
|
|
10483
|
+
kind: "mutation",
|
|
10484
|
+
auth: "admin"
|
|
10485
|
+
}), method(object({
|
|
10486
|
+
type: _enum(DeviceType),
|
|
10487
|
+
key: string(),
|
|
10488
|
+
value: unknown(),
|
|
10489
|
+
formValues: record(string(), unknown()).optional()
|
|
10490
|
+
}), FieldProbeResultSchema, {
|
|
10491
|
+
kind: "mutation",
|
|
10492
|
+
auth: "admin"
|
|
10493
|
+
});
|
|
10494
|
+
/**
|
|
10495
|
+
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
10496
|
+
* live registry access, and all management operations into a single tRPC surface.
|
|
10352
10497
|
*
|
|
10353
|
-
*
|
|
10354
|
-
*
|
|
10355
|
-
*
|
|
10356
|
-
*
|
|
10357
|
-
* callable — they are simply "unknown function" at parse time.
|
|
10498
|
+
* Replaces:
|
|
10499
|
+
* - `device-persistence` capability (persistence methods absorbed here)
|
|
10500
|
+
* - `device-management.router.ts` (deleted in Phase 2)
|
|
10501
|
+
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
10358
10502
|
*
|
|
10359
|
-
*
|
|
10360
|
-
*
|
|
10361
|
-
*
|
|
10362
|
-
*
|
|
10503
|
+
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
10504
|
+
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
10505
|
+
* - No nodeId routing needed — this is a pure hub singleton.
|
|
10506
|
+
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
10507
|
+
* - No shadow registry or cross-node aggregation required.
|
|
10508
|
+
*
|
|
10509
|
+
* Forked workers register devices back to the hub via `ctx.devices`
|
|
10510
|
+
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
10363
10511
|
*/
|
|
10364
|
-
|
|
10365
|
-
|
|
10366
|
-
|
|
10367
|
-
|
|
10368
|
-
|
|
10369
|
-
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
|
|
10378
|
-
|
|
10379
|
-
|
|
10380
|
-
|
|
10381
|
-
|
|
10382
|
-
|
|
10383
|
-
|
|
10384
|
-
|
|
10385
|
-
|
|
10386
|
-
|
|
10387
|
-
|
|
10388
|
-
|
|
10389
|
-
|
|
10390
|
-
|
|
10391
|
-
|
|
10392
|
-
|
|
10393
|
-
|
|
10394
|
-
|
|
10395
|
-
|
|
10396
|
-
|
|
10397
|
-
|
|
10398
|
-
|
|
10399
|
-
|
|
10400
|
-
},
|
|
10401
|
-
ceil: {
|
|
10402
|
-
minArgs: 1,
|
|
10403
|
-
maxArgs: 1,
|
|
10404
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
10405
|
-
},
|
|
10406
|
-
sqrt: {
|
|
10407
|
-
minArgs: 1,
|
|
10408
|
-
maxArgs: 1,
|
|
10409
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
10410
|
-
},
|
|
10411
|
-
round: {
|
|
10412
|
-
minArgs: 1,
|
|
10413
|
-
maxArgs: 2,
|
|
10414
|
-
apply: (args) => {
|
|
10415
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
10416
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
10417
|
-
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
10418
|
-
const factor = 10 ** digits;
|
|
10419
|
-
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
10420
|
-
}
|
|
10421
|
-
},
|
|
10422
|
-
pow: {
|
|
10423
|
-
minArgs: 2,
|
|
10424
|
-
maxArgs: 2,
|
|
10425
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
10426
|
-
},
|
|
10427
|
-
clamp: {
|
|
10428
|
-
minArgs: 3,
|
|
10429
|
-
maxArgs: 3,
|
|
10430
|
-
apply: (args) => {
|
|
10431
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
10432
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
10433
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
10434
|
-
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
10435
|
-
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
10436
|
-
}
|
|
10437
|
-
},
|
|
10438
|
-
avg: {
|
|
10439
|
-
minArgs: 1,
|
|
10440
|
-
maxArgs: INF,
|
|
10441
|
-
apply: (args) => {
|
|
10442
|
-
const nums = allFiniteNumbers(args, "avg");
|
|
10443
|
-
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
10444
|
-
}
|
|
10445
|
-
},
|
|
10446
|
-
sum: {
|
|
10447
|
-
minArgs: 1,
|
|
10448
|
-
maxArgs: INF,
|
|
10449
|
-
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
10450
|
-
},
|
|
10451
|
-
coalesce: {
|
|
10452
|
-
minArgs: 1,
|
|
10453
|
-
maxArgs: INF,
|
|
10454
|
-
apply: (args) => {
|
|
10455
|
-
for (const a of args) if (a !== null) return a;
|
|
10456
|
-
return null;
|
|
10457
|
-
}
|
|
10458
|
-
},
|
|
10459
|
-
age: {
|
|
10460
|
-
minArgs: 2,
|
|
10461
|
-
maxArgs: 2,
|
|
10462
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
10463
|
-
},
|
|
10464
|
-
convert: {
|
|
10465
|
-
minArgs: 3,
|
|
10466
|
-
maxArgs: 3,
|
|
10467
|
-
apply: (args, hooks) => {
|
|
10468
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
10469
|
-
const from = asString$1(args[1], "convert", 1).trim();
|
|
10470
|
-
const to = asString$1(args[2], "convert", 2).trim();
|
|
10471
|
-
if (hooks.convert) {
|
|
10472
|
-
const out = hooks.convert(x, from, to);
|
|
10473
|
-
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
10474
|
-
return finiteResult(out, "convert");
|
|
10475
|
-
}
|
|
10476
|
-
if (from === to) return x;
|
|
10477
|
-
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
10478
|
-
}
|
|
10479
|
-
}
|
|
10480
|
-
};
|
|
10481
|
-
Object.freeze(Object.assign(Object.create(null), table));
|
|
10482
|
-
/** The set of valid builtin names — used by the parser to reject unknown
|
|
10483
|
-
* callees at parse time (immediate author feedback). */
|
|
10484
|
-
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
10512
|
+
/** One child-placement directive on a container's `childLayout`. Structurally
|
|
10513
|
+
* identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
|
|
10514
|
+
* shape for the same field. The child is identified by its re-sync-stable
|
|
10515
|
+
* accessory `stableIdSuffix` (`childKey`); listed children are grouped into
|
|
10516
|
+
* named accordion sections (with optional intra-section order). */
|
|
10517
|
+
var ChildLayoutEntrySchema = object({
|
|
10518
|
+
childKey: string(),
|
|
10519
|
+
section: string(),
|
|
10520
|
+
order: number().optional(),
|
|
10521
|
+
collapsed: boolean().optional()
|
|
10522
|
+
});
|
|
10523
|
+
/** Cap-wire shape of a per-cap display refinement — mirrors
|
|
10524
|
+
* `DeviceCapDisplayOverride` in `device-management.ts`. */
|
|
10525
|
+
var DeviceCapDisplayOverrideSchema = object({
|
|
10526
|
+
unit: string().min(1).optional(),
|
|
10527
|
+
precision: number().int().min(0).max(10).optional()
|
|
10528
|
+
});
|
|
10529
|
+
/** Cap-wire shape of an operator-authored per-device display override —
|
|
10530
|
+
* mirrors `DeviceDisplayOverride` in `device-management.ts`. `precision`
|
|
10531
|
+
* bounds mirror `numeric-sensor.cap.ts` (`int 0-10`). */
|
|
10532
|
+
var DeviceDisplayOverrideSchema = object({
|
|
10533
|
+
icon: string().min(1).optional(),
|
|
10534
|
+
label: string().min(1).optional(),
|
|
10535
|
+
unit: string().min(1).optional(),
|
|
10536
|
+
precision: number().int().min(0).max(10).optional(),
|
|
10537
|
+
hidden: boolean().optional(),
|
|
10538
|
+
perCap: record(string(), DeviceCapDisplayOverrideSchema).optional()
|
|
10539
|
+
});
|
|
10540
|
+
/** Cap-wire shape of a per-role display default — mirrors `RoleDisplayDefault`
|
|
10541
|
+
* in `device-management.ts`. Keyed by `DeviceRole` string (role strings cross
|
|
10542
|
+
* the wire as plain strings everywhere else — cf. `DeviceInfoSchema.role`). */
|
|
10543
|
+
var RoleDisplayDefaultSchema = object({
|
|
10544
|
+
unit: string().min(1).optional(),
|
|
10545
|
+
precision: number().int().min(0).max(10).optional(),
|
|
10546
|
+
icon: string().min(1).optional()
|
|
10547
|
+
});
|
|
10485
10548
|
/**
|
|
10486
|
-
*
|
|
10487
|
-
*
|
|
10488
|
-
*
|
|
10489
|
-
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
10490
|
-
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
10491
|
-
* is a parse error with a source position, so member access / assignment /
|
|
10492
|
-
* template literals are lexically impossible.
|
|
10493
|
-
*/
|
|
10494
|
-
var KEYWORDS = new Set([
|
|
10495
|
-
"true",
|
|
10496
|
-
"false",
|
|
10497
|
-
"null"
|
|
10498
|
-
]);
|
|
10499
|
-
function isDigit(ch) {
|
|
10500
|
-
return ch >= "0" && ch <= "9";
|
|
10501
|
-
}
|
|
10502
|
-
function isIdentStart(ch) {
|
|
10503
|
-
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
10504
|
-
}
|
|
10505
|
-
function isIdentPart(ch) {
|
|
10506
|
-
return isIdentStart(ch) || isDigit(ch);
|
|
10507
|
-
}
|
|
10508
|
-
function isWhitespace(ch) {
|
|
10509
|
-
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
10510
|
-
}
|
|
10511
|
-
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
10512
|
-
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
10513
|
-
* string. */
|
|
10514
|
-
function tokenize(source) {
|
|
10515
|
-
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
10516
|
-
const tokens = [];
|
|
10517
|
-
let i = 0;
|
|
10518
|
-
const n = source.length;
|
|
10519
|
-
while (i < n) {
|
|
10520
|
-
const ch = source[i];
|
|
10521
|
-
if (isWhitespace(ch)) {
|
|
10522
|
-
i += 1;
|
|
10523
|
-
continue;
|
|
10524
|
-
}
|
|
10525
|
-
if (isDigit(ch)) {
|
|
10526
|
-
const start = i;
|
|
10527
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
10528
|
-
if (i < n && source[i] === ".") {
|
|
10529
|
-
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
10530
|
-
i += 1;
|
|
10531
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
10532
|
-
}
|
|
10533
|
-
const text = source.slice(start, i);
|
|
10534
|
-
const value = Number(text);
|
|
10535
|
-
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
10536
|
-
tokens.push({
|
|
10537
|
-
type: "number",
|
|
10538
|
-
value,
|
|
10539
|
-
pos: start
|
|
10540
|
-
});
|
|
10541
|
-
continue;
|
|
10542
|
-
}
|
|
10543
|
-
if (ch === "'" || ch === "\"") {
|
|
10544
|
-
const quote = ch;
|
|
10545
|
-
const start = i;
|
|
10546
|
-
i += 1;
|
|
10547
|
-
let out = "";
|
|
10548
|
-
let closed = false;
|
|
10549
|
-
while (i < n) {
|
|
10550
|
-
const c = source[i];
|
|
10551
|
-
if (c === "\\") {
|
|
10552
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
10553
|
-
if (next === "\\" || next === "'" || next === "\"") {
|
|
10554
|
-
out += next;
|
|
10555
|
-
i += 2;
|
|
10556
|
-
continue;
|
|
10557
|
-
}
|
|
10558
|
-
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
10559
|
-
}
|
|
10560
|
-
if (c === quote) {
|
|
10561
|
-
closed = true;
|
|
10562
|
-
i += 1;
|
|
10563
|
-
break;
|
|
10564
|
-
}
|
|
10565
|
-
out += c;
|
|
10566
|
-
i += 1;
|
|
10567
|
-
}
|
|
10568
|
-
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
10569
|
-
tokens.push({
|
|
10570
|
-
type: "string",
|
|
10571
|
-
value: out,
|
|
10572
|
-
pos: start
|
|
10573
|
-
});
|
|
10574
|
-
continue;
|
|
10575
|
-
}
|
|
10576
|
-
if (isIdentStart(ch)) {
|
|
10577
|
-
const start = i;
|
|
10578
|
-
while (i < n && isIdentPart(source[i])) i += 1;
|
|
10579
|
-
const text = source.slice(start, i);
|
|
10580
|
-
if (KEYWORDS.has(text)) tokens.push({
|
|
10581
|
-
type: "keyword",
|
|
10582
|
-
keyword: keywordOf(text),
|
|
10583
|
-
pos: start
|
|
10584
|
-
});
|
|
10585
|
-
else tokens.push({
|
|
10586
|
-
type: "identifier",
|
|
10587
|
-
name: text,
|
|
10588
|
-
pos: start
|
|
10589
|
-
});
|
|
10590
|
-
continue;
|
|
10591
|
-
}
|
|
10592
|
-
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
10593
|
-
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
10594
|
-
tokens.push({
|
|
10595
|
-
type: "punct",
|
|
10596
|
-
punct: two,
|
|
10597
|
-
pos: i
|
|
10598
|
-
});
|
|
10599
|
-
i += 2;
|
|
10600
|
-
continue;
|
|
10601
|
-
}
|
|
10602
|
-
if (isSinglePunct(ch)) {
|
|
10603
|
-
tokens.push({
|
|
10604
|
-
type: "punct",
|
|
10605
|
-
punct: ch,
|
|
10606
|
-
pos: i
|
|
10607
|
-
});
|
|
10608
|
-
i += 1;
|
|
10609
|
-
continue;
|
|
10610
|
-
}
|
|
10611
|
-
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
10612
|
-
}
|
|
10613
|
-
tokens.push({
|
|
10614
|
-
type: "eof",
|
|
10615
|
-
pos: n
|
|
10616
|
-
});
|
|
10617
|
-
return tokens;
|
|
10618
|
-
}
|
|
10619
|
-
function keywordOf(text) {
|
|
10620
|
-
if (text === "true") return "true";
|
|
10621
|
-
if (text === "false") return "false";
|
|
10622
|
-
return "null";
|
|
10623
|
-
}
|
|
10624
|
-
function isSinglePunct(ch) {
|
|
10625
|
-
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
10626
|
-
}
|
|
10627
|
-
/**
|
|
10628
|
-
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
10629
|
-
*
|
|
10630
|
-
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
10631
|
-
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
10632
|
-
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
10633
|
-
* string validated against the builtin table at parse time, so an unknown
|
|
10634
|
-
* function is rejected immediately (author feedback) and a persisted expression
|
|
10635
|
-
* that references a since-removed builtin degrades at read.
|
|
10636
|
-
*
|
|
10637
|
-
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
10638
|
-
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
10639
|
-
*/
|
|
10640
|
-
/** Binary/logical operator precedence (higher binds tighter). */
|
|
10641
|
-
var BINARY_PRECEDENCE = {
|
|
10642
|
-
"||": 1,
|
|
10643
|
-
"&&": 2,
|
|
10644
|
-
"==": 3,
|
|
10645
|
-
"!=": 3,
|
|
10646
|
-
"<": 4,
|
|
10647
|
-
"<=": 4,
|
|
10648
|
-
">": 4,
|
|
10649
|
-
">=": 4,
|
|
10650
|
-
"+": 5,
|
|
10651
|
-
"-": 5,
|
|
10652
|
-
"*": 6,
|
|
10653
|
-
"/": 6,
|
|
10654
|
-
"%": 6
|
|
10655
|
-
};
|
|
10656
|
-
function isLogicalOp(op) {
|
|
10657
|
-
return op === "&&" || op === "||";
|
|
10658
|
-
}
|
|
10659
|
-
function isBinaryOp(op) {
|
|
10660
|
-
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
10661
|
-
}
|
|
10662
|
-
var Parser = class {
|
|
10663
|
-
tokens;
|
|
10664
|
-
pos = 0;
|
|
10665
|
-
nodeCount = 0;
|
|
10666
|
-
identifiers = /* @__PURE__ */ new Set();
|
|
10667
|
-
callees = /* @__PURE__ */ new Set();
|
|
10668
|
-
constructor(tokens) {
|
|
10669
|
-
this.tokens = tokens;
|
|
10670
|
-
}
|
|
10671
|
-
parse() {
|
|
10672
|
-
const ast = this.parseTernary();
|
|
10673
|
-
const tok = this.peek();
|
|
10674
|
-
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
10675
|
-
return {
|
|
10676
|
-
ast,
|
|
10677
|
-
identifiers: this.identifiers,
|
|
10678
|
-
callees: this.callees,
|
|
10679
|
-
nodeCount: this.nodeCount
|
|
10680
|
-
};
|
|
10681
|
-
}
|
|
10682
|
-
peek() {
|
|
10683
|
-
return this.tokens[this.pos];
|
|
10684
|
-
}
|
|
10685
|
-
next() {
|
|
10686
|
-
return this.tokens[this.pos++];
|
|
10687
|
-
}
|
|
10688
|
-
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
10689
|
-
expectPunct(punct) {
|
|
10690
|
-
const tok = this.peek();
|
|
10691
|
-
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
10692
|
-
this.pos += 1;
|
|
10693
|
-
}
|
|
10694
|
-
matchPunct(punct) {
|
|
10695
|
-
const tok = this.peek();
|
|
10696
|
-
if (tok.type === "punct" && tok.punct === punct) {
|
|
10697
|
-
this.pos += 1;
|
|
10698
|
-
return true;
|
|
10699
|
-
}
|
|
10700
|
-
return false;
|
|
10701
|
-
}
|
|
10702
|
-
countNode() {
|
|
10703
|
-
this.nodeCount += 1;
|
|
10704
|
-
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
10705
|
-
}
|
|
10706
|
-
parseTernary() {
|
|
10707
|
-
const test = this.parseBinary(1);
|
|
10708
|
-
if (this.matchPunct("?")) {
|
|
10709
|
-
const consequent = this.parseTernary();
|
|
10710
|
-
this.expectPunct(":");
|
|
10711
|
-
const alternate = this.parseTernary();
|
|
10712
|
-
this.countNode();
|
|
10713
|
-
return {
|
|
10714
|
-
kind: "conditional",
|
|
10715
|
-
test,
|
|
10716
|
-
consequent,
|
|
10717
|
-
alternate
|
|
10718
|
-
};
|
|
10719
|
-
}
|
|
10720
|
-
return test;
|
|
10721
|
-
}
|
|
10722
|
-
parseBinary(minPrec) {
|
|
10723
|
-
let left = this.parseUnary();
|
|
10724
|
-
for (;;) {
|
|
10725
|
-
const tok = this.peek();
|
|
10726
|
-
if (tok.type !== "punct") break;
|
|
10727
|
-
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
10728
|
-
if (prec === void 0 || prec < minPrec) break;
|
|
10729
|
-
const op = tok.punct;
|
|
10730
|
-
this.pos += 1;
|
|
10731
|
-
const right = this.parseBinary(prec + 1);
|
|
10732
|
-
this.countNode();
|
|
10733
|
-
if (isLogicalOp(op)) left = {
|
|
10734
|
-
kind: "logical",
|
|
10735
|
-
op,
|
|
10736
|
-
left,
|
|
10737
|
-
right
|
|
10738
|
-
};
|
|
10739
|
-
else if (isBinaryOp(op)) left = {
|
|
10740
|
-
kind: "binary",
|
|
10741
|
-
op,
|
|
10742
|
-
left,
|
|
10743
|
-
right
|
|
10744
|
-
};
|
|
10745
|
-
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
10746
|
-
}
|
|
10747
|
-
return left;
|
|
10748
|
-
}
|
|
10749
|
-
parseUnary() {
|
|
10750
|
-
const tok = this.peek();
|
|
10751
|
-
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
10752
|
-
const op = tok.punct;
|
|
10753
|
-
this.pos += 1;
|
|
10754
|
-
const operand = this.parseUnary();
|
|
10755
|
-
this.countNode();
|
|
10756
|
-
return {
|
|
10757
|
-
kind: "unary",
|
|
10758
|
-
op,
|
|
10759
|
-
operand
|
|
10760
|
-
};
|
|
10761
|
-
}
|
|
10762
|
-
return this.parsePrimary();
|
|
10763
|
-
}
|
|
10764
|
-
parsePrimary() {
|
|
10765
|
-
const tok = this.next();
|
|
10766
|
-
switch (tok.type) {
|
|
10767
|
-
case "number":
|
|
10768
|
-
this.countNode();
|
|
10769
|
-
return {
|
|
10770
|
-
kind: "literal",
|
|
10771
|
-
value: tok.value
|
|
10772
|
-
};
|
|
10773
|
-
case "string":
|
|
10774
|
-
this.countNode();
|
|
10775
|
-
return {
|
|
10776
|
-
kind: "literal",
|
|
10777
|
-
value: tok.value
|
|
10778
|
-
};
|
|
10779
|
-
case "keyword":
|
|
10780
|
-
this.countNode();
|
|
10781
|
-
return {
|
|
10782
|
-
kind: "literal",
|
|
10783
|
-
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
10784
|
-
};
|
|
10785
|
-
case "identifier": {
|
|
10786
|
-
const nextTok = this.peek();
|
|
10787
|
-
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
10788
|
-
this.identifiers.add(tok.name);
|
|
10789
|
-
this.countNode();
|
|
10790
|
-
return {
|
|
10791
|
-
kind: "identifier",
|
|
10792
|
-
name: tok.name
|
|
10793
|
-
};
|
|
10794
|
-
}
|
|
10795
|
-
case "punct":
|
|
10796
|
-
if (tok.punct === "(") {
|
|
10797
|
-
const inner = this.parseTernary();
|
|
10798
|
-
this.expectPunct(")");
|
|
10799
|
-
return inner;
|
|
10800
|
-
}
|
|
10801
|
-
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
10802
|
-
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
10803
|
-
}
|
|
10804
|
-
}
|
|
10805
|
-
parseCall(callee, pos) {
|
|
10806
|
-
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
10807
|
-
this.expectPunct("(");
|
|
10808
|
-
const args = [];
|
|
10809
|
-
if (!this.matchPunct(")")) for (;;) {
|
|
10810
|
-
args.push(this.parseTernary());
|
|
10811
|
-
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
10812
|
-
if (this.matchPunct(",")) continue;
|
|
10813
|
-
this.expectPunct(")");
|
|
10814
|
-
break;
|
|
10815
|
-
}
|
|
10816
|
-
this.callees.add(callee);
|
|
10817
|
-
this.countNode();
|
|
10818
|
-
return {
|
|
10819
|
-
kind: "call",
|
|
10820
|
-
callee,
|
|
10821
|
-
args
|
|
10822
|
-
};
|
|
10823
|
-
}
|
|
10824
|
-
};
|
|
10825
|
-
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
10826
|
-
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
10827
|
-
function parseExpression(source) {
|
|
10828
|
-
return new Parser(tokenize(source)).parse();
|
|
10829
|
-
}
|
|
10830
|
-
/**
|
|
10831
|
-
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
10832
|
-
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
10833
|
-
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
10834
|
-
* one per read on a hot resolve path.
|
|
10835
|
-
*
|
|
10836
|
-
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
10837
|
-
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
10838
|
-
* callers is safe and maximises hit rate.
|
|
10839
|
-
*/
|
|
10840
|
-
var cache = /* @__PURE__ */ new Map();
|
|
10841
|
-
function getCached(source) {
|
|
10842
|
-
const hit = cache.get(source);
|
|
10843
|
-
if (hit !== void 0) {
|
|
10844
|
-
cache.delete(source);
|
|
10845
|
-
cache.set(source, hit);
|
|
10846
|
-
return hit;
|
|
10847
|
-
}
|
|
10848
|
-
let result;
|
|
10849
|
-
try {
|
|
10850
|
-
result = {
|
|
10851
|
-
ok: true,
|
|
10852
|
-
parsed: parseExpression(source)
|
|
10853
|
-
};
|
|
10854
|
-
} catch (err) {
|
|
10855
|
-
result = {
|
|
10856
|
-
ok: false,
|
|
10857
|
-
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
10858
|
-
};
|
|
10859
|
-
}
|
|
10860
|
-
cache.set(source, result);
|
|
10861
|
-
if (cache.size > 256) {
|
|
10862
|
-
const oldest = cache.keys().next().value;
|
|
10863
|
-
if (oldest !== void 0) cache.delete(oldest);
|
|
10864
|
-
}
|
|
10865
|
-
return result;
|
|
10866
|
-
}
|
|
10867
|
-
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
10868
|
-
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
10869
|
-
function compileExpressionSafe(source) {
|
|
10870
|
-
return getCached(source);
|
|
10871
|
-
}
|
|
10872
|
-
Object.freeze({});
|
|
10873
|
-
/**
|
|
10874
|
-
* Author-time validation. Returns `null` when the source is valid, else a
|
|
10875
|
-
* human-readable error message. Checks: the expression compiles; binding count
|
|
10876
|
-
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
10877
|
-
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
10878
|
-
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
10879
|
-
*/
|
|
10880
|
-
function validateExpressionSource(src) {
|
|
10881
|
-
const names = Object.keys(src.bindings);
|
|
10882
|
-
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
10883
|
-
for (const name of names) {
|
|
10884
|
-
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
10885
|
-
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
10886
|
-
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
10887
|
-
}
|
|
10888
|
-
const compiled = compileExpressionSafe(src.expr);
|
|
10889
|
-
if (!compiled.ok) return compiled.error;
|
|
10890
|
-
const bound = new Set(names);
|
|
10891
|
-
for (const id of compiled.parsed.identifiers) {
|
|
10892
|
-
if (id === "now") continue;
|
|
10893
|
-
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
10894
|
-
}
|
|
10895
|
-
return null;
|
|
10896
|
-
}
|
|
10897
|
-
var ProviderStatusSchema = object({
|
|
10898
|
-
connected: boolean(),
|
|
10899
|
-
deviceCount: number(),
|
|
10900
|
-
error: string().optional()
|
|
10901
|
-
});
|
|
10902
|
-
object({
|
|
10903
|
-
externalId: string(),
|
|
10904
|
-
name: string(),
|
|
10905
|
-
type: string(),
|
|
10906
|
-
metadata: record(string(), unknown()).optional()
|
|
10907
|
-
});
|
|
10908
|
-
/**
|
|
10909
|
-
* Candidate handed back from discovery and accepted by
|
|
10910
|
-
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
10911
|
-
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
10912
|
-
*/
|
|
10913
|
-
var DiscoveryCandidateSchema = object({
|
|
10914
|
-
stableId: string(),
|
|
10915
|
-
type: _enum(DeviceType),
|
|
10916
|
-
suggestedName: string(),
|
|
10917
|
-
prefilledConfig: record(string(), unknown()),
|
|
10918
|
-
/**
|
|
10919
|
-
* Optional upstream-system identity (HA entity_id, vendor MAC, …).
|
|
10920
|
-
* Discovery pre-populates this for systems that know the upstream
|
|
10921
|
-
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
10922
|
-
* flows live through the cap STATUS SLICE after adoption.
|
|
10923
|
-
*/
|
|
10924
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
10925
|
-
});
|
|
10926
|
-
/**
|
|
10927
|
-
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
10928
|
-
* Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
|
|
10929
|
-
* tRPC layer can pass it through without reshaping.
|
|
10930
|
-
*/
|
|
10931
|
-
var DeviceSummarySchema = object({
|
|
10932
|
-
id: number(),
|
|
10933
|
-
stableId: string(),
|
|
10934
|
-
addonId: string(),
|
|
10935
|
-
type: string(),
|
|
10936
|
-
name: string(),
|
|
10937
|
-
parentDeviceId: number().nullable(),
|
|
10938
|
-
online: boolean(),
|
|
10939
|
-
features: array(string()),
|
|
10940
|
-
config: record(string(), unknown()),
|
|
10941
|
-
/** Optional upstream-system identity (dispatch key + system tag).
|
|
10942
|
-
* See `SourceInfo`. Present when the device has a non-synthetic
|
|
10943
|
-
* source identifier (HA entities, vendor MAC, …); omitted when the
|
|
10944
|
-
* synthetic backfill is in effect. */
|
|
10945
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
10946
|
-
});
|
|
10947
|
-
/**
|
|
10948
|
-
* Result of a live field test (e.g. probing an RTSP URL during device
|
|
10949
|
-
* creation). Matches the UI-side `FieldProbeResult` in
|
|
10950
|
-
* `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
|
|
10951
|
-
* returned `labels` as chips next to the input.
|
|
10952
|
-
*/
|
|
10953
|
-
var FieldProbeResultSchema = object({
|
|
10954
|
-
status: _enum(["ok", "error"]),
|
|
10955
|
-
labels: array(string()).optional(),
|
|
10956
|
-
error: string().optional()
|
|
10957
|
-
});
|
|
10958
|
-
/**
|
|
10959
|
-
* The output of `getChildCreationSchema` is a UI schema tree. We store
|
|
10960
|
-
* it as `unknown` at the capability layer — the router just passes it
|
|
10961
|
-
* through and the admin UI renders it via `FormBuilder`. The actual
|
|
10962
|
-
* type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
|
|
10963
|
-
* but we deliberately avoid a Zod mirror because the union is large and
|
|
10964
|
-
* not meant for runtime validation at this seam.
|
|
10965
|
-
*/
|
|
10966
|
-
var CreationSchemaOutputSchema = unknown();
|
|
10967
|
-
method(_void(), _void(), { kind: "mutation" }), method(_void(), _void(), { kind: "mutation" }), method(_void(), ProviderStatusSchema), method(_void(), array(object({
|
|
10968
|
-
id: string(),
|
|
10969
|
-
name: string(),
|
|
10970
|
-
type: string()
|
|
10971
|
-
}))), method(object({}), boolean()), method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
10972
|
-
kind: "mutation",
|
|
10973
|
-
auth: "admin"
|
|
10974
|
-
}), method(object({}), CreationSchemaOutputSchema), method(object({}), object({ deviceType: _enum(DeviceType).nullable() })), method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
10975
|
-
kind: "mutation",
|
|
10976
|
-
auth: "admin"
|
|
10977
|
-
}), method(object({}), boolean()), method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema), method(object({
|
|
10978
|
-
type: _enum(DeviceType),
|
|
10979
|
-
config: record(string(), unknown())
|
|
10980
|
-
}), DeviceSummarySchema, {
|
|
10981
|
-
kind: "mutation",
|
|
10982
|
-
auth: "admin"
|
|
10983
|
-
}), method(object({
|
|
10984
|
-
type: _enum(DeviceType),
|
|
10985
|
-
key: string(),
|
|
10986
|
-
value: unknown(),
|
|
10987
|
-
formValues: record(string(), unknown()).optional()
|
|
10988
|
-
}), FieldProbeResultSchema, {
|
|
10989
|
-
kind: "mutation",
|
|
10990
|
-
auth: "admin"
|
|
10991
|
-
});
|
|
10992
|
-
/**
|
|
10993
|
-
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
10994
|
-
* live registry access, and all management operations into a single tRPC surface.
|
|
10995
|
-
*
|
|
10996
|
-
* Replaces:
|
|
10997
|
-
* - `device-persistence` capability (persistence methods absorbed here)
|
|
10998
|
-
* - `device-management.router.ts` (deleted in Phase 2)
|
|
10999
|
-
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
11000
|
-
*
|
|
11001
|
-
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
11002
|
-
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
11003
|
-
* - No nodeId routing needed — this is a pure hub singleton.
|
|
11004
|
-
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
11005
|
-
* - No shadow registry or cross-node aggregation required.
|
|
11006
|
-
*
|
|
11007
|
-
* Forked workers register devices back to the hub via `ctx.devices`
|
|
11008
|
-
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
11009
|
-
*/
|
|
11010
|
-
/** One child-placement directive on a container's `childLayout`. Structurally
|
|
11011
|
-
* identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
|
|
11012
|
-
* shape for the same field. The child is identified by its re-sync-stable
|
|
11013
|
-
* accessory `stableIdSuffix` (`childKey`); listed children are grouped into
|
|
11014
|
-
* named accordion sections (with optional intra-section order). */
|
|
11015
|
-
var ChildLayoutEntrySchema = object({
|
|
11016
|
-
childKey: string(),
|
|
11017
|
-
section: string(),
|
|
11018
|
-
order: number().optional(),
|
|
11019
|
-
collapsed: boolean().optional()
|
|
11020
|
-
});
|
|
11021
|
-
/** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
|
|
11022
|
-
* `device-management.ts`. Source is a union: a FIELD source copies a sibling
|
|
11023
|
-
* accessory's status field (`kind` optional/absent for wire compat); a
|
|
11024
|
-
* LITERAL source carries a per-device constant (no sibling is read); a
|
|
11025
|
-
* GLOBAL source (P2e) copies ANY device's status field, addressed by the
|
|
11026
|
-
* source device's full re-sync-stable `stableId`. */
|
|
11027
|
-
var DeviceLinkFieldSourceSchema = object({
|
|
11028
|
-
kind: literal("field").optional(),
|
|
11029
|
-
sourceKey: string(),
|
|
11030
|
-
cap: string(),
|
|
11031
|
-
fieldPath: string()
|
|
11032
|
-
});
|
|
11033
|
-
var DeviceLinkLiteralSourceSchema = object({
|
|
11034
|
-
kind: literal("literal"),
|
|
11035
|
-
value: union([
|
|
11036
|
-
string(),
|
|
11037
|
-
number(),
|
|
11038
|
-
boolean(),
|
|
11039
|
-
_null()
|
|
11040
|
-
])
|
|
11041
|
-
});
|
|
11042
|
-
var DeviceLinkGlobalSourceSchema = object({
|
|
11043
|
-
kind: literal("global"),
|
|
11044
|
-
sourceStableId: string(),
|
|
11045
|
-
cap: string(),
|
|
11046
|
-
fieldPath: string()
|
|
11047
|
-
});
|
|
11048
|
-
/** Expression source (Stage X): compute the target field from N named bindings
|
|
11049
|
-
* via the safe expression engine. Bindings are field | literal | global — never
|
|
11050
|
-
* another expression (no nesting). The `superRefine` runs the SAME author-time
|
|
11051
|
-
* validation as `validateExpressionSource` (compiles the expr, checks binding
|
|
11052
|
-
* names + identifier coverage) so every wire boundary that parses a DeviceLink
|
|
11053
|
-
* (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
|
|
11054
|
-
* Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
|
|
11055
|
-
var DeviceLinkExpressionSourceSchema = object({
|
|
11056
|
-
kind: literal("expression"),
|
|
11057
|
-
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
11058
|
-
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), union([
|
|
11059
|
-
DeviceLinkFieldSourceSchema,
|
|
11060
|
-
DeviceLinkLiteralSourceSchema,
|
|
11061
|
-
DeviceLinkGlobalSourceSchema
|
|
11062
|
-
]))
|
|
11063
|
-
}).superRefine((src, ctx) => {
|
|
11064
|
-
const err = validateExpressionSource(src);
|
|
11065
|
-
if (err !== null) ctx.addIssue({
|
|
11066
|
-
code: "custom",
|
|
11067
|
-
message: err,
|
|
11068
|
-
path: ["expr"]
|
|
11069
|
-
});
|
|
11070
|
-
});
|
|
11071
|
-
var DeviceLinkSchema = object({
|
|
11072
|
-
id: string(),
|
|
11073
|
-
source: union([
|
|
11074
|
-
DeviceLinkFieldSourceSchema,
|
|
11075
|
-
DeviceLinkLiteralSourceSchema,
|
|
11076
|
-
DeviceLinkGlobalSourceSchema,
|
|
11077
|
-
DeviceLinkExpressionSourceSchema
|
|
11078
|
-
]),
|
|
11079
|
-
target: object({
|
|
11080
|
-
cap: string(),
|
|
11081
|
-
fieldPath: string(),
|
|
11082
|
-
itemKey: string().optional()
|
|
11083
|
-
}),
|
|
11084
|
-
transform: discriminatedUnion("kind", [
|
|
11085
|
-
object({ kind: literal("identity") }),
|
|
11086
|
-
object({
|
|
11087
|
-
kind: literal("enum-map"),
|
|
11088
|
-
mapping: record(string(), union([
|
|
11089
|
-
string(),
|
|
11090
|
-
number(),
|
|
11091
|
-
boolean()
|
|
11092
|
-
])),
|
|
11093
|
-
fallback: union([
|
|
11094
|
-
string(),
|
|
11095
|
-
number(),
|
|
11096
|
-
boolean()
|
|
11097
|
-
]).optional()
|
|
11098
|
-
}),
|
|
11099
|
-
object({
|
|
11100
|
-
kind: literal("linear"),
|
|
11101
|
-
scale: number(),
|
|
11102
|
-
offset: number(),
|
|
11103
|
-
clamp: tuple([number(), number()]).readonly().optional()
|
|
11104
|
-
})
|
|
11105
|
-
]).optional()
|
|
11106
|
-
});
|
|
11107
|
-
/** Cap-wire shape of a per-cap display refinement — mirrors
|
|
11108
|
-
* `DeviceCapDisplayOverride` in `device-management.ts`. */
|
|
11109
|
-
var DeviceCapDisplayOverrideSchema = object({
|
|
11110
|
-
unit: string().min(1).optional(),
|
|
11111
|
-
precision: number().int().min(0).max(10).optional()
|
|
11112
|
-
});
|
|
11113
|
-
/** Cap-wire shape of an operator-authored per-device display override —
|
|
11114
|
-
* mirrors `DeviceDisplayOverride` in `device-management.ts`. `precision`
|
|
11115
|
-
* bounds mirror `numeric-sensor.cap.ts` (`int 0-10`). */
|
|
11116
|
-
var DeviceDisplayOverrideSchema = object({
|
|
11117
|
-
icon: string().min(1).optional(),
|
|
11118
|
-
label: string().min(1).optional(),
|
|
11119
|
-
unit: string().min(1).optional(),
|
|
11120
|
-
precision: number().int().min(0).max(10).optional(),
|
|
11121
|
-
hidden: boolean().optional(),
|
|
11122
|
-
perCap: record(string(), DeviceCapDisplayOverrideSchema).optional()
|
|
11123
|
-
});
|
|
11124
|
-
/** Cap-wire shape of a per-role display default — mirrors `RoleDisplayDefault`
|
|
11125
|
-
* in `device-management.ts`. Keyed by `DeviceRole` string (role strings cross
|
|
11126
|
-
* the wire as plain strings everywhere else — cf. `DeviceInfoSchema.role`). */
|
|
11127
|
-
var RoleDisplayDefaultSchema = object({
|
|
11128
|
-
unit: string().min(1).optional(),
|
|
11129
|
-
precision: number().int().min(0).max(10).optional(),
|
|
11130
|
-
icon: string().min(1).optional()
|
|
11131
|
-
});
|
|
11132
|
-
/**
|
|
11133
|
-
* Serializable projection of a live IDevice.
|
|
11134
|
-
* Returned by listAll, getDevice, getChildren.
|
|
11135
|
-
* Live methods (getStreamSources, getConfigSchema) are separate calls.
|
|
10549
|
+
* Serializable projection of a live IDevice.
|
|
10550
|
+
* Returned by listAll, getDevice, getChildren.
|
|
10551
|
+
* Live methods (getStreamSources, getConfigSchema) are separate calls.
|
|
11136
10552
|
*/
|
|
11137
10553
|
var DeviceInfoSchema = object({
|
|
11138
10554
|
/** Progressive, system-wide unique number. Allocated synchronously by
|
|
@@ -11183,8 +10599,6 @@ var DeviceInfoSchema = object({
|
|
|
11183
10599
|
* named accordion sections (with optional intra-section order). See
|
|
11184
10600
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
11185
10601
|
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
11186
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
11187
|
-
deviceLinks: array(DeviceLinkSchema).readonly().optional(),
|
|
11188
10602
|
/** Operator-authored per-device display override. See `DeviceMeta.display`. */
|
|
11189
10603
|
display: DeviceDisplayOverrideSchema.optional()
|
|
11190
10604
|
});
|
|
@@ -11193,7 +10607,7 @@ var ConfigEntrySchema = object({
|
|
|
11193
10607
|
value: unknown(),
|
|
11194
10608
|
description: string().optional()
|
|
11195
10609
|
});
|
|
11196
|
-
var
|
|
10610
|
+
var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
|
|
11197
10611
|
/** One resolved linked device — the compact projection consumers need. */
|
|
11198
10612
|
var LinkedDeviceSchema = object({
|
|
11199
10613
|
deviceId: number(),
|
|
@@ -11256,8 +10670,6 @@ var DeviceMetaSchema = object({
|
|
|
11256
10670
|
* accordion sections (with optional intra-section order). See
|
|
11257
10671
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
11258
10672
|
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
11259
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
11260
|
-
deviceLinks: array(DeviceLinkSchema).readonly().optional(),
|
|
11261
10673
|
/** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
|
|
11262
10674
|
* Optional: only present for accessory children that carry a known role. */
|
|
11263
10675
|
role: string().nullable().optional(),
|
|
@@ -11350,12 +10762,6 @@ method(object({
|
|
|
11350
10762
|
}), _void(), {
|
|
11351
10763
|
kind: "mutation",
|
|
11352
10764
|
auth: "admin"
|
|
11353
|
-
}), method(object({
|
|
11354
|
-
deviceId: number(),
|
|
11355
|
-
deviceLinks: array(DeviceLinkSchema).readonly()
|
|
11356
|
-
}), _void(), {
|
|
11357
|
-
kind: "mutation",
|
|
11358
|
-
auth: "admin"
|
|
11359
10765
|
}), method(object({
|
|
11360
10766
|
deviceId: number(),
|
|
11361
10767
|
display: DeviceDisplayOverrideSchema.nullable()
|
|
@@ -11437,7 +10843,7 @@ method(object({
|
|
|
11437
10843
|
* shipping 293 rows to find 12. */
|
|
11438
10844
|
isCamera: boolean().optional()
|
|
11439
10845
|
}), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
|
|
11440
|
-
mode:
|
|
10846
|
+
mode: LinkedDevicesModeSchema,
|
|
11441
10847
|
devices: array(LinkedDeviceSchema)
|
|
11442
10848
|
})), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
|
|
11443
10849
|
deviceId: number(),
|
|
@@ -11470,11 +10876,7 @@ method(object({
|
|
|
11470
10876
|
deviceId: number(),
|
|
11471
10877
|
entries: array(object({
|
|
11472
10878
|
capName: string(),
|
|
11473
|
-
kind: _enum([
|
|
11474
|
-
"native",
|
|
11475
|
-
"wrapped",
|
|
11476
|
-
"linked"
|
|
11477
|
-
]),
|
|
10879
|
+
kind: _enum(["native", "wrapped"]),
|
|
11478
10880
|
providerAddonId: string(),
|
|
11479
10881
|
providerNodeId: string(),
|
|
11480
10882
|
nativeAddonId: string()
|
|
@@ -11483,11 +10885,7 @@ method(object({
|
|
|
11483
10885
|
deviceId: number(),
|
|
11484
10886
|
entries: array(object({
|
|
11485
10887
|
capName: string(),
|
|
11486
|
-
kind: _enum([
|
|
11487
|
-
"native",
|
|
11488
|
-
"wrapped",
|
|
11489
|
-
"linked"
|
|
11490
|
-
]),
|
|
10888
|
+
kind: _enum(["native", "wrapped"]),
|
|
11491
10889
|
providerAddonId: string(),
|
|
11492
10890
|
providerNodeId: string(),
|
|
11493
10891
|
nativeAddonId: string()
|
|
@@ -12290,7 +11688,7 @@ var MotionAnalysisResultSchema = object({
|
|
|
12290
11688
|
frameHeight: number(),
|
|
12291
11689
|
analysisMs: number()
|
|
12292
11690
|
});
|
|
12293
|
-
method(object({
|
|
11691
|
+
DeviceType.Camera, method(object({
|
|
12294
11692
|
deviceId: number(),
|
|
12295
11693
|
frame: FrameInputSchema.optional(),
|
|
12296
11694
|
frameHandle: FrameHandleSchema.optional()
|
|
@@ -14479,6 +13877,18 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
14479
13877
|
* redirect_uri that does not start with one of these. Required —
|
|
14480
13878
|
* an empty list means the integration can never complete linking. */
|
|
14481
13879
|
allowedRedirectPrefixes: array(string()).min(1),
|
|
13880
|
+
/** Paths accepted as a `redirect_uri` when the host is PRIVATE — loopback,
|
|
13881
|
+
* RFC1918, CGNAT (100.64/10, Tailscale), link-local, IPv6 ULA, or an
|
|
13882
|
+
* `.local` / `.internal` / `.ts.net` name. Exists for self-hosted clients
|
|
13883
|
+
* whose address the hub cannot know in advance (a Home Assistant at
|
|
13884
|
+
* `http://<lan-ip>:8123/auth/external/callback`). The PATH must match
|
|
13885
|
+
* exactly; a public host never satisfies this branch, so it is not a
|
|
13886
|
+
* wildcard prefix by another name. */
|
|
13887
|
+
allowedPrivateHostPaths: array(string()).optional(),
|
|
13888
|
+
/** When true this is a PUBLIC client (source is published, no secret can be
|
|
13889
|
+
* protected) and PKCE is mandatory: `/authorize` refuses without an S256
|
|
13890
|
+
* `code_challenge`, `/token` refuses without the matching `code_verifier`. */
|
|
13891
|
+
requiresPkce: boolean().optional(),
|
|
14482
13892
|
/** Optional public origin (no trailing slash) that this integration's
|
|
14483
13893
|
* issued codes/tokens should carry as the `hubUrl` claim — typically the
|
|
14484
13894
|
* operator-selected external-access endpoint resolved by the addon. When
|
|
@@ -14629,7 +14039,7 @@ var TrackEnvelopeSchema = object({
|
|
|
14629
14039
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
14630
14040
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
14631
14041
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
14632
|
-
* zonesVisited, bestEventId, envelope) and returns `positions` /
|
|
14042
|
+
* zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
|
|
14633
14043
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
14634
14044
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
14635
14045
|
* (`getObjectEvents` et al.).
|
|
@@ -14866,6 +14276,24 @@ var TrackSchema = object({
|
|
|
14866
14276
|
* Populated from the persisted envelope columns on historical reads;
|
|
14867
14277
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
14868
14278
|
envelope: TrackEnvelopeSchema.optional(),
|
|
14279
|
+
/**
|
|
14280
|
+
* A face DETECTOR found a face on this track — nothing more. It says the
|
|
14281
|
+
* detail plane produced a `face` detail; it does NOT say the face was
|
|
14282
|
+
* embedded, matched, above `minFacePx`, or that the recognizer was even
|
|
14283
|
+
* enabled. Set once and never cleared.
|
|
14284
|
+
*
|
|
14285
|
+
* **This exists so "face present but not recognised" is expressible.** A
|
|
14286
|
+
* recognised identity lands in `subLabel` (attributed to the face chain via
|
|
14287
|
+
* `subLabelMeta.stepId`), so before this field a track with an unmatched face
|
|
14288
|
+
* and a track with no face at all were byte-identical on the wire and no
|
|
14289
|
+
* surface could tell them apart. The read is `hasFace === true && subLabel
|
|
14290
|
+
* === undefined`.
|
|
14291
|
+
*
|
|
14292
|
+
* **Absent ≠ false.** Every row written before the column existed omits it,
|
|
14293
|
+
* and so does every server that predates the field — a consumer must test
|
|
14294
|
+
* `=== true` and render nothing otherwise, never infer "no face".
|
|
14295
|
+
*/
|
|
14296
|
+
hasFace: boolean().optional(),
|
|
14869
14297
|
...TrackFlagFields,
|
|
14870
14298
|
...TrackRetrainFields
|
|
14871
14299
|
});
|
|
@@ -17480,6 +16908,10 @@ var SsoBridgeClaimsSchema = object({
|
|
|
17480
16908
|
integrationId: string().optional(),
|
|
17481
16909
|
/** JWT ID — unique per issued code; consumed-set enforces single-use. */
|
|
17482
16910
|
jti: string().optional(),
|
|
16911
|
+
/** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
|
|
16912
|
+
* client. Its PRESENCE is what makes the verifier mandatory at exchange,
|
|
16913
|
+
* so the requirement travels with the code and not with mutable config. */
|
|
16914
|
+
codeChallenge: string().optional(),
|
|
17483
16915
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
17484
16916
|
* tokens so the verify path can check the session is not revoked. */
|
|
17485
16917
|
sessionId: string().optional()
|
|
@@ -18032,7 +17464,7 @@ var ClipPlaybackSchema = object({
|
|
|
18032
17464
|
playbackEndpoints: array(string()).optional(),
|
|
18033
17465
|
token: string().optional()
|
|
18034
17466
|
});
|
|
18035
|
-
method(object({
|
|
17467
|
+
DeviceType.Camera, method(object({
|
|
18036
17468
|
deviceId: number(),
|
|
18037
17469
|
since: number(),
|
|
18038
17470
|
until: number(),
|
|
@@ -23090,440 +22522,1074 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
|
|
|
23090
22522
|
code: string()
|
|
23091
22523
|
}), object({ success: literal(true) }), {
|
|
23092
22524
|
kind: "mutation",
|
|
23093
|
-
auth: "admin",
|
|
23094
|
-
access: "create"
|
|
23095
|
-
}), method(object({ userId: string() }), object({ success: literal(true) }), {
|
|
22525
|
+
auth: "admin",
|
|
22526
|
+
access: "create"
|
|
22527
|
+
}), method(object({ userId: string() }), object({ success: literal(true) }), {
|
|
22528
|
+
kind: "mutation",
|
|
22529
|
+
auth: "admin",
|
|
22530
|
+
access: "delete"
|
|
22531
|
+
}), method(object({ userId: string() }), TotpStatusSchema, { auth: "admin" }), method(object({
|
|
22532
|
+
userId: string(),
|
|
22533
|
+
code: string()
|
|
22534
|
+
}), object({ valid: boolean() }), {
|
|
22535
|
+
kind: "mutation",
|
|
22536
|
+
access: "view"
|
|
22537
|
+
}), method(object({
|
|
22538
|
+
integrationId: string(),
|
|
22539
|
+
userId: string(),
|
|
22540
|
+
username: string(),
|
|
22541
|
+
scopes: array(TokenScopeSchema),
|
|
22542
|
+
redirectUri: string(),
|
|
22543
|
+
hubUrl: string(),
|
|
22544
|
+
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
22545
|
+
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
22546
|
+
codeChallenge: string().optional()
|
|
22547
|
+
}), object({ code: string() }), {
|
|
22548
|
+
kind: "mutation",
|
|
22549
|
+
access: "create"
|
|
22550
|
+
}), method(object({
|
|
22551
|
+
code: string(),
|
|
22552
|
+
redirectUri: string(),
|
|
22553
|
+
/** PKCE verifier. REQUIRED when the code carries a challenge. */
|
|
22554
|
+
codeVerifier: string().optional()
|
|
22555
|
+
}), object({
|
|
22556
|
+
accessToken: string(),
|
|
22557
|
+
refreshToken: string(),
|
|
22558
|
+
expiresIn: number()
|
|
22559
|
+
}).nullable(), {
|
|
22560
|
+
kind: "mutation",
|
|
22561
|
+
access: "view"
|
|
22562
|
+
}), method(object({ refreshToken: string() }), object({
|
|
22563
|
+
accessToken: string(),
|
|
22564
|
+
refreshToken: string(),
|
|
22565
|
+
expiresIn: number()
|
|
22566
|
+
}).nullable(), {
|
|
22567
|
+
kind: "mutation",
|
|
22568
|
+
access: "view"
|
|
22569
|
+
}), method(object({ token: string() }), object({
|
|
22570
|
+
userId: string(),
|
|
22571
|
+
username: string(),
|
|
22572
|
+
scopes: array(TokenScopeSchema)
|
|
22573
|
+
}).nullable(), { access: "view" }), method(_void(), array(OauthSessionSummarySchema), { auth: "admin" }), method(object({ id: string() }), object({ success: boolean() }), {
|
|
22574
|
+
kind: "mutation",
|
|
22575
|
+
auth: "admin",
|
|
22576
|
+
access: "delete"
|
|
22577
|
+
});
|
|
22578
|
+
/**
|
|
22579
|
+
* Robot-vacuum cap. Models HA `vacuum.*` entities — anything with a
|
|
22580
|
+
* cleaning lifecycle plus a return-to-base / locate surface and an
|
|
22581
|
+
* optional fan-speed selector.
|
|
22582
|
+
*
|
|
22583
|
+
* State follows HA's canonical vacuum lifecycle: `idle` / `cleaning` /
|
|
22584
|
+
* `paused` / `returning` / `docked` / `error`. `batteryLevel`
|
|
22585
|
+
* (0..100) is nullable — some vacuums don't report a battery
|
|
22586
|
+
* percentage. `fanSpeed` is the current speed token (provider-verbatim,
|
|
22587
|
+
* e.g. `'standard'` / `'turbo'`) and `availableFanSpeeds` lists the
|
|
22588
|
+
* tokens the hardware accepts so the UI renders only supported choices.
|
|
22589
|
+
*
|
|
22590
|
+
* The `setFanSpeed` method takes the bare `speed` token — the provider
|
|
22591
|
+
* validates it against the vacuum's own list. `locate` triggers the
|
|
22592
|
+
* find-me chirp; `returnToBase` sends it home.
|
|
22593
|
+
*
|
|
22594
|
+
* Consumable / waste tanks: `cleanWater` / `dirtyWater` / `detergent` /
|
|
22595
|
+
* `dustBin` each carry a nullable `{ level (0..100 %), status ('ok' |
|
|
22596
|
+
* 'low' | 'full') }` reading. Native providers (e.g. Dreame, Roborock)
|
|
22597
|
+
* SHOULD populate whichever tanks the hardware has — leave a field `null`
|
|
22598
|
+
* only when the device has no such tank at all, and use a `TankStatus`
|
|
22599
|
+
* with both inner fields `null` when the tank exists but its level is
|
|
22600
|
+
* currently unknown. HA `vacuum.*` entities expose no per-tank telemetry,
|
|
22601
|
+
* so the HA provider leaves all four `null`.
|
|
22602
|
+
*/
|
|
22603
|
+
var VacuumStateSchema = _enum([
|
|
22604
|
+
"idle",
|
|
22605
|
+
"cleaning",
|
|
22606
|
+
"paused",
|
|
22607
|
+
"returning",
|
|
22608
|
+
"docked",
|
|
22609
|
+
"drying",
|
|
22610
|
+
"error"
|
|
22611
|
+
]);
|
|
22612
|
+
/**
|
|
22613
|
+
* One consumable / waste tank on a robot vacuum (clean-water, dirty-water,
|
|
22614
|
+
* detergent or dust-bin). A tank can report a numeric fill `level` (0..100),
|
|
22615
|
+
* a discrete `status` (binary-style hardware), or both. Both `null` means the
|
|
22616
|
+
* level is currently unknown; the OWNING field being `null` means the
|
|
22617
|
+
* hardware has no such tank at all.
|
|
22618
|
+
*/
|
|
22619
|
+
var TankStatusSchema = object({
|
|
22620
|
+
/** Numeric fill 0..100 when the hardware reports a percentage; null otherwise. */
|
|
22621
|
+
level: number().min(0).max(100).nullable(),
|
|
22622
|
+
/** Discrete state when the hardware is binary-mode; null otherwise. */
|
|
22623
|
+
status: _enum([
|
|
22624
|
+
"ok",
|
|
22625
|
+
"low",
|
|
22626
|
+
"full"
|
|
22627
|
+
]).nullable()
|
|
22628
|
+
});
|
|
22629
|
+
object({
|
|
22630
|
+
/** Lifecycle state of the vacuum. */
|
|
22631
|
+
state: VacuumStateSchema,
|
|
22632
|
+
/** 0..100 battery percentage. Null when the device has no battery
|
|
22633
|
+
* reading. */
|
|
22634
|
+
batteryLevel: number().min(0).max(100).nullable(),
|
|
22635
|
+
/** Current fan-speed token (provider-verbatim). Null when unknown or
|
|
22636
|
+
* the vacuum has no speed control. */
|
|
22637
|
+
fanSpeed: string().nullable(),
|
|
22638
|
+
/** Speed tokens the hardware accepts — drives the UI selector. */
|
|
22639
|
+
availableFanSpeeds: array(string()),
|
|
22640
|
+
/** Clean-water (mop) tank. Null when the hardware has no clean-water tank. */
|
|
22641
|
+
cleanWater: TankStatusSchema.nullable(),
|
|
22642
|
+
/** Dirty-water (recovery) tank. Null when the hardware has no dirty-water tank. */
|
|
22643
|
+
dirtyWater: TankStatusSchema.nullable(),
|
|
22644
|
+
/** Detergent tank. Null when the hardware has no detergent tank. */
|
|
22645
|
+
detergent: TankStatusSchema.nullable(),
|
|
22646
|
+
/** Dust bin. Null when the hardware has no dust bin. */
|
|
22647
|
+
dustBin: TankStatusSchema.nullable(),
|
|
22648
|
+
/** 0..100 cleaning-completion percentage of the current task, or null. */
|
|
22649
|
+
progressPercent: number().min(0).max(100).nullable(),
|
|
22650
|
+
/** Current error code (0 / null = no error). */
|
|
22651
|
+
errorCode: number().nullable(),
|
|
22652
|
+
/** Human label for {@link errorCode}, or null when none / undecodable. */
|
|
22653
|
+
errorLabel: string().nullable(),
|
|
22654
|
+
/** Ms epoch when the slice was last updated. */
|
|
22655
|
+
lastChangedAt: number()
|
|
22656
|
+
});
|
|
22657
|
+
DeviceType.Vacuum, method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
22658
|
+
kind: "mutation",
|
|
22659
|
+
auth: "admin"
|
|
22660
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
22661
|
+
kind: "mutation",
|
|
22662
|
+
auth: "admin"
|
|
22663
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
22664
|
+
kind: "mutation",
|
|
22665
|
+
auth: "admin"
|
|
22666
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
22667
|
+
kind: "mutation",
|
|
22668
|
+
auth: "admin"
|
|
22669
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
22670
|
+
kind: "mutation",
|
|
22671
|
+
auth: "admin"
|
|
22672
|
+
}), method(object({
|
|
22673
|
+
deviceId: number().int().nonnegative(),
|
|
22674
|
+
speed: string().min(1)
|
|
22675
|
+
}), _void(), {
|
|
22676
|
+
kind: "mutation",
|
|
22677
|
+
auth: "admin"
|
|
22678
|
+
});
|
|
22679
|
+
object({
|
|
22680
|
+
/** Lifecycle state of the valve. */
|
|
22681
|
+
state: _enum([
|
|
22682
|
+
"open",
|
|
22683
|
+
"opening",
|
|
22684
|
+
"closing",
|
|
22685
|
+
"closed",
|
|
22686
|
+
"stopped"
|
|
22687
|
+
]),
|
|
22688
|
+
/** 0 = fully closed, 100 = fully open. Null when the device has no
|
|
22689
|
+
* intermediate position surface. */
|
|
22690
|
+
position: number().min(0).max(100).nullable(),
|
|
22691
|
+
/** Ms epoch when the slice was last updated. */
|
|
22692
|
+
lastChangedAt: number()
|
|
22693
|
+
});
|
|
22694
|
+
DeviceType.Valve, method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
22695
|
+
kind: "mutation",
|
|
22696
|
+
auth: "admin"
|
|
22697
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
23096
22698
|
kind: "mutation",
|
|
23097
|
-
auth: "admin"
|
|
23098
|
-
|
|
23099
|
-
}), method(object({ userId: string() }), TotpStatusSchema, { auth: "admin" }), method(object({
|
|
23100
|
-
userId: string(),
|
|
23101
|
-
code: string()
|
|
23102
|
-
}), object({ valid: boolean() }), {
|
|
22699
|
+
auth: "admin"
|
|
22700
|
+
}), method(object({ deviceId: number().int().nonnegative() }), _void(), {
|
|
23103
22701
|
kind: "mutation",
|
|
23104
|
-
|
|
22702
|
+
auth: "admin"
|
|
23105
22703
|
}), method(object({
|
|
23106
|
-
|
|
23107
|
-
|
|
23108
|
-
|
|
23109
|
-
scopes: array(TokenScopeSchema),
|
|
23110
|
-
redirectUri: string(),
|
|
23111
|
-
hubUrl: string()
|
|
23112
|
-
}), object({ code: string() }), {
|
|
22704
|
+
deviceId: number().int().nonnegative(),
|
|
22705
|
+
position: number().min(0).max(100)
|
|
22706
|
+
}), _void(), {
|
|
23113
22707
|
kind: "mutation",
|
|
23114
|
-
|
|
22708
|
+
auth: "admin"
|
|
22709
|
+
});
|
|
22710
|
+
object({
|
|
22711
|
+
detected: boolean(),
|
|
22712
|
+
/** Ms epoch of the last transition. 0 if never observed. */
|
|
22713
|
+
lastChangedAt: number()
|
|
22714
|
+
});
|
|
22715
|
+
DeviceType.Sensor;
|
|
22716
|
+
object({
|
|
22717
|
+
/** Current measured temperature. Null when not reported. */
|
|
22718
|
+
currentTemp: number().nullable(),
|
|
22719
|
+
/** Target temperature setpoint. Null when no setpoint surface. */
|
|
22720
|
+
targetTemp: number().nullable(),
|
|
22721
|
+
/** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
|
|
22722
|
+
* `heat_pump` / `high_demand` / `performance` / `off`). Null when the
|
|
22723
|
+
* device reports an unknown state. */
|
|
22724
|
+
operationMode: string().nullable(),
|
|
22725
|
+
/** Available operation modes = HA `operation_list`. */
|
|
22726
|
+
availableModes: array(string()),
|
|
22727
|
+
/** Away mode (HA `away_mode` 'on'/'off' → bool). Null when the device
|
|
22728
|
+
* has no away surface. */
|
|
22729
|
+
away: boolean().nullable(),
|
|
22730
|
+
/** HA `min_temp` attribute. Null when not reported. */
|
|
22731
|
+
minTemp: number().nullable(),
|
|
22732
|
+
/** HA `max_temp` attribute. Null when not reported. */
|
|
22733
|
+
maxTemp: number().nullable(),
|
|
22734
|
+
/** Ms epoch when the slice was last updated. */
|
|
22735
|
+
lastChangedAt: number()
|
|
22736
|
+
});
|
|
22737
|
+
DeviceType.WaterHeater, method(object({
|
|
22738
|
+
deviceId: number().int().nonnegative(),
|
|
22739
|
+
temp: number().finite()
|
|
22740
|
+
}), _void(), {
|
|
22741
|
+
kind: "mutation",
|
|
22742
|
+
auth: "admin"
|
|
23115
22743
|
}), method(object({
|
|
23116
|
-
|
|
23117
|
-
|
|
23118
|
-
}),
|
|
23119
|
-
accessToken: string(),
|
|
23120
|
-
refreshToken: string(),
|
|
23121
|
-
expiresIn: number()
|
|
23122
|
-
}).nullable(), {
|
|
22744
|
+
deviceId: number().int().nonnegative(),
|
|
22745
|
+
mode: string().min(1)
|
|
22746
|
+
}), _void(), {
|
|
23123
22747
|
kind: "mutation",
|
|
23124
|
-
|
|
23125
|
-
}), method(object({
|
|
23126
|
-
|
|
23127
|
-
|
|
23128
|
-
|
|
23129
|
-
}).nullable(), {
|
|
22748
|
+
auth: "admin"
|
|
22749
|
+
}), method(object({
|
|
22750
|
+
deviceId: number().int().nonnegative(),
|
|
22751
|
+
on: boolean()
|
|
22752
|
+
}), _void(), {
|
|
23130
22753
|
kind: "mutation",
|
|
23131
|
-
|
|
23132
|
-
})
|
|
23133
|
-
|
|
23134
|
-
|
|
23135
|
-
|
|
23136
|
-
|
|
22754
|
+
auth: "admin"
|
|
22755
|
+
});
|
|
22756
|
+
object({
|
|
22757
|
+
/** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
|
|
22758
|
+
* when no condition has been reported yet. */
|
|
22759
|
+
condition: string().nullable(),
|
|
22760
|
+
/** Current temperature in the reported unit. Null when not provided. */
|
|
22761
|
+
temperature: number().nullable(),
|
|
22762
|
+
/** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
|
|
22763
|
+
temperatureUnit: string().nullable(),
|
|
22764
|
+
/** Relative humidity (0..100). Null when not provided. */
|
|
22765
|
+
humidity: number().min(0).max(100).nullable(),
|
|
22766
|
+
/** Barometric pressure in the reported unit. Null when not provided. */
|
|
22767
|
+
pressure: number().nullable(),
|
|
22768
|
+
/** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
|
|
22769
|
+
pressureUnit: string().nullable(),
|
|
22770
|
+
/** Wind speed in the reported unit. Null when not provided. */
|
|
22771
|
+
windSpeed: number().nullable(),
|
|
22772
|
+
/** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
|
|
22773
|
+
windSpeedUnit: string().nullable(),
|
|
22774
|
+
/** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
|
|
22775
|
+
windBearing: number().nullable(),
|
|
22776
|
+
/** Ms epoch when the slice was last updated. */
|
|
22777
|
+
lastFetchedAt: number()
|
|
22778
|
+
});
|
|
22779
|
+
DeviceType.Weather;
|
|
22780
|
+
/**
|
|
22781
|
+
* Per-zone occupancy aggregation produced by the analytics frame
|
|
22782
|
+
* processor on every inference result. Covers the full combinatorial
|
|
22783
|
+
* matrix the operator UI needs: total objects everywhere, total
|
|
22784
|
+
* objects per zone, single class everywhere, single class per zone,
|
|
22785
|
+
* objects outside any zone.
|
|
22786
|
+
*
|
|
22787
|
+
* Counts are derived from the analytics tracker (tracked detections
|
|
22788
|
+
* with stable trackIds), not raw detector hits — this filters out
|
|
22789
|
+
* one-off detector flickers and gives counts that match what the user
|
|
22790
|
+
* sees on the live overlay.
|
|
22791
|
+
*
|
|
22792
|
+
* Overlap policy: a detection that intersects two zones counts in
|
|
22793
|
+
* BOTH zones' `byClass` and `totalObjects` (count-in-each). The
|
|
22794
|
+
* `frame` aggregate de-duplicates trivially since it's frame-wide.
|
|
22795
|
+
* `unzoned` counts only detections that landed in zero zones.
|
|
22796
|
+
*/
|
|
22797
|
+
var PerScopeBreakdownSchema = object({
|
|
22798
|
+
/** Total tracked objects in this scope (frame / zone / unzoned). */
|
|
22799
|
+
totalObjects: number().int().nonnegative(),
|
|
22800
|
+
/** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
|
|
22801
|
+
byClass: record(string(), number().int().nonnegative())
|
|
22802
|
+
});
|
|
22803
|
+
var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
|
|
22804
|
+
zoneId: string(),
|
|
22805
|
+
zoneName: string(),
|
|
22806
|
+
/** TrackIds of objects currently inside this zone — for cross-reference
|
|
22807
|
+
* with the per-track detail panel and live overlay. */
|
|
22808
|
+
trackIds: array(string()).readonly()
|
|
22809
|
+
});
|
|
22810
|
+
/**
|
|
22811
|
+
* A parked ("stationary") object surfaced alongside occupancy — an object that
|
|
22812
|
+
* settled and stopped moving. It is NO LONGER a tracked object (the tracker was
|
|
22813
|
+
* told to forget it so it stops re-spawning tracks/events), but it IS still
|
|
22814
|
+
* physically present, so it keeps counting toward `frame` occupancy and is
|
|
22815
|
+
* listed here so the UI can show it in a dedicated "Stationary" section instead
|
|
22816
|
+
* of flooding the live event feed.
|
|
22817
|
+
*/
|
|
22818
|
+
var StationaryObjectSchema = object({
|
|
22819
|
+
id: string(),
|
|
22820
|
+
className: string(),
|
|
22821
|
+
bbox: object({
|
|
22822
|
+
x: number(),
|
|
22823
|
+
y: number(),
|
|
22824
|
+
w: number(),
|
|
22825
|
+
h: number()
|
|
22826
|
+
}),
|
|
22827
|
+
frameWidth: number().int().nonnegative(),
|
|
22828
|
+
frameHeight: number().int().nonnegative(),
|
|
22829
|
+
/** When the source track was first seen. */
|
|
22830
|
+
firstSeenAt: number().int(),
|
|
22831
|
+
/** When the object was recognised as parked (promotion time). */
|
|
22832
|
+
becameStationaryAt: number().int(),
|
|
22833
|
+
/** Last frame a detection confirmed the object is still there. */
|
|
22834
|
+
lastConfirmedAt: number().int(),
|
|
22835
|
+
/** Enrichment label carried from the source track (identity / plate). */
|
|
22836
|
+
label: string().optional(),
|
|
22837
|
+
/** Native-resolution key-frame media key for the parked object's best image. */
|
|
22838
|
+
keyFrameMediaKey: string().optional()
|
|
22839
|
+
});
|
|
22840
|
+
var CameraOccupancySnapshotSchema = object({
|
|
22841
|
+
/** Frame timestamp of the inference result that produced this snapshot. */
|
|
22842
|
+
ts: number().int(),
|
|
22843
|
+
/** Frame width/height in pixels — let the UI normalize bbox coords. */
|
|
22844
|
+
frameWidth: number().int().nonnegative(),
|
|
22845
|
+
frameHeight: number().int().nonnegative(),
|
|
22846
|
+
/** Per-zone breakdown — one entry per defined zone (user + onboard). */
|
|
22847
|
+
zones: array(ZoneScopeBreakdownSchema).readonly(),
|
|
22848
|
+
/** Frame-wide aggregate (everywhere, regardless of zone membership).
|
|
22849
|
+
* INCLUDES currently-confirmed stationary objects (they are still present). */
|
|
22850
|
+
frame: PerScopeBreakdownSchema,
|
|
22851
|
+
/** Detections that landed outside every zone. Empty when no zones defined. */
|
|
22852
|
+
unzoned: PerScopeBreakdownSchema,
|
|
22853
|
+
/** Parked objects on this camera (additive — absent on legacy snapshots).
|
|
22854
|
+
* Surfaced separately so the UI shows them in a dedicated section rather
|
|
22855
|
+
* than as repeated tracks/events. */
|
|
22856
|
+
stationaryObjects: array(StationaryObjectSchema).readonly().optional()
|
|
22857
|
+
});
|
|
22858
|
+
/**
|
|
22859
|
+
* Time-series resolution. The history methods return one bucket per
|
|
22860
|
+
* step over the requested range. Smaller resolutions cost more
|
|
22861
|
+
* memory + bandwidth; bound to discrete steps so caller cannot ask
|
|
22862
|
+
* for arbitrary fractional buckets.
|
|
22863
|
+
*/
|
|
22864
|
+
var HistoryResolutionEnum = _enum([
|
|
22865
|
+
"minute",
|
|
22866
|
+
"5min",
|
|
22867
|
+
"hour"
|
|
22868
|
+
]);
|
|
22869
|
+
var HistoryRangeSchema = object({
|
|
22870
|
+
/** Range start (epoch ms, inclusive). */
|
|
22871
|
+
from: number().int(),
|
|
22872
|
+
/** Range end (epoch ms, inclusive). Defaults to "now" at query time. */
|
|
22873
|
+
to: number().int(),
|
|
22874
|
+
resolution: HistoryResolutionEnum
|
|
22875
|
+
});
|
|
22876
|
+
var HistoryPointSchema = object({
|
|
22877
|
+
/** Bucket midpoint (epoch ms). */
|
|
22878
|
+
ts: number().int(),
|
|
22879
|
+
/** Object count averaged over the bucket (rounded to nearest integer). */
|
|
22880
|
+
count: number().int().nonnegative()
|
|
22881
|
+
});
|
|
22882
|
+
DeviceType.Camera, method(object({ deviceId: number() }), CameraOccupancySnapshotSchema.nullable()), method(object({
|
|
22883
|
+
deviceId: number(),
|
|
22884
|
+
zoneId: string(),
|
|
22885
|
+
className: string().optional()
|
|
22886
|
+
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
|
|
22887
|
+
deviceId: number(),
|
|
22888
|
+
className: string().optional()
|
|
22889
|
+
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly()), method(object({
|
|
22890
|
+
deviceId: number(),
|
|
22891
|
+
className: string().optional()
|
|
22892
|
+
}).extend(HistoryRangeSchema.shape), array(HistoryPointSchema).readonly());
|
|
22893
|
+
/**
|
|
22894
|
+
* Stages a {@link ZoneRule} can apply to. Discriminator on the rules
|
|
22895
|
+
* cap so a single CRUD surface backs every consumer; each stage has
|
|
22896
|
+
* its own dev-state mirror slice (`motion-zone-rules`,
|
|
22897
|
+
* `detection-zone-rules`, …) so consumer addons subscribe independently.
|
|
22898
|
+
*
|
|
22899
|
+
* Extend the enum here when a new gating consumer comes online (audio
|
|
22900
|
+
* gating, alert filtering, …) — no other surface needs to change.
|
|
22901
|
+
*/
|
|
22902
|
+
var ZoneRuleStageEnum = _enum([
|
|
22903
|
+
"motion",
|
|
22904
|
+
"detection",
|
|
22905
|
+
"package"
|
|
22906
|
+
]);
|
|
22907
|
+
DeviceType.Camera, method(object({
|
|
22908
|
+
deviceId: number(),
|
|
22909
|
+
stage: ZoneRuleStageEnum
|
|
22910
|
+
}), array(ZoneRuleSchema).readonly()), method(object({
|
|
22911
|
+
deviceId: number(),
|
|
22912
|
+
stage: ZoneRuleStageEnum,
|
|
22913
|
+
rules: array(ZoneRuleSchema).readonly()
|
|
22914
|
+
}), _void(), {
|
|
23137
22915
|
kind: "mutation",
|
|
23138
|
-
auth: "admin"
|
|
23139
|
-
|
|
22916
|
+
auth: "admin"
|
|
22917
|
+
}), object({
|
|
22918
|
+
motion: array(ZoneRuleSchema).readonly(),
|
|
22919
|
+
detection: array(ZoneRuleSchema).readonly(),
|
|
22920
|
+
package: array(ZoneRuleSchema).readonly()
|
|
23140
22921
|
});
|
|
23141
22922
|
/**
|
|
23142
|
-
*
|
|
23143
|
-
*
|
|
23144
|
-
*
|
|
22923
|
+
* Accessory device helpers — shared across drivers.
|
|
22924
|
+
*
|
|
22925
|
+
* Many vendor-specific drivers register accessory child devices on
|
|
22926
|
+
* top of a parent (Reolink: siren / floodlight / PIR / autotrack /
|
|
22927
|
+
* chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
|
|
22928
|
+
* driver picks the right `DeviceType` + `DeviceRole` explicitly when
|
|
22929
|
+
* spawning, builds a name derived from the parent, and produces a
|
|
22930
|
+
* stableId tied to the parent so boot-restore can reconstruct the
|
|
22931
|
+
* relationship.
|
|
22932
|
+
*
|
|
22933
|
+
* Centralised `(kind → DeviceType)` mapping was dropped on purpose:
|
|
22934
|
+
* drivers may reasonably disagree on the right type for an accessory
|
|
22935
|
+
* (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
|
|
22936
|
+
* read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
|
|
22937
|
+
* one canonical mapping was over-prescriptive and added a layer of
|
|
22938
|
+
* indirection without saving meaningful code at call sites — the
|
|
22939
|
+
* driver knows its own hardware best.
|
|
22940
|
+
*/
|
|
22941
|
+
/**
|
|
22942
|
+
* Subset of `DeviceRole` values that drivers register as child
|
|
22943
|
+
* accessories of a parent device. Sourced verbatim from `DeviceRole`
|
|
22944
|
+
* — `AccessoryKind` is the alias drivers use when building accessory
|
|
22945
|
+
* children, so the call site reads as
|
|
22946
|
+
* `accessoryStableId(parent, AccessoryKind.Siren)` rather than
|
|
22947
|
+
* `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
|
|
22948
|
+
* any role works, including non-accessory ones like Doorbell).
|
|
22949
|
+
*/
|
|
22950
|
+
var AccessoryKind = {
|
|
22951
|
+
Siren: DeviceRole.Siren,
|
|
22952
|
+
Floodlight: DeviceRole.Floodlight,
|
|
22953
|
+
Spotlight: DeviceRole.Spotlight,
|
|
22954
|
+
PirSensor: DeviceRole.PirSensor,
|
|
22955
|
+
Chime: DeviceRole.Chime,
|
|
22956
|
+
Autotrack: DeviceRole.Autotrack,
|
|
22957
|
+
Nightvision: DeviceRole.Nightvision,
|
|
22958
|
+
PrivacyMask: DeviceRole.PrivacyMask
|
|
22959
|
+
};
|
|
22960
|
+
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
22961
|
+
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;
|
|
22962
|
+
new Set(Object.values(DeviceType));
|
|
22963
|
+
DeviceFeature.BatteryOperated;
|
|
22964
|
+
/**
|
|
22965
|
+
* Error types for the safe expression engine. Two distinct classes so callers
|
|
22966
|
+
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
22967
|
+
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
22968
|
+
*/
|
|
22969
|
+
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
22970
|
+
* the failure is anchored to a character (author-facing inline feedback). */
|
|
22971
|
+
var ExpressionParseError = class extends Error {
|
|
22972
|
+
position;
|
|
22973
|
+
constructor(message, position) {
|
|
22974
|
+
super(message);
|
|
22975
|
+
this.name = "ExpressionParseError";
|
|
22976
|
+
this.position = position;
|
|
22977
|
+
}
|
|
22978
|
+
};
|
|
22979
|
+
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
22980
|
+
* result, unknown builtin, step-budget exceeded). */
|
|
22981
|
+
var ExpressionEvalError = class extends Error {
|
|
22982
|
+
constructor(message) {
|
|
22983
|
+
super(message);
|
|
22984
|
+
this.name = "ExpressionEvalError";
|
|
22985
|
+
}
|
|
22986
|
+
};
|
|
22987
|
+
/**
|
|
22988
|
+
* Frozen, null-prototype builtin function table for the expression engine
|
|
22989
|
+
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
22990
|
+
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
22991
|
+
* own-property check against it.
|
|
23145
22992
|
*
|
|
23146
|
-
*
|
|
23147
|
-
*
|
|
23148
|
-
*
|
|
23149
|
-
*
|
|
23150
|
-
*
|
|
23151
|
-
* tokens the hardware accepts so the UI renders only supported choices.
|
|
22993
|
+
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
22994
|
+
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
22995
|
+
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
22996
|
+
* (there is no `Object.prototype` in the chain), so those names are not
|
|
22997
|
+
* callable — they are simply "unknown function" at parse time.
|
|
23152
22998
|
*
|
|
23153
|
-
*
|
|
23154
|
-
*
|
|
23155
|
-
*
|
|
22999
|
+
* Every numeric argument is validated as a finite number and every numeric
|
|
23000
|
+
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
23001
|
+
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
23002
|
+
* closed rather than emitting a garbage value.
|
|
23003
|
+
*/
|
|
23004
|
+
function asFiniteNumber(value, name, index) {
|
|
23005
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
23006
|
+
return value;
|
|
23007
|
+
}
|
|
23008
|
+
function asString$1(value, name, index) {
|
|
23009
|
+
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
23010
|
+
return value;
|
|
23011
|
+
}
|
|
23012
|
+
function finiteResult(value, name) {
|
|
23013
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
23014
|
+
return value;
|
|
23015
|
+
}
|
|
23016
|
+
function allFiniteNumbers(args, name) {
|
|
23017
|
+
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
23018
|
+
}
|
|
23019
|
+
var INF = Number.POSITIVE_INFINITY;
|
|
23020
|
+
var table = {
|
|
23021
|
+
min: {
|
|
23022
|
+
minArgs: 1,
|
|
23023
|
+
maxArgs: INF,
|
|
23024
|
+
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
23025
|
+
},
|
|
23026
|
+
max: {
|
|
23027
|
+
minArgs: 1,
|
|
23028
|
+
maxArgs: INF,
|
|
23029
|
+
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
23030
|
+
},
|
|
23031
|
+
abs: {
|
|
23032
|
+
minArgs: 1,
|
|
23033
|
+
maxArgs: 1,
|
|
23034
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
23035
|
+
},
|
|
23036
|
+
floor: {
|
|
23037
|
+
minArgs: 1,
|
|
23038
|
+
maxArgs: 1,
|
|
23039
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
23040
|
+
},
|
|
23041
|
+
ceil: {
|
|
23042
|
+
minArgs: 1,
|
|
23043
|
+
maxArgs: 1,
|
|
23044
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
23045
|
+
},
|
|
23046
|
+
sqrt: {
|
|
23047
|
+
minArgs: 1,
|
|
23048
|
+
maxArgs: 1,
|
|
23049
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
23050
|
+
},
|
|
23051
|
+
round: {
|
|
23052
|
+
minArgs: 1,
|
|
23053
|
+
maxArgs: 2,
|
|
23054
|
+
apply: (args) => {
|
|
23055
|
+
const x = asFiniteNumber(args[0], "round", 0);
|
|
23056
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
23057
|
+
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
23058
|
+
const factor = 10 ** digits;
|
|
23059
|
+
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
23060
|
+
}
|
|
23061
|
+
},
|
|
23062
|
+
pow: {
|
|
23063
|
+
minArgs: 2,
|
|
23064
|
+
maxArgs: 2,
|
|
23065
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
23066
|
+
},
|
|
23067
|
+
clamp: {
|
|
23068
|
+
minArgs: 3,
|
|
23069
|
+
maxArgs: 3,
|
|
23070
|
+
apply: (args) => {
|
|
23071
|
+
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
23072
|
+
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
23073
|
+
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
23074
|
+
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
23075
|
+
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
23076
|
+
}
|
|
23077
|
+
},
|
|
23078
|
+
avg: {
|
|
23079
|
+
minArgs: 1,
|
|
23080
|
+
maxArgs: INF,
|
|
23081
|
+
apply: (args) => {
|
|
23082
|
+
const nums = allFiniteNumbers(args, "avg");
|
|
23083
|
+
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
23084
|
+
}
|
|
23085
|
+
},
|
|
23086
|
+
sum: {
|
|
23087
|
+
minArgs: 1,
|
|
23088
|
+
maxArgs: INF,
|
|
23089
|
+
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
23090
|
+
},
|
|
23091
|
+
coalesce: {
|
|
23092
|
+
minArgs: 1,
|
|
23093
|
+
maxArgs: INF,
|
|
23094
|
+
apply: (args) => {
|
|
23095
|
+
for (const a of args) if (a !== null) return a;
|
|
23096
|
+
return null;
|
|
23097
|
+
}
|
|
23098
|
+
},
|
|
23099
|
+
age: {
|
|
23100
|
+
minArgs: 2,
|
|
23101
|
+
maxArgs: 2,
|
|
23102
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
23103
|
+
},
|
|
23104
|
+
convert: {
|
|
23105
|
+
minArgs: 3,
|
|
23106
|
+
maxArgs: 3,
|
|
23107
|
+
apply: (args, hooks) => {
|
|
23108
|
+
const x = asFiniteNumber(args[0], "convert", 0);
|
|
23109
|
+
const from = asString$1(args[1], "convert", 1).trim();
|
|
23110
|
+
const to = asString$1(args[2], "convert", 2).trim();
|
|
23111
|
+
if (hooks.convert) {
|
|
23112
|
+
const out = hooks.convert(x, from, to);
|
|
23113
|
+
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
23114
|
+
return finiteResult(out, "convert");
|
|
23115
|
+
}
|
|
23116
|
+
if (from === to) return x;
|
|
23117
|
+
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
23118
|
+
}
|
|
23119
|
+
}
|
|
23120
|
+
};
|
|
23121
|
+
Object.freeze(Object.assign(Object.create(null), table));
|
|
23122
|
+
/** The set of valid builtin names — used by the parser to reject unknown
|
|
23123
|
+
* callees at parse time (immediate author feedback). */
|
|
23124
|
+
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
23125
|
+
/**
|
|
23126
|
+
* Resource-bound constants for the safe expression engine.
|
|
23156
23127
|
*
|
|
23157
|
-
*
|
|
23158
|
-
*
|
|
23159
|
-
*
|
|
23160
|
-
*
|
|
23161
|
-
*
|
|
23162
|
-
* with both inner fields `null` when the tank exists but its level is
|
|
23163
|
-
* currently unknown. HA `vacuum.*` entities expose no per-tank telemetry,
|
|
23164
|
-
* so the HA provider leaves all four `null`.
|
|
23128
|
+
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
23129
|
+
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
23130
|
+
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
23131
|
+
* work a single author-supplied expression can request, so a hostile or
|
|
23132
|
+
* accidental pathological string can never spend unbounded CPU/memory.
|
|
23165
23133
|
*/
|
|
23166
|
-
|
|
23167
|
-
|
|
23168
|
-
|
|
23169
|
-
|
|
23170
|
-
|
|
23171
|
-
|
|
23172
|
-
|
|
23173
|
-
|
|
23134
|
+
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
23135
|
+
* rejected without allocation. */
|
|
23136
|
+
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
23137
|
+
/** A legal binding / identifier name. */
|
|
23138
|
+
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
23139
|
+
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
23140
|
+
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
23141
|
+
var RESERVED_BINDING_NAMES = new Set([
|
|
23142
|
+
"now",
|
|
23143
|
+
"true",
|
|
23144
|
+
"false",
|
|
23145
|
+
"null"
|
|
23174
23146
|
]);
|
|
23175
23147
|
/**
|
|
23176
|
-
*
|
|
23177
|
-
*
|
|
23178
|
-
*
|
|
23179
|
-
*
|
|
23180
|
-
*
|
|
23148
|
+
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
23149
|
+
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
23150
|
+
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
23151
|
+
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
23152
|
+
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
23153
|
+
* is a parse error with a source position, so member access / assignment /
|
|
23154
|
+
* template literals are lexically impossible.
|
|
23181
23155
|
*/
|
|
23182
|
-
var
|
|
23183
|
-
|
|
23184
|
-
|
|
23185
|
-
|
|
23186
|
-
|
|
23187
|
-
|
|
23188
|
-
|
|
23189
|
-
|
|
23190
|
-
|
|
23191
|
-
|
|
23192
|
-
|
|
23193
|
-
|
|
23194
|
-
|
|
23195
|
-
|
|
23196
|
-
|
|
23197
|
-
|
|
23198
|
-
|
|
23199
|
-
|
|
23200
|
-
|
|
23201
|
-
|
|
23202
|
-
|
|
23203
|
-
|
|
23204
|
-
|
|
23205
|
-
|
|
23206
|
-
|
|
23207
|
-
|
|
23208
|
-
|
|
23209
|
-
|
|
23210
|
-
|
|
23211
|
-
|
|
23212
|
-
|
|
23213
|
-
|
|
23214
|
-
|
|
23215
|
-
|
|
23216
|
-
|
|
23217
|
-
|
|
23218
|
-
|
|
23219
|
-
|
|
23220
|
-
|
|
23221
|
-
|
|
23222
|
-
|
|
23223
|
-
|
|
23224
|
-
|
|
23225
|
-
|
|
23226
|
-
|
|
23227
|
-
|
|
23228
|
-
|
|
23229
|
-
|
|
23230
|
-
|
|
23231
|
-
|
|
23232
|
-
|
|
23233
|
-
|
|
23234
|
-
|
|
23235
|
-
|
|
23236
|
-
|
|
23237
|
-
|
|
23238
|
-
|
|
23239
|
-
|
|
23240
|
-
|
|
23241
|
-
|
|
23242
|
-
|
|
23243
|
-
|
|
23244
|
-
|
|
23245
|
-
|
|
23246
|
-
|
|
23247
|
-
|
|
23248
|
-
|
|
23249
|
-
|
|
23250
|
-
|
|
23251
|
-
|
|
23252
|
-
|
|
23253
|
-
|
|
23254
|
-
|
|
23255
|
-
|
|
23256
|
-
|
|
23257
|
-
|
|
23258
|
-
|
|
23259
|
-
|
|
23260
|
-
|
|
23261
|
-
|
|
23262
|
-
|
|
23263
|
-
}
|
|
23264
|
-
|
|
23265
|
-
|
|
23266
|
-
|
|
23267
|
-
|
|
23268
|
-
|
|
23269
|
-
|
|
23270
|
-
|
|
23271
|
-
|
|
23272
|
-
});
|
|
23273
|
-
|
|
23274
|
-
|
|
23275
|
-
|
|
23276
|
-
|
|
23277
|
-
});
|
|
23278
|
-
|
|
23279
|
-
|
|
23280
|
-
|
|
23281
|
-
|
|
23282
|
-
|
|
23283
|
-
|
|
23284
|
-
|
|
23285
|
-
|
|
23286
|
-
|
|
23287
|
-
|
|
23288
|
-
|
|
23289
|
-
|
|
23290
|
-
|
|
23291
|
-
|
|
23292
|
-
|
|
23293
|
-
|
|
23294
|
-
|
|
23295
|
-
|
|
23296
|
-
|
|
23297
|
-
|
|
23298
|
-
|
|
23299
|
-
});
|
|
23300
|
-
|
|
23301
|
-
|
|
23302
|
-
|
|
23303
|
-
|
|
23304
|
-
|
|
23305
|
-
|
|
23306
|
-
}
|
|
23307
|
-
|
|
23308
|
-
|
|
23309
|
-
|
|
23310
|
-
|
|
23311
|
-
|
|
23312
|
-
|
|
23313
|
-
|
|
23314
|
-
|
|
23315
|
-
}), _void(), {
|
|
23316
|
-
kind: "mutation",
|
|
23317
|
-
auth: "admin"
|
|
23318
|
-
});
|
|
23319
|
-
object({
|
|
23320
|
-
/** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
|
|
23321
|
-
* when no condition has been reported yet. */
|
|
23322
|
-
condition: string().nullable(),
|
|
23323
|
-
/** Current temperature in the reported unit. Null when not provided. */
|
|
23324
|
-
temperature: number().nullable(),
|
|
23325
|
-
/** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
|
|
23326
|
-
temperatureUnit: string().nullable(),
|
|
23327
|
-
/** Relative humidity (0..100). Null when not provided. */
|
|
23328
|
-
humidity: number().min(0).max(100).nullable(),
|
|
23329
|
-
/** Barometric pressure in the reported unit. Null when not provided. */
|
|
23330
|
-
pressure: number().nullable(),
|
|
23331
|
-
/** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
|
|
23332
|
-
pressureUnit: string().nullable(),
|
|
23333
|
-
/** Wind speed in the reported unit. Null when not provided. */
|
|
23334
|
-
windSpeed: number().nullable(),
|
|
23335
|
-
/** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
|
|
23336
|
-
windSpeedUnit: string().nullable(),
|
|
23337
|
-
/** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
|
|
23338
|
-
windBearing: number().nullable(),
|
|
23339
|
-
/** Ms epoch when the slice was last updated. */
|
|
23340
|
-
lastFetchedAt: number()
|
|
23341
|
-
});
|
|
23342
|
-
DeviceType.Weather;
|
|
23156
|
+
var KEYWORDS = new Set([
|
|
23157
|
+
"true",
|
|
23158
|
+
"false",
|
|
23159
|
+
"null"
|
|
23160
|
+
]);
|
|
23161
|
+
function isDigit(ch) {
|
|
23162
|
+
return ch >= "0" && ch <= "9";
|
|
23163
|
+
}
|
|
23164
|
+
function isIdentStart(ch) {
|
|
23165
|
+
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
23166
|
+
}
|
|
23167
|
+
function isIdentPart(ch) {
|
|
23168
|
+
return isIdentStart(ch) || isDigit(ch);
|
|
23169
|
+
}
|
|
23170
|
+
function isWhitespace(ch) {
|
|
23171
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
23172
|
+
}
|
|
23173
|
+
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
23174
|
+
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
23175
|
+
* string. */
|
|
23176
|
+
function tokenize(source) {
|
|
23177
|
+
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
23178
|
+
const tokens = [];
|
|
23179
|
+
let i = 0;
|
|
23180
|
+
const n = source.length;
|
|
23181
|
+
while (i < n) {
|
|
23182
|
+
const ch = source[i];
|
|
23183
|
+
if (isWhitespace(ch)) {
|
|
23184
|
+
i += 1;
|
|
23185
|
+
continue;
|
|
23186
|
+
}
|
|
23187
|
+
if (isDigit(ch)) {
|
|
23188
|
+
const start = i;
|
|
23189
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
23190
|
+
if (i < n && source[i] === ".") {
|
|
23191
|
+
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
23192
|
+
i += 1;
|
|
23193
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
23194
|
+
}
|
|
23195
|
+
const text = source.slice(start, i);
|
|
23196
|
+
const value = Number(text);
|
|
23197
|
+
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
23198
|
+
tokens.push({
|
|
23199
|
+
type: "number",
|
|
23200
|
+
value,
|
|
23201
|
+
pos: start
|
|
23202
|
+
});
|
|
23203
|
+
continue;
|
|
23204
|
+
}
|
|
23205
|
+
if (ch === "'" || ch === "\"") {
|
|
23206
|
+
const quote = ch;
|
|
23207
|
+
const start = i;
|
|
23208
|
+
i += 1;
|
|
23209
|
+
let out = "";
|
|
23210
|
+
let closed = false;
|
|
23211
|
+
while (i < n) {
|
|
23212
|
+
const c = source[i];
|
|
23213
|
+
if (c === "\\") {
|
|
23214
|
+
const next = i + 1 < n ? source[i + 1] : "";
|
|
23215
|
+
if (next === "\\" || next === "'" || next === "\"") {
|
|
23216
|
+
out += next;
|
|
23217
|
+
i += 2;
|
|
23218
|
+
continue;
|
|
23219
|
+
}
|
|
23220
|
+
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
23221
|
+
}
|
|
23222
|
+
if (c === quote) {
|
|
23223
|
+
closed = true;
|
|
23224
|
+
i += 1;
|
|
23225
|
+
break;
|
|
23226
|
+
}
|
|
23227
|
+
out += c;
|
|
23228
|
+
i += 1;
|
|
23229
|
+
}
|
|
23230
|
+
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
23231
|
+
tokens.push({
|
|
23232
|
+
type: "string",
|
|
23233
|
+
value: out,
|
|
23234
|
+
pos: start
|
|
23235
|
+
});
|
|
23236
|
+
continue;
|
|
23237
|
+
}
|
|
23238
|
+
if (isIdentStart(ch)) {
|
|
23239
|
+
const start = i;
|
|
23240
|
+
while (i < n && isIdentPart(source[i])) i += 1;
|
|
23241
|
+
const text = source.slice(start, i);
|
|
23242
|
+
if (KEYWORDS.has(text)) tokens.push({
|
|
23243
|
+
type: "keyword",
|
|
23244
|
+
keyword: keywordOf(text),
|
|
23245
|
+
pos: start
|
|
23246
|
+
});
|
|
23247
|
+
else tokens.push({
|
|
23248
|
+
type: "identifier",
|
|
23249
|
+
name: text,
|
|
23250
|
+
pos: start
|
|
23251
|
+
});
|
|
23252
|
+
continue;
|
|
23253
|
+
}
|
|
23254
|
+
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
23255
|
+
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
23256
|
+
tokens.push({
|
|
23257
|
+
type: "punct",
|
|
23258
|
+
punct: two,
|
|
23259
|
+
pos: i
|
|
23260
|
+
});
|
|
23261
|
+
i += 2;
|
|
23262
|
+
continue;
|
|
23263
|
+
}
|
|
23264
|
+
if (isSinglePunct(ch)) {
|
|
23265
|
+
tokens.push({
|
|
23266
|
+
type: "punct",
|
|
23267
|
+
punct: ch,
|
|
23268
|
+
pos: i
|
|
23269
|
+
});
|
|
23270
|
+
i += 1;
|
|
23271
|
+
continue;
|
|
23272
|
+
}
|
|
23273
|
+
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
23274
|
+
}
|
|
23275
|
+
tokens.push({
|
|
23276
|
+
type: "eof",
|
|
23277
|
+
pos: n
|
|
23278
|
+
});
|
|
23279
|
+
return tokens;
|
|
23280
|
+
}
|
|
23281
|
+
function keywordOf(text) {
|
|
23282
|
+
if (text === "true") return "true";
|
|
23283
|
+
if (text === "false") return "false";
|
|
23284
|
+
return "null";
|
|
23285
|
+
}
|
|
23286
|
+
function isSinglePunct(ch) {
|
|
23287
|
+
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
23288
|
+
}
|
|
23343
23289
|
/**
|
|
23344
|
-
*
|
|
23345
|
-
* processor on every inference result. Covers the full combinatorial
|
|
23346
|
-
* matrix the operator UI needs: total objects everywhere, total
|
|
23347
|
-
* objects per zone, single class everywhere, single class per zone,
|
|
23348
|
-
* objects outside any zone.
|
|
23290
|
+
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
23349
23291
|
*
|
|
23350
|
-
*
|
|
23351
|
-
*
|
|
23352
|
-
*
|
|
23353
|
-
*
|
|
23292
|
+
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
23293
|
+
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
23294
|
+
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
23295
|
+
* string validated against the builtin table at parse time, so an unknown
|
|
23296
|
+
* function is rejected immediately (author feedback) and a persisted expression
|
|
23297
|
+
* that references a since-removed builtin degrades at read.
|
|
23354
23298
|
*
|
|
23355
|
-
*
|
|
23356
|
-
*
|
|
23357
|
-
* `frame` aggregate de-duplicates trivially since it's frame-wide.
|
|
23358
|
-
* `unzoned` counts only detections that landed in zero zones.
|
|
23359
|
-
*/
|
|
23360
|
-
var PerScopeBreakdownSchema = object({
|
|
23361
|
-
/** Total tracked objects in this scope (frame / zone / unzoned). */
|
|
23362
|
-
totalObjects: number().int().nonnegative(),
|
|
23363
|
-
/** Per-class count. Keys are macro class names (e.g. `person`, `car`). */
|
|
23364
|
-
byClass: record(string(), number().int().nonnegative())
|
|
23365
|
-
});
|
|
23366
|
-
var ZoneScopeBreakdownSchema = PerScopeBreakdownSchema.extend({
|
|
23367
|
-
zoneId: string(),
|
|
23368
|
-
zoneName: string(),
|
|
23369
|
-
/** TrackIds of objects currently inside this zone — for cross-reference
|
|
23370
|
-
* with the per-track detail panel and live overlay. */
|
|
23371
|
-
trackIds: array(string()).readonly()
|
|
23372
|
-
});
|
|
23373
|
-
/**
|
|
23374
|
-
* A parked ("stationary") object surfaced alongside occupancy — an object that
|
|
23375
|
-
* settled and stopped moving. It is NO LONGER a tracked object (the tracker was
|
|
23376
|
-
* told to forget it so it stops re-spawning tracks/events), but it IS still
|
|
23377
|
-
* physically present, so it keeps counting toward `frame` occupancy and is
|
|
23378
|
-
* listed here so the UI can show it in a dedicated "Stationary" section instead
|
|
23379
|
-
* of flooding the live event feed.
|
|
23299
|
+
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
23300
|
+
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
23380
23301
|
*/
|
|
23381
|
-
|
|
23382
|
-
|
|
23383
|
-
|
|
23384
|
-
|
|
23385
|
-
|
|
23386
|
-
|
|
23387
|
-
|
|
23388
|
-
|
|
23389
|
-
|
|
23390
|
-
|
|
23391
|
-
|
|
23392
|
-
|
|
23393
|
-
|
|
23394
|
-
|
|
23395
|
-
|
|
23396
|
-
|
|
23397
|
-
|
|
23398
|
-
|
|
23399
|
-
|
|
23400
|
-
|
|
23401
|
-
|
|
23402
|
-
}
|
|
23403
|
-
var
|
|
23404
|
-
|
|
23405
|
-
|
|
23406
|
-
|
|
23407
|
-
|
|
23408
|
-
|
|
23409
|
-
|
|
23410
|
-
|
|
23411
|
-
|
|
23412
|
-
|
|
23413
|
-
|
|
23414
|
-
|
|
23415
|
-
|
|
23416
|
-
|
|
23417
|
-
|
|
23418
|
-
|
|
23419
|
-
|
|
23420
|
-
|
|
23302
|
+
/** Binary/logical operator precedence (higher binds tighter). */
|
|
23303
|
+
var BINARY_PRECEDENCE = {
|
|
23304
|
+
"||": 1,
|
|
23305
|
+
"&&": 2,
|
|
23306
|
+
"==": 3,
|
|
23307
|
+
"!=": 3,
|
|
23308
|
+
"<": 4,
|
|
23309
|
+
"<=": 4,
|
|
23310
|
+
">": 4,
|
|
23311
|
+
">=": 4,
|
|
23312
|
+
"+": 5,
|
|
23313
|
+
"-": 5,
|
|
23314
|
+
"*": 6,
|
|
23315
|
+
"/": 6,
|
|
23316
|
+
"%": 6
|
|
23317
|
+
};
|
|
23318
|
+
function isLogicalOp(op) {
|
|
23319
|
+
return op === "&&" || op === "||";
|
|
23320
|
+
}
|
|
23321
|
+
function isBinaryOp(op) {
|
|
23322
|
+
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
23323
|
+
}
|
|
23324
|
+
var Parser = class {
|
|
23325
|
+
tokens;
|
|
23326
|
+
pos = 0;
|
|
23327
|
+
nodeCount = 0;
|
|
23328
|
+
identifiers = /* @__PURE__ */ new Set();
|
|
23329
|
+
callees = /* @__PURE__ */ new Set();
|
|
23330
|
+
constructor(tokens) {
|
|
23331
|
+
this.tokens = tokens;
|
|
23332
|
+
}
|
|
23333
|
+
parse() {
|
|
23334
|
+
const ast = this.parseTernary();
|
|
23335
|
+
const tok = this.peek();
|
|
23336
|
+
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
23337
|
+
return {
|
|
23338
|
+
ast,
|
|
23339
|
+
identifiers: this.identifiers,
|
|
23340
|
+
callees: this.callees,
|
|
23341
|
+
nodeCount: this.nodeCount
|
|
23342
|
+
};
|
|
23343
|
+
}
|
|
23344
|
+
peek() {
|
|
23345
|
+
return this.tokens[this.pos];
|
|
23346
|
+
}
|
|
23347
|
+
next() {
|
|
23348
|
+
return this.tokens[this.pos++];
|
|
23349
|
+
}
|
|
23350
|
+
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
23351
|
+
expectPunct(punct) {
|
|
23352
|
+
const tok = this.peek();
|
|
23353
|
+
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
23354
|
+
this.pos += 1;
|
|
23355
|
+
}
|
|
23356
|
+
matchPunct(punct) {
|
|
23357
|
+
const tok = this.peek();
|
|
23358
|
+
if (tok.type === "punct" && tok.punct === punct) {
|
|
23359
|
+
this.pos += 1;
|
|
23360
|
+
return true;
|
|
23361
|
+
}
|
|
23362
|
+
return false;
|
|
23363
|
+
}
|
|
23364
|
+
countNode() {
|
|
23365
|
+
this.nodeCount += 1;
|
|
23366
|
+
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
23367
|
+
}
|
|
23368
|
+
parseTernary() {
|
|
23369
|
+
const test = this.parseBinary(1);
|
|
23370
|
+
if (this.matchPunct("?")) {
|
|
23371
|
+
const consequent = this.parseTernary();
|
|
23372
|
+
this.expectPunct(":");
|
|
23373
|
+
const alternate = this.parseTernary();
|
|
23374
|
+
this.countNode();
|
|
23375
|
+
return {
|
|
23376
|
+
kind: "conditional",
|
|
23377
|
+
test,
|
|
23378
|
+
consequent,
|
|
23379
|
+
alternate
|
|
23380
|
+
};
|
|
23381
|
+
}
|
|
23382
|
+
return test;
|
|
23383
|
+
}
|
|
23384
|
+
parseBinary(minPrec) {
|
|
23385
|
+
let left = this.parseUnary();
|
|
23386
|
+
for (;;) {
|
|
23387
|
+
const tok = this.peek();
|
|
23388
|
+
if (tok.type !== "punct") break;
|
|
23389
|
+
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
23390
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
23391
|
+
const op = tok.punct;
|
|
23392
|
+
this.pos += 1;
|
|
23393
|
+
const right = this.parseBinary(prec + 1);
|
|
23394
|
+
this.countNode();
|
|
23395
|
+
if (isLogicalOp(op)) left = {
|
|
23396
|
+
kind: "logical",
|
|
23397
|
+
op,
|
|
23398
|
+
left,
|
|
23399
|
+
right
|
|
23400
|
+
};
|
|
23401
|
+
else if (isBinaryOp(op)) left = {
|
|
23402
|
+
kind: "binary",
|
|
23403
|
+
op,
|
|
23404
|
+
left,
|
|
23405
|
+
right
|
|
23406
|
+
};
|
|
23407
|
+
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
23408
|
+
}
|
|
23409
|
+
return left;
|
|
23410
|
+
}
|
|
23411
|
+
parseUnary() {
|
|
23412
|
+
const tok = this.peek();
|
|
23413
|
+
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
23414
|
+
const op = tok.punct;
|
|
23415
|
+
this.pos += 1;
|
|
23416
|
+
const operand = this.parseUnary();
|
|
23417
|
+
this.countNode();
|
|
23418
|
+
return {
|
|
23419
|
+
kind: "unary",
|
|
23420
|
+
op,
|
|
23421
|
+
operand
|
|
23422
|
+
};
|
|
23423
|
+
}
|
|
23424
|
+
return this.parsePrimary();
|
|
23425
|
+
}
|
|
23426
|
+
parsePrimary() {
|
|
23427
|
+
const tok = this.next();
|
|
23428
|
+
switch (tok.type) {
|
|
23429
|
+
case "number":
|
|
23430
|
+
this.countNode();
|
|
23431
|
+
return {
|
|
23432
|
+
kind: "literal",
|
|
23433
|
+
value: tok.value
|
|
23434
|
+
};
|
|
23435
|
+
case "string":
|
|
23436
|
+
this.countNode();
|
|
23437
|
+
return {
|
|
23438
|
+
kind: "literal",
|
|
23439
|
+
value: tok.value
|
|
23440
|
+
};
|
|
23441
|
+
case "keyword":
|
|
23442
|
+
this.countNode();
|
|
23443
|
+
return {
|
|
23444
|
+
kind: "literal",
|
|
23445
|
+
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
23446
|
+
};
|
|
23447
|
+
case "identifier": {
|
|
23448
|
+
const nextTok = this.peek();
|
|
23449
|
+
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
23450
|
+
this.identifiers.add(tok.name);
|
|
23451
|
+
this.countNode();
|
|
23452
|
+
return {
|
|
23453
|
+
kind: "identifier",
|
|
23454
|
+
name: tok.name
|
|
23455
|
+
};
|
|
23456
|
+
}
|
|
23457
|
+
case "punct":
|
|
23458
|
+
if (tok.punct === "(") {
|
|
23459
|
+
const inner = this.parseTernary();
|
|
23460
|
+
this.expectPunct(")");
|
|
23461
|
+
return inner;
|
|
23462
|
+
}
|
|
23463
|
+
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
23464
|
+
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
23465
|
+
}
|
|
23466
|
+
}
|
|
23467
|
+
parseCall(callee, pos) {
|
|
23468
|
+
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
23469
|
+
this.expectPunct("(");
|
|
23470
|
+
const args = [];
|
|
23471
|
+
if (!this.matchPunct(")")) for (;;) {
|
|
23472
|
+
args.push(this.parseTernary());
|
|
23473
|
+
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
23474
|
+
if (this.matchPunct(",")) continue;
|
|
23475
|
+
this.expectPunct(")");
|
|
23476
|
+
break;
|
|
23477
|
+
}
|
|
23478
|
+
this.callees.add(callee);
|
|
23479
|
+
this.countNode();
|
|
23480
|
+
return {
|
|
23481
|
+
kind: "call",
|
|
23482
|
+
callee,
|
|
23483
|
+
args
|
|
23484
|
+
};
|
|
23485
|
+
}
|
|
23486
|
+
};
|
|
23487
|
+
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
23488
|
+
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
23489
|
+
function parseExpression(source) {
|
|
23490
|
+
return new Parser(tokenize(source)).parse();
|
|
23491
|
+
}
|
|
23421
23492
|
/**
|
|
23422
|
-
*
|
|
23423
|
-
*
|
|
23424
|
-
*
|
|
23425
|
-
*
|
|
23493
|
+
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
23494
|
+
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
23495
|
+
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
23496
|
+
* one per read on a hot resolve path.
|
|
23497
|
+
*
|
|
23498
|
+
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
23499
|
+
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
23500
|
+
* callers is safe and maximises hit rate.
|
|
23426
23501
|
*/
|
|
23427
|
-
var
|
|
23428
|
-
|
|
23429
|
-
|
|
23430
|
-
|
|
23431
|
-
|
|
23432
|
-
|
|
23433
|
-
|
|
23434
|
-
|
|
23435
|
-
|
|
23436
|
-
|
|
23437
|
-
|
|
23438
|
-
|
|
23439
|
-
|
|
23440
|
-
|
|
23441
|
-
|
|
23442
|
-
|
|
23443
|
-
|
|
23444
|
-
|
|
23445
|
-
|
|
23446
|
-
|
|
23447
|
-
|
|
23448
|
-
|
|
23449
|
-
|
|
23450
|
-
|
|
23451
|
-
|
|
23452
|
-
|
|
23453
|
-
|
|
23454
|
-
|
|
23455
|
-
|
|
23502
|
+
var cache = /* @__PURE__ */ new Map();
|
|
23503
|
+
function getCached(source) {
|
|
23504
|
+
const hit = cache.get(source);
|
|
23505
|
+
if (hit !== void 0) {
|
|
23506
|
+
cache.delete(source);
|
|
23507
|
+
cache.set(source, hit);
|
|
23508
|
+
return hit;
|
|
23509
|
+
}
|
|
23510
|
+
let result;
|
|
23511
|
+
try {
|
|
23512
|
+
result = {
|
|
23513
|
+
ok: true,
|
|
23514
|
+
parsed: parseExpression(source)
|
|
23515
|
+
};
|
|
23516
|
+
} catch (err) {
|
|
23517
|
+
result = {
|
|
23518
|
+
ok: false,
|
|
23519
|
+
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
23520
|
+
};
|
|
23521
|
+
}
|
|
23522
|
+
cache.set(source, result);
|
|
23523
|
+
if (cache.size > 256) {
|
|
23524
|
+
const oldest = cache.keys().next().value;
|
|
23525
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
23526
|
+
}
|
|
23527
|
+
return result;
|
|
23528
|
+
}
|
|
23529
|
+
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
23530
|
+
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
23531
|
+
function compileExpressionSafe(source) {
|
|
23532
|
+
return getCached(source);
|
|
23533
|
+
}
|
|
23534
|
+
Object.freeze({});
|
|
23456
23535
|
/**
|
|
23457
|
-
*
|
|
23458
|
-
*
|
|
23459
|
-
*
|
|
23460
|
-
* `
|
|
23461
|
-
*
|
|
23462
|
-
* Extend the enum here when a new gating consumer comes online (audio
|
|
23463
|
-
* gating, alert filtering, …) — no other surface needs to change.
|
|
23536
|
+
* Author-time validation. Returns `null` when the source is valid, else a
|
|
23537
|
+
* human-readable error message. Checks: the expression compiles; binding count
|
|
23538
|
+
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
23539
|
+
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
23540
|
+
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
23464
23541
|
*/
|
|
23465
|
-
|
|
23466
|
-
|
|
23467
|
-
|
|
23468
|
-
|
|
23542
|
+
function validateExpressionSource(src) {
|
|
23543
|
+
const names = Object.keys(src.bindings);
|
|
23544
|
+
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
23545
|
+
for (const name of names) {
|
|
23546
|
+
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
23547
|
+
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
23548
|
+
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
23549
|
+
}
|
|
23550
|
+
const compiled = compileExpressionSafe(src.expr);
|
|
23551
|
+
if (!compiled.ok) return compiled.error;
|
|
23552
|
+
const bound = new Set(names);
|
|
23553
|
+
for (const id of compiled.parsed.identifiers) {
|
|
23554
|
+
if (id === "now") continue;
|
|
23555
|
+
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
23556
|
+
}
|
|
23557
|
+
return null;
|
|
23558
|
+
}
|
|
23559
|
+
var ExpressionBindingSourceSchema = union([
|
|
23560
|
+
object({
|
|
23561
|
+
kind: literal("field").optional(),
|
|
23562
|
+
sourceKey: string(),
|
|
23563
|
+
cap: string(),
|
|
23564
|
+
fieldPath: string()
|
|
23565
|
+
}),
|
|
23566
|
+
object({
|
|
23567
|
+
kind: literal("literal"),
|
|
23568
|
+
value: union([
|
|
23569
|
+
string(),
|
|
23570
|
+
number(),
|
|
23571
|
+
boolean(),
|
|
23572
|
+
_null()
|
|
23573
|
+
])
|
|
23574
|
+
}),
|
|
23575
|
+
object({
|
|
23576
|
+
kind: literal("global"),
|
|
23577
|
+
sourceStableId: string(),
|
|
23578
|
+
cap: string(),
|
|
23579
|
+
fieldPath: string()
|
|
23580
|
+
})
|
|
23469
23581
|
]);
|
|
23470
|
-
|
|
23471
|
-
|
|
23472
|
-
|
|
23473
|
-
})
|
|
23474
|
-
|
|
23475
|
-
|
|
23476
|
-
|
|
23477
|
-
|
|
23478
|
-
|
|
23479
|
-
|
|
23480
|
-
}), object({
|
|
23481
|
-
motion: array(ZoneRuleSchema).readonly(),
|
|
23482
|
-
detection: array(ZoneRuleSchema).readonly(),
|
|
23483
|
-
package: array(ZoneRuleSchema).readonly()
|
|
23582
|
+
object({
|
|
23583
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
23584
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
23585
|
+
}).superRefine((src, ctx) => {
|
|
23586
|
+
const err = validateExpressionSource(src);
|
|
23587
|
+
if (err !== null) ctx.addIssue({
|
|
23588
|
+
code: "custom",
|
|
23589
|
+
message: err,
|
|
23590
|
+
path: ["expr"]
|
|
23591
|
+
});
|
|
23484
23592
|
});
|
|
23485
|
-
/**
|
|
23486
|
-
* Accessory device helpers — shared across drivers.
|
|
23487
|
-
*
|
|
23488
|
-
* Many vendor-specific drivers register accessory child devices on
|
|
23489
|
-
* top of a parent (Reolink: siren / floodlight / PIR / autotrack /
|
|
23490
|
-
* chime; ONVIF: relay outputs; future: Tapo Hub child devices). Each
|
|
23491
|
-
* driver picks the right `DeviceType` + `DeviceRole` explicitly when
|
|
23492
|
-
* spawning, builds a name derived from the parent, and produces a
|
|
23493
|
-
* stableId tied to the parent so boot-restore can reconstruct the
|
|
23494
|
-
* relationship.
|
|
23495
|
-
*
|
|
23496
|
-
* Centralised `(kind → DeviceType)` mapping was dropped on purpose:
|
|
23497
|
-
* drivers may reasonably disagree on the right type for an accessory
|
|
23498
|
-
* (a Reolink PIR exposes a switch on/off + sensitivity, while a hypothetical
|
|
23499
|
-
* read-only motion-only sensor might be `DeviceType.Sensor`). Forcing
|
|
23500
|
-
* one canonical mapping was over-prescriptive and added a layer of
|
|
23501
|
-
* indirection without saving meaningful code at call sites — the
|
|
23502
|
-
* driver knows its own hardware best.
|
|
23503
|
-
*/
|
|
23504
|
-
/**
|
|
23505
|
-
* Subset of `DeviceRole` values that drivers register as child
|
|
23506
|
-
* accessories of a parent device. Sourced verbatim from `DeviceRole`
|
|
23507
|
-
* — `AccessoryKind` is the alias drivers use when building accessory
|
|
23508
|
-
* children, so the call site reads as
|
|
23509
|
-
* `accessoryStableId(parent, AccessoryKind.Siren)` rather than
|
|
23510
|
-
* `accessoryStableId(parent, DeviceRole.Siren)` (which would imply
|
|
23511
|
-
* any role works, including non-accessory ones like Doorbell).
|
|
23512
|
-
*/
|
|
23513
|
-
var AccessoryKind = {
|
|
23514
|
-
Siren: DeviceRole.Siren,
|
|
23515
|
-
Floodlight: DeviceRole.Floodlight,
|
|
23516
|
-
Spotlight: DeviceRole.Spotlight,
|
|
23517
|
-
PirSensor: DeviceRole.PirSensor,
|
|
23518
|
-
Chime: DeviceRole.Chime,
|
|
23519
|
-
Autotrack: DeviceRole.Autotrack,
|
|
23520
|
-
Nightvision: DeviceRole.Nightvision,
|
|
23521
|
-
PrivacyMask: DeviceRole.PrivacyMask
|
|
23522
|
-
};
|
|
23523
|
-
AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
|
|
23524
|
-
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;
|
|
23525
|
-
new Set(Object.values(DeviceType));
|
|
23526
|
-
DeviceFeature.BatteryOperated;
|
|
23527
23593
|
Object.freeze({
|
|
23528
23594
|
"accessories.setChildHidden": {
|
|
23529
23595
|
capName: "accessories",
|
|
@@ -24347,6 +24413,12 @@ Object.freeze({
|
|
|
24347
24413
|
addonId: null,
|
|
24348
24414
|
access: "view"
|
|
24349
24415
|
},
|
|
24416
|
+
"coreBlocks.restart": {
|
|
24417
|
+
capName: "core-blocks",
|
|
24418
|
+
capScope: "system",
|
|
24419
|
+
addonId: null,
|
|
24420
|
+
access: "create"
|
|
24421
|
+
},
|
|
24350
24422
|
"coreBlocks.setEnabled": {
|
|
24351
24423
|
capName: "core-blocks",
|
|
24352
24424
|
capScope: "system",
|
|
@@ -24983,12 +25055,6 @@ Object.freeze({
|
|
|
24983
25055
|
addonId: null,
|
|
24984
25056
|
access: "create"
|
|
24985
25057
|
},
|
|
24986
|
-
"deviceManager.setDeviceLinks": {
|
|
24987
|
-
capName: "device-manager",
|
|
24988
|
-
capScope: "system",
|
|
24989
|
-
addonId: null,
|
|
24990
|
-
access: "create"
|
|
24991
|
-
},
|
|
24992
25058
|
"deviceManager.setDisabled": {
|
|
24993
25059
|
capName: "device-manager",
|
|
24994
25060
|
capScope: "system",
|
|
@@ -28133,6 +28199,12 @@ Object.freeze({
|
|
|
28133
28199
|
addonId: null,
|
|
28134
28200
|
access: "create"
|
|
28135
28201
|
},
|
|
28202
|
+
"streamBroker.fetchEventMedia": {
|
|
28203
|
+
capName: "stream-broker",
|
|
28204
|
+
capScope: "system",
|
|
28205
|
+
addonId: null,
|
|
28206
|
+
access: "create"
|
|
28207
|
+
},
|
|
28136
28208
|
"streamBroker.getAllRtspEntries": {
|
|
28137
28209
|
capName: "stream-broker",
|
|
28138
28210
|
capScope: "system",
|
|
@@ -28217,6 +28289,12 @@ Object.freeze({
|
|
|
28217
28289
|
addonId: null,
|
|
28218
28290
|
access: "create"
|
|
28219
28291
|
},
|
|
28292
|
+
"streamBroker.produceEventMedia": {
|
|
28293
|
+
capName: "stream-broker",
|
|
28294
|
+
capScope: "system",
|
|
28295
|
+
addonId: null,
|
|
28296
|
+
access: "create"
|
|
28297
|
+
},
|
|
28220
28298
|
"streamBroker.publishCameraStream": {
|
|
28221
28299
|
capName: "stream-broker",
|
|
28222
28300
|
capScope: "system",
|
|
@@ -29138,7 +29216,7 @@ var AdminUIAddon = class extends BaseAddon {
|
|
|
29138
29216
|
capability: adminUiCapability,
|
|
29139
29217
|
provider: {
|
|
29140
29218
|
getStaticDir: async () => ({ staticDir: resolveAdminUiDistDir() }),
|
|
29141
|
-
getVersion: async () => ({ version: "1.2.
|
|
29219
|
+
getVersion: async () => ({ version: "1.2.37" })
|
|
29142
29220
|
}
|
|
29143
29221
|
}];
|
|
29144
29222
|
}
|