@camstack/addon-provider-amcrest 0.2.10 → 0.2.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/addon.js +2248 -1325
- package/dist/addon.mjs +2248 -1325
- package/package.json +1 -1
package/dist/addon.js
CHANGED
|
@@ -6466,7 +6466,20 @@ var BrokerStatsSchema = object({
|
|
|
6466
6466
|
sampleRate: number(),
|
|
6467
6467
|
channels: number(),
|
|
6468
6468
|
supported: boolean()
|
|
6469
|
-
}).nullable().optional()
|
|
6469
|
+
}).nullable().optional(),
|
|
6470
|
+
/**
|
|
6471
|
+
* BROKER-SIDE AUDIO MUTE (D83). `true` = this broker is deliberately
|
|
6472
|
+
* distributing none of the device's audio, on live or recording.
|
|
6473
|
+
*
|
|
6474
|
+
* Present so a silent camera can be told apart from a broken one on the
|
|
6475
|
+
* stream panel itself, without cross-referencing the switch group: a
|
|
6476
|
+
* broker holding an `audio` track descriptor while `audioMuted` is true is
|
|
6477
|
+
* working exactly as asked. `audioMutedDropped` counts the audio units
|
|
6478
|
+
* thrown away since the current dial — it is how you confirm from stats
|
|
6479
|
+
* alone that the mute is on the packet path and not merely persisted.
|
|
6480
|
+
*/
|
|
6481
|
+
audioMuted: boolean().optional(),
|
|
6482
|
+
audioMutedDropped: number().optional()
|
|
6470
6483
|
});
|
|
6471
6484
|
/**
|
|
6472
6485
|
* Exporter-facing "profile restream" entry. Returned by
|
|
@@ -7099,6 +7112,19 @@ object({
|
|
|
7099
7112
|
* | `notifications` | `notificationRules.setDeviceMuted` | `NotificationCenter.evaluateAndEnqueue` returns before any rule is evaluated |
|
|
7100
7113
|
* | `privacy-mask` | `privacyMask.setMask({ enabled })` → the CAMERA | the camera blanks the masked regions itself; every stream and recording carries the black boxes |
|
|
7101
7114
|
* | `device-audio` | `privacyMask.setAudioEnabled` → the CAMERA | the camera stops encoding an audio track at all; every consumer sees silent video |
|
|
7115
|
+
* | `broker-audio` | `streamBroker.setDeviceAudioMute` → `DeviceOverride.audioMuted` | `StreamBroker.setAudioMuted` drops the audio plane at the source: no `type:'audio'` packet leaves `fanOutEncoded`, no RTP reaches the restreamer, and the restreamer serves the video-only SDP |
|
|
7116
|
+
*
|
|
7117
|
+
* ## `device-audio` and `broker-audio` are two functions, not two knobs
|
|
7118
|
+
*
|
|
7119
|
+
* They look adjacent and they are not the same control ([D83](../../../../docs/decisions/adr-0083.md)):
|
|
7120
|
+
* `device-audio` writes the CAMERA, so it is hardware privacy — the microphone
|
|
7121
|
+
* genuinely stops, it survives CamStack entirely, and it costs a multi-second
|
|
7122
|
+
* encoder restart on every flip. `broker-audio` writes THIS server, so it is
|
|
7123
|
+
* instant, vendor-independent and reversible without touching the camera, and
|
|
7124
|
+
* a camera that ignores or lacks the ISAPI/Reolink control is still silenced.
|
|
7125
|
+
* D62 forbids a second switch that *disagrees* with the first; these two
|
|
7126
|
+
* cannot disagree, because neither reads the other's store — the camera holds
|
|
7127
|
+
* one, the broker holds the other, and each reports its own fact.
|
|
7102
7128
|
*
|
|
7103
7129
|
* ## The two switches whose authority is not on this server
|
|
7104
7130
|
*
|
|
@@ -7160,6 +7186,7 @@ var CameraSwitchIdSchema = _enum([
|
|
|
7160
7186
|
"object-detection",
|
|
7161
7187
|
"privacy-mask",
|
|
7162
7188
|
"device-audio",
|
|
7189
|
+
"broker-audio",
|
|
7163
7190
|
"audio-analysis",
|
|
7164
7191
|
"recording",
|
|
7165
7192
|
"notifications"
|
|
@@ -7185,7 +7212,8 @@ var CameraSwitchAuthoritySchema = discriminatedUnion("kind", [
|
|
|
7185
7212
|
object({
|
|
7186
7213
|
kind: literal("camera-mask"),
|
|
7187
7214
|
capName: string()
|
|
7188
|
-
})
|
|
7215
|
+
}),
|
|
7216
|
+
object({ kind: literal("broker-audio-mute") })
|
|
7189
7217
|
]);
|
|
7190
7218
|
/**
|
|
7191
7219
|
* Why a switch is not offered for this camera. Rendered instead of the
|
|
@@ -8911,6 +8939,69 @@ var StreamFormatSchema = _enum([
|
|
|
8911
8939
|
"mjpeg",
|
|
8912
8940
|
"rtsp"
|
|
8913
8941
|
]);
|
|
8942
|
+
/** A container `produceEventMedia` can emit. */
|
|
8943
|
+
var EventMediaKindSchema = _enum(["mp4", "gif"]);
|
|
8944
|
+
/**
|
|
8945
|
+
* One produced artifact, referenced by HANDLE.
|
|
8946
|
+
*
|
|
8947
|
+
* Never inline bytes: a produced clip is 200 KB–5 MB and every consumer of this
|
|
8948
|
+
* method is in another runner ([D9](../../../../docs/decisions/adr-0009.md),
|
|
8949
|
+
* [D18](../../../../docs/decisions/adr-0018.md) — cross-process media is fetched
|
|
8950
|
+
* on demand, compressed, by handle). `bytes` is here so a caller can decide
|
|
8951
|
+
* whether it wants the fetch at all.
|
|
8952
|
+
*/
|
|
8953
|
+
var EventMediaArtifactSchema = object({
|
|
8954
|
+
kind: EventMediaKindSchema,
|
|
8955
|
+
/** Opaque, single-camera, short-lived. Redeem with `fetchEventMedia`. */
|
|
8956
|
+
handle: string(),
|
|
8957
|
+
/**
|
|
8958
|
+
* The node holding the bytes — the ROUTING key for `fetchEventMedia`.
|
|
8959
|
+
*
|
|
8960
|
+
* `stream-broker` is a singleton cap and an unpinned call never leaves the
|
|
8961
|
+
* hub, so a handle produced on an agent's broker would be redeemed against
|
|
8962
|
+
* the hub's store and come back `null`. Same contract, same field name and
|
|
8963
|
+
* the same reason as `FrameHandleSchema.nodeId`: the producer stamps where it
|
|
8964
|
+
* lives and the consumer pins to it.
|
|
8965
|
+
*/
|
|
8966
|
+
nodeId: string(),
|
|
8967
|
+
mime: string(),
|
|
8968
|
+
bytes: number().int(),
|
|
8969
|
+
width: number().int(),
|
|
8970
|
+
height: number().int()
|
|
8971
|
+
});
|
|
8972
|
+
/**
|
|
8973
|
+
* What a production actually covered — the answer to the only question an
|
|
8974
|
+
* operator asks about a notification clip.
|
|
8975
|
+
*
|
|
8976
|
+
* `fromTs`/`toTs` are WALL CLOCK, derived from the ring's own packet timeline,
|
|
8977
|
+
* so a caller can state "this clip starts 4.1 s before the event" instead of
|
|
8978
|
+
* inferring it from a duration. A production whose `fromTs` is later than the
|
|
8979
|
+
* event is a production with no pre-roll, and that is exactly the defect this
|
|
8980
|
+
* method exists to make visible rather than plausible.
|
|
8981
|
+
*/
|
|
8982
|
+
var EventMediaCoverageSchema = object({
|
|
8983
|
+
fromTs: number(),
|
|
8984
|
+
toTs: number(),
|
|
8985
|
+
/** Encoded packets in the muxed window. */
|
|
8986
|
+
packets: number().int()
|
|
8987
|
+
});
|
|
8988
|
+
/**
|
|
8989
|
+
* The result of ONE cut, in every container the caller asked for.
|
|
8990
|
+
*
|
|
8991
|
+
* Every artifact in `media` came out of the SAME window of the SAME rendition —
|
|
8992
|
+
* that is the whole reason this is one method rather than one call per format.
|
|
8993
|
+
* A consumer attaching a gif and a video can no longer show two different
|
|
8994
|
+
* moments, because it never chose two sources.
|
|
8995
|
+
*/
|
|
8996
|
+
var EventMediaProductionSchema = object({
|
|
8997
|
+
media: array(EventMediaArtifactSchema).readonly(),
|
|
8998
|
+
coverage: EventMediaCoverageSchema,
|
|
8999
|
+
/** The rendition actually cut from — what the default or the fallback chose. */
|
|
9000
|
+
profile: CamProfileSchema,
|
|
9001
|
+
/** `copy` = the camera's own H.264, untouched. `encode` = re-encoded (H.265
|
|
9002
|
+
* source, a downscale, or a playback rate other than 1). */
|
|
9003
|
+
video: _enum(["copy", "encode"])
|
|
9004
|
+
});
|
|
8914
9005
|
var RtspRestreamEntrySchema = object({
|
|
8915
9006
|
brokerId: string(),
|
|
8916
9007
|
url: string(),
|
|
@@ -9310,6 +9401,56 @@ method(object({
|
|
|
9310
9401
|
}), {
|
|
9311
9402
|
kind: "mutation",
|
|
9312
9403
|
auth: "admin"
|
|
9404
|
+
}), method(object({
|
|
9405
|
+
deviceId: number(),
|
|
9406
|
+
/** Absent = the largest H.264 rendition at or below 1080p, which is
|
|
9407
|
+
* also the one that can be copied. Falls back to whatever the ring
|
|
9408
|
+
* actually retained, and the answer says which. */
|
|
9409
|
+
profile: CamProfileSchema.optional(),
|
|
9410
|
+
aroundMs: number(),
|
|
9411
|
+
preSeconds: number().min(0).max(20).default(4),
|
|
9412
|
+
postSeconds: number().min(0).max(20).default(6),
|
|
9413
|
+
kinds: array(EventMediaKindSchema).min(1).default(["mp4"]),
|
|
9414
|
+
/** GIF geometry. The video keeps the source's own. */
|
|
9415
|
+
gifMaxWidth: number().int().min(120).max(1280).default(640),
|
|
9416
|
+
/**
|
|
9417
|
+
* The gif's own PLAYBACK rate in frames per second — what the finished
|
|
9418
|
+
* gif runs at, not how many source frames feed it. The decimation that
|
|
9419
|
+
* feeds it samples `gifFps / gifSpeed` source frames per second, so at
|
|
9420
|
+
* the defaults a 12 fps gif is built out of 3 source frames a second.
|
|
9421
|
+
*/
|
|
9422
|
+
gifFps: number().int().min(1).max(15).default(12),
|
|
9423
|
+
/**
|
|
9424
|
+
* How fast the GIF plays against real time, independent of `speed`.
|
|
9425
|
+
*
|
|
9426
|
+
* 4× by default, by operator request: a notification gif is glanced at
|
|
9427
|
+
* on a lock screen, so a ~12 s window has to be over in ~3 s. It stays
|
|
9428
|
+
* a separate knob from `speed` even though both now default to 4 —
|
|
9429
|
+
* a caller wanting a real-time video and a fast gif must not have to
|
|
9430
|
+
* choose.
|
|
9431
|
+
*/
|
|
9432
|
+
gifSpeed: number().min(1).max(8).default(4),
|
|
9433
|
+
/**
|
|
9434
|
+
* Playback rate of the VIDEO. Also 4× by default, by operator decision.
|
|
9435
|
+
*
|
|
9436
|
+
* `1` is real time and is the ONLY value that allows the copy branch —
|
|
9437
|
+
* anything else forces `libx264` over the window. That was priced
|
|
9438
|
+
* before it was chosen: a per-event burst measured at 0.23 s and 254 KB
|
|
9439
|
+
* on a real 615 720p cut, against 922 KB for the copy it replaces. A
|
|
9440
|
+
* re-encode is capped at 720p (`EVENT_CLIP_ENCODE_MAX_WIDTH`), because
|
|
9441
|
+
* once the decode is forced the width stops being free.
|
|
9442
|
+
*/
|
|
9443
|
+
speed: number().min(1).max(8).default(4)
|
|
9444
|
+
}), EventMediaProductionSchema, {
|
|
9445
|
+
kind: "mutation",
|
|
9446
|
+
auth: "admin"
|
|
9447
|
+
}), method(object({ handle: string() }), object({
|
|
9448
|
+
base64: string(),
|
|
9449
|
+
mime: string(),
|
|
9450
|
+
bytes: number().int()
|
|
9451
|
+
}).nullable(), {
|
|
9452
|
+
kind: "mutation",
|
|
9453
|
+
auth: "admin"
|
|
9313
9454
|
}), method(_void(), array(CameraStreamSchema).readonly()), method(_void(), array(ProfileSlotSchema).readonly()), method(object({ brokerId: string() }), BrokerStatsSchema), method(object({ brokerId: string() }), object({
|
|
9314
9455
|
probed: boolean(),
|
|
9315
9456
|
summary: string()
|
|
@@ -9382,7 +9523,25 @@ method(object({
|
|
|
9382
9523
|
}), _void(), {
|
|
9383
9524
|
kind: "mutation",
|
|
9384
9525
|
auth: "admin"
|
|
9385
|
-
}), method(object({ brokerId: string() }), boolean()), object({
|
|
9526
|
+
}), method(object({ brokerId: string() }), boolean()), method(object({ deviceId: number().int() }), object({
|
|
9527
|
+
muted: boolean(),
|
|
9528
|
+
/**
|
|
9529
|
+
* How many live non-derived brokers currently hold the mute. Purely
|
|
9530
|
+
* diagnostic: `muted` is the policy and is authoritative on its own
|
|
9531
|
+
* (it applies to brokers that do not exist yet), while this says
|
|
9532
|
+
* whether anything is presently being silenced.
|
|
9533
|
+
*/
|
|
9534
|
+
appliedBrokers: number().int().nonnegative()
|
|
9535
|
+
})), method(object({
|
|
9536
|
+
deviceId: number().int(),
|
|
9537
|
+
muted: boolean()
|
|
9538
|
+
}), object({
|
|
9539
|
+
muted: boolean(),
|
|
9540
|
+
appliedBrokers: number().int().nonnegative()
|
|
9541
|
+
}), {
|
|
9542
|
+
kind: "mutation",
|
|
9543
|
+
auth: "admin"
|
|
9544
|
+
}), object({
|
|
9386
9545
|
deviceId: number().int().nonnegative(),
|
|
9387
9546
|
camStreamId: string(),
|
|
9388
9547
|
profile: CamProfileSchema
|
|
@@ -9579,25 +9738,6 @@ var cameraStreamsCapability = {
|
|
|
9579
9738
|
lastChangedAt: number()
|
|
9580
9739
|
})
|
|
9581
9740
|
};
|
|
9582
|
-
/**
|
|
9583
|
-
* core-blocks — user-authored TypeScript, stored in the kernel and executed in
|
|
9584
|
-
* its own process.
|
|
9585
|
-
*
|
|
9586
|
-
* Spec: `docs/superpowers/specs/2026-08-04-core-blocks-and-synthetic-devices-design.md`.
|
|
9587
|
-
*
|
|
9588
|
-
* The first use is **owning devices without being a device provider**: a block
|
|
9589
|
-
* declares devices under a system or custom integration and drives their state,
|
|
9590
|
-
* with the same `ctx` an addon gets. Automations come later; nothing here
|
|
9591
|
-
* models a trigger.
|
|
9592
|
-
*
|
|
9593
|
-
* **Stated plainly, because it does not change by being true:** a block has an
|
|
9594
|
-
* addon's powers — devices, storage, the event bus, `ctx.api`. It is a plugin
|
|
9595
|
-
* with no review step. What makes that survivable is not a sandbox, it is
|
|
9596
|
-
* PROCESS ISOLATION: one process per block, supervised by `CrashSupervisor`,
|
|
9597
|
-
* so a block that throws or never returns is marked `failed` and visible
|
|
9598
|
-
* instead of taking the hub with it (D6). Every method here is admin-only, and
|
|
9599
|
-
* must stay so.
|
|
9600
|
-
*/
|
|
9601
9741
|
/** Where a block runs. The operator chooses — a block driving a device on an
|
|
9602
9742
|
* agent is the reason placement is not fixed to the hub. */
|
|
9603
9743
|
var CoreBlockPlacementSchema = union([literal("hub"), string().min(1)]);
|
|
@@ -9669,6 +9809,9 @@ method(object({}), object({ blocks: array(CoreBlockSchema) }), { auth: "admin" }
|
|
|
9669
9809
|
}), object({ block: CoreBlockSchema }), {
|
|
9670
9810
|
kind: "mutation",
|
|
9671
9811
|
auth: "admin"
|
|
9812
|
+
}), method(object({ blockId: string() }), object({ block: CoreBlockSchema }), {
|
|
9813
|
+
kind: "mutation",
|
|
9814
|
+
auth: "admin"
|
|
9672
9815
|
}), method(object({ code: string() }), CoreBlockCompileResultSchema, {
|
|
9673
9816
|
kind: "mutation",
|
|
9674
9817
|
auth: "admin"
|
|
@@ -10447,895 +10590,214 @@ var ExposeInputSchema = object({
|
|
|
10447
10590
|
});
|
|
10448
10591
|
var UnexposeInputSchema = object({ deviceId: string() });
|
|
10449
10592
|
method(_void(), DeviceExportStatusSchema), method(_void(), array(DeviceKindSchema)), method(_void(), array(ExposedDeviceSchema)), method(ExposeInputSchema, _void(), { kind: "mutation" }), method(UnexposeInputSchema, _void(), { kind: "mutation" });
|
|
10593
|
+
var ProviderStatusSchema = object({
|
|
10594
|
+
connected: boolean(),
|
|
10595
|
+
deviceCount: number(),
|
|
10596
|
+
error: string().optional()
|
|
10597
|
+
});
|
|
10598
|
+
object({
|
|
10599
|
+
externalId: string(),
|
|
10600
|
+
name: string(),
|
|
10601
|
+
type: string(),
|
|
10602
|
+
metadata: record(string(), unknown()).optional()
|
|
10603
|
+
});
|
|
10450
10604
|
/**
|
|
10451
|
-
*
|
|
10452
|
-
*
|
|
10453
|
-
*
|
|
10454
|
-
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
10455
|
-
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
10456
|
-
* work a single author-supplied expression can request, so a hostile or
|
|
10457
|
-
* accidental pathological string can never spend unbounded CPU/memory.
|
|
10605
|
+
* Candidate handed back from discovery and accepted by
|
|
10606
|
+
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
10607
|
+
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
10458
10608
|
*/
|
|
10459
|
-
|
|
10460
|
-
|
|
10461
|
-
|
|
10462
|
-
|
|
10463
|
-
|
|
10464
|
-
/**
|
|
10465
|
-
*
|
|
10466
|
-
|
|
10467
|
-
|
|
10468
|
-
|
|
10469
|
-
|
|
10470
|
-
|
|
10471
|
-
|
|
10609
|
+
var DiscoveryCandidateSchema = object({
|
|
10610
|
+
stableId: string(),
|
|
10611
|
+
type: _enum(DeviceType),
|
|
10612
|
+
suggestedName: string(),
|
|
10613
|
+
prefilledConfig: record(string(), unknown()),
|
|
10614
|
+
/**
|
|
10615
|
+
* Optional upstream-system identity (HA entity_id, vendor MAC, …).
|
|
10616
|
+
* Discovery pre-populates this for systems that know the upstream
|
|
10617
|
+
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
10618
|
+
* flows live through the cap STATUS SLICE after adoption.
|
|
10619
|
+
*/
|
|
10620
|
+
sourceInfo: SourceInfoSchema.optional()
|
|
10621
|
+
});
|
|
10472
10622
|
/**
|
|
10473
|
-
*
|
|
10474
|
-
*
|
|
10475
|
-
*
|
|
10623
|
+
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
10624
|
+
* Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
|
|
10625
|
+
* tRPC layer can pass it through without reshaping.
|
|
10476
10626
|
*/
|
|
10477
|
-
|
|
10478
|
-
|
|
10479
|
-
|
|
10480
|
-
|
|
10481
|
-
|
|
10482
|
-
|
|
10483
|
-
|
|
10484
|
-
|
|
10485
|
-
|
|
10486
|
-
|
|
10487
|
-
/**
|
|
10488
|
-
*
|
|
10489
|
-
|
|
10490
|
-
|
|
10491
|
-
|
|
10492
|
-
|
|
10627
|
+
var DeviceSummarySchema = object({
|
|
10628
|
+
id: number(),
|
|
10629
|
+
stableId: string(),
|
|
10630
|
+
addonId: string(),
|
|
10631
|
+
type: string(),
|
|
10632
|
+
name: string(),
|
|
10633
|
+
parentDeviceId: number().nullable(),
|
|
10634
|
+
online: boolean(),
|
|
10635
|
+
features: array(string()),
|
|
10636
|
+
config: record(string(), unknown()),
|
|
10637
|
+
/** Optional upstream-system identity (dispatch key + system tag).
|
|
10638
|
+
* See `SourceInfo`. Present when the device has a non-synthetic
|
|
10639
|
+
* source identifier (HA entities, vendor MAC, …); omitted when the
|
|
10640
|
+
* synthetic backfill is in effect. */
|
|
10641
|
+
sourceInfo: SourceInfoSchema.optional()
|
|
10642
|
+
});
|
|
10643
|
+
/**
|
|
10644
|
+
* Result of a live field test (e.g. probing an RTSP URL during device
|
|
10645
|
+
* creation). Matches the UI-side `FieldProbeResult` in
|
|
10646
|
+
* `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
|
|
10647
|
+
* returned `labels` as chips next to the input.
|
|
10648
|
+
*/
|
|
10649
|
+
var FieldProbeResultSchema = object({
|
|
10650
|
+
status: _enum(["ok", "error"]),
|
|
10651
|
+
labels: array(string()).optional(),
|
|
10652
|
+
error: string().optional()
|
|
10653
|
+
});
|
|
10654
|
+
/**
|
|
10655
|
+
* The output of `getChildCreationSchema` is a UI schema tree. We store
|
|
10656
|
+
* it as `unknown` at the capability layer — the router just passes it
|
|
10657
|
+
* through and the admin UI renders it via `FormBuilder`. The actual
|
|
10658
|
+
* type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
|
|
10659
|
+
* but we deliberately avoid a Zod mirror because the union is large and
|
|
10660
|
+
* not meant for runtime validation at this seam.
|
|
10661
|
+
*/
|
|
10662
|
+
var CreationSchemaOutputSchema = unknown();
|
|
10663
|
+
var deviceProviderCapability = {
|
|
10664
|
+
name: "device-provider",
|
|
10665
|
+
scope: "system",
|
|
10666
|
+
mode: "collection",
|
|
10667
|
+
methods: {
|
|
10668
|
+
start: method(_void(), _void(), { kind: "mutation" }),
|
|
10669
|
+
stop: method(_void(), _void(), { kind: "mutation" }),
|
|
10670
|
+
getStatus: method(_void(), ProviderStatusSchema),
|
|
10671
|
+
getDevices: method(_void(), array(object({
|
|
10672
|
+
id: string(),
|
|
10673
|
+
name: string(),
|
|
10674
|
+
type: string()
|
|
10675
|
+
}))),
|
|
10676
|
+
supportsDiscovery: method(object({}), boolean()),
|
|
10677
|
+
/**
|
|
10678
|
+
* Run a network scan. `params` carries optional provider-specific scan
|
|
10679
|
+
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
10680
|
+
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
10681
|
+
* (provider uses its local-network default).
|
|
10682
|
+
*/
|
|
10683
|
+
discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
10684
|
+
kind: "mutation",
|
|
10685
|
+
auth: "admin"
|
|
10686
|
+
}),
|
|
10687
|
+
/**
|
|
10688
|
+
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
10689
|
+
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
10690
|
+
* `null` when the provider takes no extra scan params — the generic
|
|
10691
|
+
* aggregated scan never renders this; the per-integration scan does.
|
|
10692
|
+
*/
|
|
10693
|
+
getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
|
|
10694
|
+
/**
|
|
10695
|
+
* The DeviceType this provider creates via manual add (Camera for
|
|
10696
|
+
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
10697
|
+
* provider does not support manual creation. Lets the Add-Device dialog
|
|
10698
|
+
* pick the right type instead of assuming Camera.
|
|
10699
|
+
*/
|
|
10700
|
+
getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
|
|
10701
|
+
adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
10702
|
+
kind: "mutation",
|
|
10703
|
+
auth: "admin"
|
|
10704
|
+
}),
|
|
10705
|
+
supportsManualCreation: method(object({}), boolean()),
|
|
10706
|
+
/**
|
|
10707
|
+
* Fetch the creation form schema for a given DeviceType. Returns
|
|
10708
|
+
* `null` when the provider does not support manually creating
|
|
10709
|
+
* devices of that type. The output is a `ConfigUISchema` — the
|
|
10710
|
+
* router type-asserts it at the boundary.
|
|
10711
|
+
*/
|
|
10712
|
+
getChildCreationSchema: method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema),
|
|
10713
|
+
createDevice: method(object({
|
|
10714
|
+
type: _enum(DeviceType),
|
|
10715
|
+
config: record(string(), unknown())
|
|
10716
|
+
}), DeviceSummarySchema, {
|
|
10717
|
+
kind: "mutation",
|
|
10718
|
+
auth: "admin"
|
|
10719
|
+
}),
|
|
10720
|
+
/**
|
|
10721
|
+
* Test a single field in the creation form before the device has
|
|
10722
|
+
* been persisted. Typical use: probing an RTSP URL entered by the
|
|
10723
|
+
* user. Providers that don't support field probing return
|
|
10724
|
+
* `{ success: true, message: 'Field test not supported' }`.
|
|
10725
|
+
*
|
|
10726
|
+
* `formValues` is the live snapshot of every field in the form at
|
|
10727
|
+
* the moment the user clicked Test — useful for probes that depend
|
|
10728
|
+
* on multiple fields together (e.g. Reolink autodetect needs host
|
|
10729
|
+
* + credentials + UID + transport mode in a single call). Optional
|
|
10730
|
+
* for backwards compatibility; providers free to ignore it.
|
|
10731
|
+
*/
|
|
10732
|
+
testCreationField: method(object({
|
|
10733
|
+
type: _enum(DeviceType),
|
|
10734
|
+
key: string(),
|
|
10735
|
+
value: unknown(),
|
|
10736
|
+
formValues: record(string(), unknown()).optional()
|
|
10737
|
+
}), FieldProbeResultSchema, {
|
|
10738
|
+
kind: "mutation",
|
|
10739
|
+
auth: "admin"
|
|
10740
|
+
})
|
|
10493
10741
|
}
|
|
10494
10742
|
};
|
|
10495
10743
|
/**
|
|
10496
|
-
*
|
|
10497
|
-
*
|
|
10498
|
-
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
10499
|
-
* own-property check against it.
|
|
10744
|
+
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
10745
|
+
* live registry access, and all management operations into a single tRPC surface.
|
|
10500
10746
|
*
|
|
10501
|
-
*
|
|
10502
|
-
*
|
|
10503
|
-
*
|
|
10504
|
-
*
|
|
10505
|
-
* callable — they are simply "unknown function" at parse time.
|
|
10747
|
+
* Replaces:
|
|
10748
|
+
* - `device-persistence` capability (persistence methods absorbed here)
|
|
10749
|
+
* - `device-management.router.ts` (deleted in Phase 2)
|
|
10750
|
+
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
10506
10751
|
*
|
|
10507
|
-
*
|
|
10508
|
-
*
|
|
10509
|
-
*
|
|
10510
|
-
*
|
|
10752
|
+
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
10753
|
+
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
10754
|
+
* - No nodeId routing needed — this is a pure hub singleton.
|
|
10755
|
+
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
10756
|
+
* - No shadow registry or cross-node aggregation required.
|
|
10757
|
+
*
|
|
10758
|
+
* Forked workers register devices back to the hub via `ctx.devices`
|
|
10759
|
+
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
10511
10760
|
*/
|
|
10512
|
-
|
|
10513
|
-
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
10520
|
-
|
|
10521
|
-
|
|
10522
|
-
|
|
10523
|
-
|
|
10524
|
-
|
|
10525
|
-
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10530
|
-
|
|
10531
|
-
|
|
10532
|
-
|
|
10533
|
-
|
|
10534
|
-
|
|
10535
|
-
|
|
10536
|
-
|
|
10537
|
-
|
|
10538
|
-
|
|
10539
|
-
|
|
10540
|
-
|
|
10541
|
-
|
|
10542
|
-
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
|
|
10546
|
-
|
|
10547
|
-
|
|
10548
|
-
},
|
|
10549
|
-
ceil: {
|
|
10550
|
-
minArgs: 1,
|
|
10551
|
-
maxArgs: 1,
|
|
10552
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
10553
|
-
},
|
|
10554
|
-
sqrt: {
|
|
10555
|
-
minArgs: 1,
|
|
10556
|
-
maxArgs: 1,
|
|
10557
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
10558
|
-
},
|
|
10559
|
-
round: {
|
|
10560
|
-
minArgs: 1,
|
|
10561
|
-
maxArgs: 2,
|
|
10562
|
-
apply: (args) => {
|
|
10563
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
10564
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
10565
|
-
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
10566
|
-
const factor = 10 ** digits;
|
|
10567
|
-
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
10568
|
-
}
|
|
10569
|
-
},
|
|
10570
|
-
pow: {
|
|
10571
|
-
minArgs: 2,
|
|
10572
|
-
maxArgs: 2,
|
|
10573
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
10574
|
-
},
|
|
10575
|
-
clamp: {
|
|
10576
|
-
minArgs: 3,
|
|
10577
|
-
maxArgs: 3,
|
|
10578
|
-
apply: (args) => {
|
|
10579
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
10580
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
10581
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
10582
|
-
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
10583
|
-
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
10584
|
-
}
|
|
10585
|
-
},
|
|
10586
|
-
avg: {
|
|
10587
|
-
minArgs: 1,
|
|
10588
|
-
maxArgs: INF,
|
|
10589
|
-
apply: (args) => {
|
|
10590
|
-
const nums = allFiniteNumbers(args, "avg");
|
|
10591
|
-
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
10592
|
-
}
|
|
10593
|
-
},
|
|
10594
|
-
sum: {
|
|
10595
|
-
minArgs: 1,
|
|
10596
|
-
maxArgs: INF,
|
|
10597
|
-
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
10598
|
-
},
|
|
10599
|
-
coalesce: {
|
|
10600
|
-
minArgs: 1,
|
|
10601
|
-
maxArgs: INF,
|
|
10602
|
-
apply: (args) => {
|
|
10603
|
-
for (const a of args) if (a !== null) return a;
|
|
10604
|
-
return null;
|
|
10605
|
-
}
|
|
10606
|
-
},
|
|
10607
|
-
age: {
|
|
10608
|
-
minArgs: 2,
|
|
10609
|
-
maxArgs: 2,
|
|
10610
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
10611
|
-
},
|
|
10612
|
-
convert: {
|
|
10613
|
-
minArgs: 3,
|
|
10614
|
-
maxArgs: 3,
|
|
10615
|
-
apply: (args, hooks) => {
|
|
10616
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
10617
|
-
const from = asString$1(args[1], "convert", 1).trim();
|
|
10618
|
-
const to = asString$1(args[2], "convert", 2).trim();
|
|
10619
|
-
if (hooks.convert) {
|
|
10620
|
-
const out = hooks.convert(x, from, to);
|
|
10621
|
-
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
10622
|
-
return finiteResult(out, "convert");
|
|
10623
|
-
}
|
|
10624
|
-
if (from === to) return x;
|
|
10625
|
-
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
10626
|
-
}
|
|
10627
|
-
}
|
|
10628
|
-
};
|
|
10629
|
-
Object.freeze(Object.assign(Object.create(null), table));
|
|
10630
|
-
/** The set of valid builtin names — used by the parser to reject unknown
|
|
10631
|
-
* callees at parse time (immediate author feedback). */
|
|
10632
|
-
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
10761
|
+
/** One child-placement directive on a container's `childLayout`. Structurally
|
|
10762
|
+
* identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
|
|
10763
|
+
* shape for the same field. The child is identified by its re-sync-stable
|
|
10764
|
+
* accessory `stableIdSuffix` (`childKey`); listed children are grouped into
|
|
10765
|
+
* named accordion sections (with optional intra-section order). */
|
|
10766
|
+
var ChildLayoutEntrySchema = object({
|
|
10767
|
+
childKey: string(),
|
|
10768
|
+
section: string(),
|
|
10769
|
+
order: number().optional(),
|
|
10770
|
+
collapsed: boolean().optional()
|
|
10771
|
+
});
|
|
10772
|
+
/** Cap-wire shape of a per-cap display refinement — mirrors
|
|
10773
|
+
* `DeviceCapDisplayOverride` in `device-management.ts`. */
|
|
10774
|
+
var DeviceCapDisplayOverrideSchema = object({
|
|
10775
|
+
unit: string().min(1).optional(),
|
|
10776
|
+
precision: number().int().min(0).max(10).optional()
|
|
10777
|
+
});
|
|
10778
|
+
/** Cap-wire shape of an operator-authored per-device display override —
|
|
10779
|
+
* mirrors `DeviceDisplayOverride` in `device-management.ts`. `precision`
|
|
10780
|
+
* bounds mirror `numeric-sensor.cap.ts` (`int 0-10`). */
|
|
10781
|
+
var DeviceDisplayOverrideSchema = object({
|
|
10782
|
+
icon: string().min(1).optional(),
|
|
10783
|
+
label: string().min(1).optional(),
|
|
10784
|
+
unit: string().min(1).optional(),
|
|
10785
|
+
precision: number().int().min(0).max(10).optional(),
|
|
10786
|
+
hidden: boolean().optional(),
|
|
10787
|
+
perCap: record(string(), DeviceCapDisplayOverrideSchema).optional()
|
|
10788
|
+
});
|
|
10789
|
+
/** Cap-wire shape of a per-role display default — mirrors `RoleDisplayDefault`
|
|
10790
|
+
* in `device-management.ts`. Keyed by `DeviceRole` string (role strings cross
|
|
10791
|
+
* the wire as plain strings everywhere else — cf. `DeviceInfoSchema.role`). */
|
|
10792
|
+
var RoleDisplayDefaultSchema = object({
|
|
10793
|
+
unit: string().min(1).optional(),
|
|
10794
|
+
precision: number().int().min(0).max(10).optional(),
|
|
10795
|
+
icon: string().min(1).optional()
|
|
10796
|
+
});
|
|
10633
10797
|
/**
|
|
10634
|
-
*
|
|
10635
|
-
*
|
|
10636
|
-
*
|
|
10637
|
-
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
10638
|
-
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
10639
|
-
* is a parse error with a source position, so member access / assignment /
|
|
10640
|
-
* template literals are lexically impossible.
|
|
10641
|
-
*/
|
|
10642
|
-
var KEYWORDS = new Set([
|
|
10643
|
-
"true",
|
|
10644
|
-
"false",
|
|
10645
|
-
"null"
|
|
10646
|
-
]);
|
|
10647
|
-
function isDigit(ch) {
|
|
10648
|
-
return ch >= "0" && ch <= "9";
|
|
10649
|
-
}
|
|
10650
|
-
function isIdentStart(ch) {
|
|
10651
|
-
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
10652
|
-
}
|
|
10653
|
-
function isIdentPart(ch) {
|
|
10654
|
-
return isIdentStart(ch) || isDigit(ch);
|
|
10655
|
-
}
|
|
10656
|
-
function isWhitespace(ch) {
|
|
10657
|
-
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
10658
|
-
}
|
|
10659
|
-
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
10660
|
-
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
10661
|
-
* string. */
|
|
10662
|
-
function tokenize(source) {
|
|
10663
|
-
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
10664
|
-
const tokens = [];
|
|
10665
|
-
let i = 0;
|
|
10666
|
-
const n = source.length;
|
|
10667
|
-
while (i < n) {
|
|
10668
|
-
const ch = source[i];
|
|
10669
|
-
if (isWhitespace(ch)) {
|
|
10670
|
-
i += 1;
|
|
10671
|
-
continue;
|
|
10672
|
-
}
|
|
10673
|
-
if (isDigit(ch)) {
|
|
10674
|
-
const start = i;
|
|
10675
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
10676
|
-
if (i < n && source[i] === ".") {
|
|
10677
|
-
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
10678
|
-
i += 1;
|
|
10679
|
-
while (i < n && isDigit(source[i])) i += 1;
|
|
10680
|
-
}
|
|
10681
|
-
const text = source.slice(start, i);
|
|
10682
|
-
const value = Number(text);
|
|
10683
|
-
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
10684
|
-
tokens.push({
|
|
10685
|
-
type: "number",
|
|
10686
|
-
value,
|
|
10687
|
-
pos: start
|
|
10688
|
-
});
|
|
10689
|
-
continue;
|
|
10690
|
-
}
|
|
10691
|
-
if (ch === "'" || ch === "\"") {
|
|
10692
|
-
const quote = ch;
|
|
10693
|
-
const start = i;
|
|
10694
|
-
i += 1;
|
|
10695
|
-
let out = "";
|
|
10696
|
-
let closed = false;
|
|
10697
|
-
while (i < n) {
|
|
10698
|
-
const c = source[i];
|
|
10699
|
-
if (c === "\\") {
|
|
10700
|
-
const next = i + 1 < n ? source[i + 1] : "";
|
|
10701
|
-
if (next === "\\" || next === "'" || next === "\"") {
|
|
10702
|
-
out += next;
|
|
10703
|
-
i += 2;
|
|
10704
|
-
continue;
|
|
10705
|
-
}
|
|
10706
|
-
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
10707
|
-
}
|
|
10708
|
-
if (c === quote) {
|
|
10709
|
-
closed = true;
|
|
10710
|
-
i += 1;
|
|
10711
|
-
break;
|
|
10712
|
-
}
|
|
10713
|
-
out += c;
|
|
10714
|
-
i += 1;
|
|
10715
|
-
}
|
|
10716
|
-
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
10717
|
-
tokens.push({
|
|
10718
|
-
type: "string",
|
|
10719
|
-
value: out,
|
|
10720
|
-
pos: start
|
|
10721
|
-
});
|
|
10722
|
-
continue;
|
|
10723
|
-
}
|
|
10724
|
-
if (isIdentStart(ch)) {
|
|
10725
|
-
const start = i;
|
|
10726
|
-
while (i < n && isIdentPart(source[i])) i += 1;
|
|
10727
|
-
const text = source.slice(start, i);
|
|
10728
|
-
if (KEYWORDS.has(text)) tokens.push({
|
|
10729
|
-
type: "keyword",
|
|
10730
|
-
keyword: keywordOf(text),
|
|
10731
|
-
pos: start
|
|
10732
|
-
});
|
|
10733
|
-
else tokens.push({
|
|
10734
|
-
type: "identifier",
|
|
10735
|
-
name: text,
|
|
10736
|
-
pos: start
|
|
10737
|
-
});
|
|
10738
|
-
continue;
|
|
10739
|
-
}
|
|
10740
|
-
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
10741
|
-
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
10742
|
-
tokens.push({
|
|
10743
|
-
type: "punct",
|
|
10744
|
-
punct: two,
|
|
10745
|
-
pos: i
|
|
10746
|
-
});
|
|
10747
|
-
i += 2;
|
|
10748
|
-
continue;
|
|
10749
|
-
}
|
|
10750
|
-
if (isSinglePunct(ch)) {
|
|
10751
|
-
tokens.push({
|
|
10752
|
-
type: "punct",
|
|
10753
|
-
punct: ch,
|
|
10754
|
-
pos: i
|
|
10755
|
-
});
|
|
10756
|
-
i += 1;
|
|
10757
|
-
continue;
|
|
10758
|
-
}
|
|
10759
|
-
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
10760
|
-
}
|
|
10761
|
-
tokens.push({
|
|
10762
|
-
type: "eof",
|
|
10763
|
-
pos: n
|
|
10764
|
-
});
|
|
10765
|
-
return tokens;
|
|
10766
|
-
}
|
|
10767
|
-
function keywordOf(text) {
|
|
10768
|
-
if (text === "true") return "true";
|
|
10769
|
-
if (text === "false") return "false";
|
|
10770
|
-
return "null";
|
|
10771
|
-
}
|
|
10772
|
-
function isSinglePunct(ch) {
|
|
10773
|
-
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
10774
|
-
}
|
|
10775
|
-
/**
|
|
10776
|
-
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
10777
|
-
*
|
|
10778
|
-
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
10779
|
-
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
10780
|
-
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
10781
|
-
* string validated against the builtin table at parse time, so an unknown
|
|
10782
|
-
* function is rejected immediately (author feedback) and a persisted expression
|
|
10783
|
-
* that references a since-removed builtin degrades at read.
|
|
10784
|
-
*
|
|
10785
|
-
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
10786
|
-
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
10787
|
-
*/
|
|
10788
|
-
/** Binary/logical operator precedence (higher binds tighter). */
|
|
10789
|
-
var BINARY_PRECEDENCE = {
|
|
10790
|
-
"||": 1,
|
|
10791
|
-
"&&": 2,
|
|
10792
|
-
"==": 3,
|
|
10793
|
-
"!=": 3,
|
|
10794
|
-
"<": 4,
|
|
10795
|
-
"<=": 4,
|
|
10796
|
-
">": 4,
|
|
10797
|
-
">=": 4,
|
|
10798
|
-
"+": 5,
|
|
10799
|
-
"-": 5,
|
|
10800
|
-
"*": 6,
|
|
10801
|
-
"/": 6,
|
|
10802
|
-
"%": 6
|
|
10803
|
-
};
|
|
10804
|
-
function isLogicalOp(op) {
|
|
10805
|
-
return op === "&&" || op === "||";
|
|
10806
|
-
}
|
|
10807
|
-
function isBinaryOp(op) {
|
|
10808
|
-
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
10809
|
-
}
|
|
10810
|
-
var Parser = class {
|
|
10811
|
-
tokens;
|
|
10812
|
-
pos = 0;
|
|
10813
|
-
nodeCount = 0;
|
|
10814
|
-
identifiers = /* @__PURE__ */ new Set();
|
|
10815
|
-
callees = /* @__PURE__ */ new Set();
|
|
10816
|
-
constructor(tokens) {
|
|
10817
|
-
this.tokens = tokens;
|
|
10818
|
-
}
|
|
10819
|
-
parse() {
|
|
10820
|
-
const ast = this.parseTernary();
|
|
10821
|
-
const tok = this.peek();
|
|
10822
|
-
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
10823
|
-
return {
|
|
10824
|
-
ast,
|
|
10825
|
-
identifiers: this.identifiers,
|
|
10826
|
-
callees: this.callees,
|
|
10827
|
-
nodeCount: this.nodeCount
|
|
10828
|
-
};
|
|
10829
|
-
}
|
|
10830
|
-
peek() {
|
|
10831
|
-
return this.tokens[this.pos];
|
|
10832
|
-
}
|
|
10833
|
-
next() {
|
|
10834
|
-
return this.tokens[this.pos++];
|
|
10835
|
-
}
|
|
10836
|
-
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
10837
|
-
expectPunct(punct) {
|
|
10838
|
-
const tok = this.peek();
|
|
10839
|
-
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
10840
|
-
this.pos += 1;
|
|
10841
|
-
}
|
|
10842
|
-
matchPunct(punct) {
|
|
10843
|
-
const tok = this.peek();
|
|
10844
|
-
if (tok.type === "punct" && tok.punct === punct) {
|
|
10845
|
-
this.pos += 1;
|
|
10846
|
-
return true;
|
|
10847
|
-
}
|
|
10848
|
-
return false;
|
|
10849
|
-
}
|
|
10850
|
-
countNode() {
|
|
10851
|
-
this.nodeCount += 1;
|
|
10852
|
-
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
10853
|
-
}
|
|
10854
|
-
parseTernary() {
|
|
10855
|
-
const test = this.parseBinary(1);
|
|
10856
|
-
if (this.matchPunct("?")) {
|
|
10857
|
-
const consequent = this.parseTernary();
|
|
10858
|
-
this.expectPunct(":");
|
|
10859
|
-
const alternate = this.parseTernary();
|
|
10860
|
-
this.countNode();
|
|
10861
|
-
return {
|
|
10862
|
-
kind: "conditional",
|
|
10863
|
-
test,
|
|
10864
|
-
consequent,
|
|
10865
|
-
alternate
|
|
10866
|
-
};
|
|
10867
|
-
}
|
|
10868
|
-
return test;
|
|
10869
|
-
}
|
|
10870
|
-
parseBinary(minPrec) {
|
|
10871
|
-
let left = this.parseUnary();
|
|
10872
|
-
for (;;) {
|
|
10873
|
-
const tok = this.peek();
|
|
10874
|
-
if (tok.type !== "punct") break;
|
|
10875
|
-
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
10876
|
-
if (prec === void 0 || prec < minPrec) break;
|
|
10877
|
-
const op = tok.punct;
|
|
10878
|
-
this.pos += 1;
|
|
10879
|
-
const right = this.parseBinary(prec + 1);
|
|
10880
|
-
this.countNode();
|
|
10881
|
-
if (isLogicalOp(op)) left = {
|
|
10882
|
-
kind: "logical",
|
|
10883
|
-
op,
|
|
10884
|
-
left,
|
|
10885
|
-
right
|
|
10886
|
-
};
|
|
10887
|
-
else if (isBinaryOp(op)) left = {
|
|
10888
|
-
kind: "binary",
|
|
10889
|
-
op,
|
|
10890
|
-
left,
|
|
10891
|
-
right
|
|
10892
|
-
};
|
|
10893
|
-
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
10894
|
-
}
|
|
10895
|
-
return left;
|
|
10896
|
-
}
|
|
10897
|
-
parseUnary() {
|
|
10898
|
-
const tok = this.peek();
|
|
10899
|
-
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
10900
|
-
const op = tok.punct;
|
|
10901
|
-
this.pos += 1;
|
|
10902
|
-
const operand = this.parseUnary();
|
|
10903
|
-
this.countNode();
|
|
10904
|
-
return {
|
|
10905
|
-
kind: "unary",
|
|
10906
|
-
op,
|
|
10907
|
-
operand
|
|
10908
|
-
};
|
|
10909
|
-
}
|
|
10910
|
-
return this.parsePrimary();
|
|
10911
|
-
}
|
|
10912
|
-
parsePrimary() {
|
|
10913
|
-
const tok = this.next();
|
|
10914
|
-
switch (tok.type) {
|
|
10915
|
-
case "number":
|
|
10916
|
-
this.countNode();
|
|
10917
|
-
return {
|
|
10918
|
-
kind: "literal",
|
|
10919
|
-
value: tok.value
|
|
10920
|
-
};
|
|
10921
|
-
case "string":
|
|
10922
|
-
this.countNode();
|
|
10923
|
-
return {
|
|
10924
|
-
kind: "literal",
|
|
10925
|
-
value: tok.value
|
|
10926
|
-
};
|
|
10927
|
-
case "keyword":
|
|
10928
|
-
this.countNode();
|
|
10929
|
-
return {
|
|
10930
|
-
kind: "literal",
|
|
10931
|
-
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
10932
|
-
};
|
|
10933
|
-
case "identifier": {
|
|
10934
|
-
const nextTok = this.peek();
|
|
10935
|
-
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
10936
|
-
this.identifiers.add(tok.name);
|
|
10937
|
-
this.countNode();
|
|
10938
|
-
return {
|
|
10939
|
-
kind: "identifier",
|
|
10940
|
-
name: tok.name
|
|
10941
|
-
};
|
|
10942
|
-
}
|
|
10943
|
-
case "punct":
|
|
10944
|
-
if (tok.punct === "(") {
|
|
10945
|
-
const inner = this.parseTernary();
|
|
10946
|
-
this.expectPunct(")");
|
|
10947
|
-
return inner;
|
|
10948
|
-
}
|
|
10949
|
-
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
10950
|
-
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
10951
|
-
}
|
|
10952
|
-
}
|
|
10953
|
-
parseCall(callee, pos) {
|
|
10954
|
-
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
10955
|
-
this.expectPunct("(");
|
|
10956
|
-
const args = [];
|
|
10957
|
-
if (!this.matchPunct(")")) for (;;) {
|
|
10958
|
-
args.push(this.parseTernary());
|
|
10959
|
-
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
10960
|
-
if (this.matchPunct(",")) continue;
|
|
10961
|
-
this.expectPunct(")");
|
|
10962
|
-
break;
|
|
10963
|
-
}
|
|
10964
|
-
this.callees.add(callee);
|
|
10965
|
-
this.countNode();
|
|
10966
|
-
return {
|
|
10967
|
-
kind: "call",
|
|
10968
|
-
callee,
|
|
10969
|
-
args
|
|
10970
|
-
};
|
|
10971
|
-
}
|
|
10972
|
-
};
|
|
10973
|
-
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
10974
|
-
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
10975
|
-
function parseExpression(source) {
|
|
10976
|
-
return new Parser(tokenize(source)).parse();
|
|
10977
|
-
}
|
|
10978
|
-
/**
|
|
10979
|
-
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
10980
|
-
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
10981
|
-
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
10982
|
-
* one per read on a hot resolve path.
|
|
10983
|
-
*
|
|
10984
|
-
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
10985
|
-
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
10986
|
-
* callers is safe and maximises hit rate.
|
|
10987
|
-
*/
|
|
10988
|
-
var cache = /* @__PURE__ */ new Map();
|
|
10989
|
-
function getCached(source) {
|
|
10990
|
-
const hit = cache.get(source);
|
|
10991
|
-
if (hit !== void 0) {
|
|
10992
|
-
cache.delete(source);
|
|
10993
|
-
cache.set(source, hit);
|
|
10994
|
-
return hit;
|
|
10995
|
-
}
|
|
10996
|
-
let result;
|
|
10997
|
-
try {
|
|
10998
|
-
result = {
|
|
10999
|
-
ok: true,
|
|
11000
|
-
parsed: parseExpression(source)
|
|
11001
|
-
};
|
|
11002
|
-
} catch (err) {
|
|
11003
|
-
result = {
|
|
11004
|
-
ok: false,
|
|
11005
|
-
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
11006
|
-
};
|
|
11007
|
-
}
|
|
11008
|
-
cache.set(source, result);
|
|
11009
|
-
if (cache.size > 256) {
|
|
11010
|
-
const oldest = cache.keys().next().value;
|
|
11011
|
-
if (oldest !== void 0) cache.delete(oldest);
|
|
11012
|
-
}
|
|
11013
|
-
return result;
|
|
11014
|
-
}
|
|
11015
|
-
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
11016
|
-
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
11017
|
-
function compileExpressionSafe(source) {
|
|
11018
|
-
return getCached(source);
|
|
11019
|
-
}
|
|
11020
|
-
Object.freeze({});
|
|
11021
|
-
/**
|
|
11022
|
-
* Author-time validation. Returns `null` when the source is valid, else a
|
|
11023
|
-
* human-readable error message. Checks: the expression compiles; binding count
|
|
11024
|
-
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
11025
|
-
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
11026
|
-
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
11027
|
-
*/
|
|
11028
|
-
function validateExpressionSource(src) {
|
|
11029
|
-
const names = Object.keys(src.bindings);
|
|
11030
|
-
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
11031
|
-
for (const name of names) {
|
|
11032
|
-
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
11033
|
-
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
11034
|
-
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
11035
|
-
}
|
|
11036
|
-
const compiled = compileExpressionSafe(src.expr);
|
|
11037
|
-
if (!compiled.ok) return compiled.error;
|
|
11038
|
-
const bound = new Set(names);
|
|
11039
|
-
for (const id of compiled.parsed.identifiers) {
|
|
11040
|
-
if (id === "now") continue;
|
|
11041
|
-
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
11042
|
-
}
|
|
11043
|
-
return null;
|
|
11044
|
-
}
|
|
11045
|
-
var ProviderStatusSchema = object({
|
|
11046
|
-
connected: boolean(),
|
|
11047
|
-
deviceCount: number(),
|
|
11048
|
-
error: string().optional()
|
|
11049
|
-
});
|
|
11050
|
-
object({
|
|
11051
|
-
externalId: string(),
|
|
11052
|
-
name: string(),
|
|
11053
|
-
type: string(),
|
|
11054
|
-
metadata: record(string(), unknown()).optional()
|
|
11055
|
-
});
|
|
11056
|
-
/**
|
|
11057
|
-
* Candidate handed back from discovery and accepted by
|
|
11058
|
-
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
11059
|
-
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
11060
|
-
*/
|
|
11061
|
-
var DiscoveryCandidateSchema = object({
|
|
11062
|
-
stableId: string(),
|
|
11063
|
-
type: _enum(DeviceType),
|
|
11064
|
-
suggestedName: string(),
|
|
11065
|
-
prefilledConfig: record(string(), unknown()),
|
|
11066
|
-
/**
|
|
11067
|
-
* Optional upstream-system identity (HA entity_id, vendor MAC, …).
|
|
11068
|
-
* Discovery pre-populates this for systems that know the upstream
|
|
11069
|
-
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
11070
|
-
* flows live through the cap STATUS SLICE after adoption.
|
|
11071
|
-
*/
|
|
11072
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
11073
|
-
});
|
|
11074
|
-
/**
|
|
11075
|
-
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
11076
|
-
* Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
|
|
11077
|
-
* tRPC layer can pass it through without reshaping.
|
|
11078
|
-
*/
|
|
11079
|
-
var DeviceSummarySchema = object({
|
|
11080
|
-
id: number(),
|
|
11081
|
-
stableId: string(),
|
|
11082
|
-
addonId: string(),
|
|
11083
|
-
type: string(),
|
|
11084
|
-
name: string(),
|
|
11085
|
-
parentDeviceId: number().nullable(),
|
|
11086
|
-
online: boolean(),
|
|
11087
|
-
features: array(string()),
|
|
11088
|
-
config: record(string(), unknown()),
|
|
11089
|
-
/** Optional upstream-system identity (dispatch key + system tag).
|
|
11090
|
-
* See `SourceInfo`. Present when the device has a non-synthetic
|
|
11091
|
-
* source identifier (HA entities, vendor MAC, …); omitted when the
|
|
11092
|
-
* synthetic backfill is in effect. */
|
|
11093
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
11094
|
-
});
|
|
11095
|
-
/**
|
|
11096
|
-
* Result of a live field test (e.g. probing an RTSP URL during device
|
|
11097
|
-
* creation). Matches the UI-side `FieldProbeResult` in
|
|
11098
|
-
* `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
|
|
11099
|
-
* returned `labels` as chips next to the input.
|
|
11100
|
-
*/
|
|
11101
|
-
var FieldProbeResultSchema = object({
|
|
11102
|
-
status: _enum(["ok", "error"]),
|
|
11103
|
-
labels: array(string()).optional(),
|
|
11104
|
-
error: string().optional()
|
|
11105
|
-
});
|
|
11106
|
-
/**
|
|
11107
|
-
* The output of `getChildCreationSchema` is a UI schema tree. We store
|
|
11108
|
-
* it as `unknown` at the capability layer — the router just passes it
|
|
11109
|
-
* through and the admin UI renders it via `FormBuilder`. The actual
|
|
11110
|
-
* type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
|
|
11111
|
-
* but we deliberately avoid a Zod mirror because the union is large and
|
|
11112
|
-
* not meant for runtime validation at this seam.
|
|
11113
|
-
*/
|
|
11114
|
-
var CreationSchemaOutputSchema = unknown();
|
|
11115
|
-
var deviceProviderCapability = {
|
|
11116
|
-
name: "device-provider",
|
|
11117
|
-
scope: "system",
|
|
11118
|
-
mode: "collection",
|
|
11119
|
-
methods: {
|
|
11120
|
-
start: method(_void(), _void(), { kind: "mutation" }),
|
|
11121
|
-
stop: method(_void(), _void(), { kind: "mutation" }),
|
|
11122
|
-
getStatus: method(_void(), ProviderStatusSchema),
|
|
11123
|
-
getDevices: method(_void(), array(object({
|
|
11124
|
-
id: string(),
|
|
11125
|
-
name: string(),
|
|
11126
|
-
type: string()
|
|
11127
|
-
}))),
|
|
11128
|
-
supportsDiscovery: method(object({}), boolean()),
|
|
11129
|
-
/**
|
|
11130
|
-
* Run a network scan. `params` carries optional provider-specific scan
|
|
11131
|
-
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
11132
|
-
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
11133
|
-
* (provider uses its local-network default).
|
|
11134
|
-
*/
|
|
11135
|
-
discoverDevices: method(object({ params: record(string(), unknown()).optional() }), array(DiscoveryCandidateSchema), {
|
|
11136
|
-
kind: "mutation",
|
|
11137
|
-
auth: "admin"
|
|
11138
|
-
}),
|
|
11139
|
-
/**
|
|
11140
|
-
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
11141
|
-
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
11142
|
-
* `null` when the provider takes no extra scan params — the generic
|
|
11143
|
-
* aggregated scan never renders this; the per-integration scan does.
|
|
11144
|
-
*/
|
|
11145
|
-
getDiscoveryParamsSchema: method(object({}), CreationSchemaOutputSchema),
|
|
11146
|
-
/**
|
|
11147
|
-
* The DeviceType this provider creates via manual add (Camera for
|
|
11148
|
-
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
11149
|
-
* provider does not support manual creation. Lets the Add-Device dialog
|
|
11150
|
-
* pick the right type instead of assuming Camera.
|
|
11151
|
-
*/
|
|
11152
|
-
getManualCreationType: method(object({}), object({ deviceType: _enum(DeviceType).nullable() })),
|
|
11153
|
-
adoptDiscoveredDevice: method(object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
11154
|
-
kind: "mutation",
|
|
11155
|
-
auth: "admin"
|
|
11156
|
-
}),
|
|
11157
|
-
supportsManualCreation: method(object({}), boolean()),
|
|
11158
|
-
/**
|
|
11159
|
-
* Fetch the creation form schema for a given DeviceType. Returns
|
|
11160
|
-
* `null` when the provider does not support manually creating
|
|
11161
|
-
* devices of that type. The output is a `ConfigUISchema` — the
|
|
11162
|
-
* router type-asserts it at the boundary.
|
|
11163
|
-
*/
|
|
11164
|
-
getChildCreationSchema: method(object({ type: _enum(DeviceType) }), CreationSchemaOutputSchema),
|
|
11165
|
-
createDevice: method(object({
|
|
11166
|
-
type: _enum(DeviceType),
|
|
11167
|
-
config: record(string(), unknown())
|
|
11168
|
-
}), DeviceSummarySchema, {
|
|
11169
|
-
kind: "mutation",
|
|
11170
|
-
auth: "admin"
|
|
11171
|
-
}),
|
|
11172
|
-
/**
|
|
11173
|
-
* Test a single field in the creation form before the device has
|
|
11174
|
-
* been persisted. Typical use: probing an RTSP URL entered by the
|
|
11175
|
-
* user. Providers that don't support field probing return
|
|
11176
|
-
* `{ success: true, message: 'Field test not supported' }`.
|
|
11177
|
-
*
|
|
11178
|
-
* `formValues` is the live snapshot of every field in the form at
|
|
11179
|
-
* the moment the user clicked Test — useful for probes that depend
|
|
11180
|
-
* on multiple fields together (e.g. Reolink autodetect needs host
|
|
11181
|
-
* + credentials + UID + transport mode in a single call). Optional
|
|
11182
|
-
* for backwards compatibility; providers free to ignore it.
|
|
11183
|
-
*/
|
|
11184
|
-
testCreationField: method(object({
|
|
11185
|
-
type: _enum(DeviceType),
|
|
11186
|
-
key: string(),
|
|
11187
|
-
value: unknown(),
|
|
11188
|
-
formValues: record(string(), unknown()).optional()
|
|
11189
|
-
}), FieldProbeResultSchema, {
|
|
11190
|
-
kind: "mutation",
|
|
11191
|
-
auth: "admin"
|
|
11192
|
-
})
|
|
11193
|
-
}
|
|
11194
|
-
};
|
|
11195
|
-
/**
|
|
11196
|
-
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
11197
|
-
* live registry access, and all management operations into a single tRPC surface.
|
|
11198
|
-
*
|
|
11199
|
-
* Replaces:
|
|
11200
|
-
* - `device-persistence` capability (persistence methods absorbed here)
|
|
11201
|
-
* - `device-management.router.ts` (deleted in Phase 2)
|
|
11202
|
-
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
11203
|
-
*
|
|
11204
|
-
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
11205
|
-
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
11206
|
-
* - No nodeId routing needed — this is a pure hub singleton.
|
|
11207
|
-
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
11208
|
-
* - No shadow registry or cross-node aggregation required.
|
|
11209
|
-
*
|
|
11210
|
-
* Forked workers register devices back to the hub via `ctx.devices`
|
|
11211
|
-
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
11212
|
-
*/
|
|
11213
|
-
/** One child-placement directive on a container's `childLayout`. Structurally
|
|
11214
|
-
* identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
|
|
11215
|
-
* shape for the same field. The child is identified by its re-sync-stable
|
|
11216
|
-
* accessory `stableIdSuffix` (`childKey`); listed children are grouped into
|
|
11217
|
-
* named accordion sections (with optional intra-section order). */
|
|
11218
|
-
var ChildLayoutEntrySchema = object({
|
|
11219
|
-
childKey: string(),
|
|
11220
|
-
section: string(),
|
|
11221
|
-
order: number().optional(),
|
|
11222
|
-
collapsed: boolean().optional()
|
|
11223
|
-
});
|
|
11224
|
-
/** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
|
|
11225
|
-
* `device-management.ts`. Source is a union: a FIELD source copies a sibling
|
|
11226
|
-
* accessory's status field (`kind` optional/absent for wire compat); a
|
|
11227
|
-
* LITERAL source carries a per-device constant (no sibling is read); a
|
|
11228
|
-
* GLOBAL source (P2e) copies ANY device's status field, addressed by the
|
|
11229
|
-
* source device's full re-sync-stable `stableId`. */
|
|
11230
|
-
var DeviceLinkFieldSourceSchema = object({
|
|
11231
|
-
kind: literal("field").optional(),
|
|
11232
|
-
sourceKey: string(),
|
|
11233
|
-
cap: string(),
|
|
11234
|
-
fieldPath: string()
|
|
11235
|
-
});
|
|
11236
|
-
var DeviceLinkLiteralSourceSchema = object({
|
|
11237
|
-
kind: literal("literal"),
|
|
11238
|
-
value: union([
|
|
11239
|
-
string(),
|
|
11240
|
-
number(),
|
|
11241
|
-
boolean(),
|
|
11242
|
-
_null()
|
|
11243
|
-
])
|
|
11244
|
-
});
|
|
11245
|
-
var DeviceLinkGlobalSourceSchema = object({
|
|
11246
|
-
kind: literal("global"),
|
|
11247
|
-
sourceStableId: string(),
|
|
11248
|
-
cap: string(),
|
|
11249
|
-
fieldPath: string()
|
|
11250
|
-
});
|
|
11251
|
-
/** Expression source (Stage X): compute the target field from N named bindings
|
|
11252
|
-
* via the safe expression engine. Bindings are field | literal | global — never
|
|
11253
|
-
* another expression (no nesting). The `superRefine` runs the SAME author-time
|
|
11254
|
-
* validation as `validateExpressionSource` (compiles the expr, checks binding
|
|
11255
|
-
* names + identifier coverage) so every wire boundary that parses a DeviceLink
|
|
11256
|
-
* (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
|
|
11257
|
-
* Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
|
|
11258
|
-
var DeviceLinkExpressionSourceSchema = object({
|
|
11259
|
-
kind: literal("expression"),
|
|
11260
|
-
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
11261
|
-
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), union([
|
|
11262
|
-
DeviceLinkFieldSourceSchema,
|
|
11263
|
-
DeviceLinkLiteralSourceSchema,
|
|
11264
|
-
DeviceLinkGlobalSourceSchema
|
|
11265
|
-
]))
|
|
11266
|
-
}).superRefine((src, ctx) => {
|
|
11267
|
-
const err = validateExpressionSource(src);
|
|
11268
|
-
if (err !== null) ctx.addIssue({
|
|
11269
|
-
code: "custom",
|
|
11270
|
-
message: err,
|
|
11271
|
-
path: ["expr"]
|
|
11272
|
-
});
|
|
11273
|
-
});
|
|
11274
|
-
var DeviceLinkSchema = object({
|
|
11275
|
-
id: string(),
|
|
11276
|
-
source: union([
|
|
11277
|
-
DeviceLinkFieldSourceSchema,
|
|
11278
|
-
DeviceLinkLiteralSourceSchema,
|
|
11279
|
-
DeviceLinkGlobalSourceSchema,
|
|
11280
|
-
DeviceLinkExpressionSourceSchema
|
|
11281
|
-
]),
|
|
11282
|
-
target: object({
|
|
11283
|
-
cap: string(),
|
|
11284
|
-
fieldPath: string(),
|
|
11285
|
-
itemKey: string().optional()
|
|
11286
|
-
}),
|
|
11287
|
-
transform: discriminatedUnion("kind", [
|
|
11288
|
-
object({ kind: literal("identity") }),
|
|
11289
|
-
object({
|
|
11290
|
-
kind: literal("enum-map"),
|
|
11291
|
-
mapping: record(string(), union([
|
|
11292
|
-
string(),
|
|
11293
|
-
number(),
|
|
11294
|
-
boolean()
|
|
11295
|
-
])),
|
|
11296
|
-
fallback: union([
|
|
11297
|
-
string(),
|
|
11298
|
-
number(),
|
|
11299
|
-
boolean()
|
|
11300
|
-
]).optional()
|
|
11301
|
-
}),
|
|
11302
|
-
object({
|
|
11303
|
-
kind: literal("linear"),
|
|
11304
|
-
scale: number(),
|
|
11305
|
-
offset: number(),
|
|
11306
|
-
clamp: tuple([number(), number()]).readonly().optional()
|
|
11307
|
-
})
|
|
11308
|
-
]).optional()
|
|
11309
|
-
});
|
|
11310
|
-
/** Cap-wire shape of a per-cap display refinement — mirrors
|
|
11311
|
-
* `DeviceCapDisplayOverride` in `device-management.ts`. */
|
|
11312
|
-
var DeviceCapDisplayOverrideSchema = object({
|
|
11313
|
-
unit: string().min(1).optional(),
|
|
11314
|
-
precision: number().int().min(0).max(10).optional()
|
|
11315
|
-
});
|
|
11316
|
-
/** Cap-wire shape of an operator-authored per-device display override —
|
|
11317
|
-
* mirrors `DeviceDisplayOverride` in `device-management.ts`. `precision`
|
|
11318
|
-
* bounds mirror `numeric-sensor.cap.ts` (`int 0-10`). */
|
|
11319
|
-
var DeviceDisplayOverrideSchema = object({
|
|
11320
|
-
icon: string().min(1).optional(),
|
|
11321
|
-
label: string().min(1).optional(),
|
|
11322
|
-
unit: string().min(1).optional(),
|
|
11323
|
-
precision: number().int().min(0).max(10).optional(),
|
|
11324
|
-
hidden: boolean().optional(),
|
|
11325
|
-
perCap: record(string(), DeviceCapDisplayOverrideSchema).optional()
|
|
11326
|
-
});
|
|
11327
|
-
/** Cap-wire shape of a per-role display default — mirrors `RoleDisplayDefault`
|
|
11328
|
-
* in `device-management.ts`. Keyed by `DeviceRole` string (role strings cross
|
|
11329
|
-
* the wire as plain strings everywhere else — cf. `DeviceInfoSchema.role`). */
|
|
11330
|
-
var RoleDisplayDefaultSchema = object({
|
|
11331
|
-
unit: string().min(1).optional(),
|
|
11332
|
-
precision: number().int().min(0).max(10).optional(),
|
|
11333
|
-
icon: string().min(1).optional()
|
|
11334
|
-
});
|
|
11335
|
-
/**
|
|
11336
|
-
* Serializable projection of a live IDevice.
|
|
11337
|
-
* Returned by listAll, getDevice, getChildren.
|
|
11338
|
-
* Live methods (getStreamSources, getConfigSchema) are separate calls.
|
|
10798
|
+
* Serializable projection of a live IDevice.
|
|
10799
|
+
* Returned by listAll, getDevice, getChildren.
|
|
10800
|
+
* Live methods (getStreamSources, getConfigSchema) are separate calls.
|
|
11339
10801
|
*/
|
|
11340
10802
|
var DeviceInfoSchema = object({
|
|
11341
10803
|
/** Progressive, system-wide unique number. Allocated synchronously by
|
|
@@ -11386,8 +10848,6 @@ var DeviceInfoSchema = object({
|
|
|
11386
10848
|
* named accordion sections (with optional intra-section order). See
|
|
11387
10849
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
11388
10850
|
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
11389
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
11390
|
-
deviceLinks: array(DeviceLinkSchema).readonly().optional(),
|
|
11391
10851
|
/** Operator-authored per-device display override. See `DeviceMeta.display`. */
|
|
11392
10852
|
display: DeviceDisplayOverrideSchema.optional()
|
|
11393
10853
|
});
|
|
@@ -11396,7 +10856,7 @@ var ConfigEntrySchema = object({
|
|
|
11396
10856
|
value: unknown(),
|
|
11397
10857
|
description: string().optional()
|
|
11398
10858
|
});
|
|
11399
|
-
var
|
|
10859
|
+
var LinkedDevicesModeSchema = _enum(["auto", "manual"]);
|
|
11400
10860
|
/** One resolved linked device — the compact projection consumers need. */
|
|
11401
10861
|
var LinkedDeviceSchema = object({
|
|
11402
10862
|
deviceId: number(),
|
|
@@ -11459,8 +10919,6 @@ var DeviceMetaSchema = object({
|
|
|
11459
10919
|
* accordion sections (with optional intra-section order). See
|
|
11460
10920
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
11461
10921
|
childLayout: array(ChildLayoutEntrySchema).readonly().optional(),
|
|
11462
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
11463
|
-
deviceLinks: array(DeviceLinkSchema).readonly().optional(),
|
|
11464
10922
|
/** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
|
|
11465
10923
|
* Optional: only present for accessory children that carry a known role. */
|
|
11466
10924
|
role: string().nullable().optional(),
|
|
@@ -11553,12 +11011,6 @@ method(object({
|
|
|
11553
11011
|
}), _void(), {
|
|
11554
11012
|
kind: "mutation",
|
|
11555
11013
|
auth: "admin"
|
|
11556
|
-
}), method(object({
|
|
11557
|
-
deviceId: number(),
|
|
11558
|
-
deviceLinks: array(DeviceLinkSchema).readonly()
|
|
11559
|
-
}), _void(), {
|
|
11560
|
-
kind: "mutation",
|
|
11561
|
-
auth: "admin"
|
|
11562
11014
|
}), method(object({
|
|
11563
11015
|
deviceId: number(),
|
|
11564
11016
|
display: DeviceDisplayOverrideSchema.nullable()
|
|
@@ -11640,7 +11092,7 @@ method(object({
|
|
|
11640
11092
|
* shipping 293 rows to find 12. */
|
|
11641
11093
|
isCamera: boolean().optional()
|
|
11642
11094
|
}), array(DeviceInfoSchema)), method(object({ deviceId: number() }), DeviceInfoSchema.nullable()), method(object({ parentDeviceId: number() }), array(DeviceInfoSchema)), method(object({ deviceId: number() }), object({
|
|
11643
|
-
mode:
|
|
11095
|
+
mode: LinkedDevicesModeSchema,
|
|
11644
11096
|
devices: array(LinkedDeviceSchema)
|
|
11645
11097
|
})), method(object({ deviceId: number() }), array(StreamSourceEntrySchema$1)), method(object({ deviceId: number() }), array(ConfigEntrySchema)), method(object({ deviceId: number() }), ConfigUISchemaOutput), method(object({
|
|
11646
11098
|
deviceId: number(),
|
|
@@ -11673,11 +11125,7 @@ method(object({
|
|
|
11673
11125
|
deviceId: number(),
|
|
11674
11126
|
entries: array(object({
|
|
11675
11127
|
capName: string(),
|
|
11676
|
-
kind: _enum([
|
|
11677
|
-
"native",
|
|
11678
|
-
"wrapped",
|
|
11679
|
-
"linked"
|
|
11680
|
-
]),
|
|
11128
|
+
kind: _enum(["native", "wrapped"]),
|
|
11681
11129
|
providerAddonId: string(),
|
|
11682
11130
|
providerNodeId: string(),
|
|
11683
11131
|
nativeAddonId: string()
|
|
@@ -11686,11 +11134,7 @@ method(object({
|
|
|
11686
11134
|
deviceId: number(),
|
|
11687
11135
|
entries: array(object({
|
|
11688
11136
|
capName: string(),
|
|
11689
|
-
kind: _enum([
|
|
11690
|
-
"native",
|
|
11691
|
-
"wrapped",
|
|
11692
|
-
"linked"
|
|
11693
|
-
]),
|
|
11137
|
+
kind: _enum(["native", "wrapped"]),
|
|
11694
11138
|
providerAddonId: string(),
|
|
11695
11139
|
providerNodeId: string(),
|
|
11696
11140
|
nativeAddonId: string()
|
|
@@ -12493,7 +11937,7 @@ var MotionAnalysisResultSchema = object({
|
|
|
12493
11937
|
frameHeight: number(),
|
|
12494
11938
|
analysisMs: number()
|
|
12495
11939
|
});
|
|
12496
|
-
method(object({
|
|
11940
|
+
DeviceType.Camera, method(object({
|
|
12497
11941
|
deviceId: number(),
|
|
12498
11942
|
frame: FrameInputSchema.optional(),
|
|
12499
11943
|
frameHandle: FrameHandleSchema.optional()
|
|
@@ -14710,6 +14154,18 @@ var OauthIntegrationDescriptorSchema = object({
|
|
|
14710
14154
|
* redirect_uri that does not start with one of these. Required —
|
|
14711
14155
|
* an empty list means the integration can never complete linking. */
|
|
14712
14156
|
allowedRedirectPrefixes: array(string()).min(1),
|
|
14157
|
+
/** Paths accepted as a `redirect_uri` when the host is PRIVATE — loopback,
|
|
14158
|
+
* RFC1918, CGNAT (100.64/10, Tailscale), link-local, IPv6 ULA, or an
|
|
14159
|
+
* `.local` / `.internal` / `.ts.net` name. Exists for self-hosted clients
|
|
14160
|
+
* whose address the hub cannot know in advance (a Home Assistant at
|
|
14161
|
+
* `http://<lan-ip>:8123/auth/external/callback`). The PATH must match
|
|
14162
|
+
* exactly; a public host never satisfies this branch, so it is not a
|
|
14163
|
+
* wildcard prefix by another name. */
|
|
14164
|
+
allowedPrivateHostPaths: array(string()).optional(),
|
|
14165
|
+
/** When true this is a PUBLIC client (source is published, no secret can be
|
|
14166
|
+
* protected) and PKCE is mandatory: `/authorize` refuses without an S256
|
|
14167
|
+
* `code_challenge`, `/token` refuses without the matching `code_verifier`. */
|
|
14168
|
+
requiresPkce: boolean().optional(),
|
|
14713
14169
|
/** Optional public origin (no trailing slash) that this integration's
|
|
14714
14170
|
* issued codes/tokens should carry as the `hubUrl` claim — typically the
|
|
14715
14171
|
* operator-selected external-access endpoint resolved by the addon. When
|
|
@@ -14860,7 +14316,7 @@ var TrackEnvelopeSchema = object({
|
|
|
14860
14316
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
14861
14317
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
14862
14318
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
14863
|
-
* zonesVisited, bestEventId, envelope) and returns `positions` /
|
|
14319
|
+
* zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
|
|
14864
14320
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
14865
14321
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
14866
14322
|
* (`getObjectEvents` et al.).
|
|
@@ -14902,6 +14358,30 @@ var TrackSourceSchema = _enum([
|
|
|
14902
14358
|
"audio"
|
|
14903
14359
|
]);
|
|
14904
14360
|
/**
|
|
14361
|
+
* Where a track sits in the RETRAIN lifecycle (D81).
|
|
14362
|
+
*
|
|
14363
|
+
* - `none` — never marked, or un-marked. Evictable.
|
|
14364
|
+
* - `staging` — the operator wants this track as training material and has not
|
|
14365
|
+
* finished with it. **This is the only state retention holds**: the track and
|
|
14366
|
+
* everything it owns (object events, crops, keyframes, CLIP vector) survive
|
|
14367
|
+
* the device's age window.
|
|
14368
|
+
* - `trained` — the retrain page has taken what it needed. The frames it chose
|
|
14369
|
+
* were COPIED into the retrain dataset at selection time, so the dataset no
|
|
14370
|
+
* longer depends on the track's media and the track becomes EVICTABLE again.
|
|
14371
|
+
* Terminal for the plain `markForTrain` toggle: returning it to `staging` is
|
|
14372
|
+
* a deliberate action of the retrain page, not a side effect of a checkbox.
|
|
14373
|
+
*
|
|
14374
|
+
* There is no `null`. The state is stored `TEXT NOT NULL DEFAULT 'none'` because
|
|
14375
|
+
* the store's filter language has only positive equality and `whereIn` — no
|
|
14376
|
+
* negation, no IS NULL — so a NULL would be unselectable by ANY predicate and
|
|
14377
|
+
* would make the entire pre-column history immortal in one deploy.
|
|
14378
|
+
*/
|
|
14379
|
+
var RetrainStatusSchema = _enum([
|
|
14380
|
+
"none",
|
|
14381
|
+
"staging",
|
|
14382
|
+
"trained"
|
|
14383
|
+
]);
|
|
14384
|
+
/**
|
|
14905
14385
|
* Per-track OPERATOR flags — set by hand from the admin UI or the viewer, never
|
|
14906
14386
|
* by the pipeline. Spread into `TrackSchema` and `KeyEventSchema` from one place
|
|
14907
14387
|
* so the two surfaces cannot drift.
|
|
@@ -14911,18 +14391,31 @@ var TrackSourceSchema = _enum([
|
|
|
14911
14391
|
* columns existed read as absent, and a consumer that needs a boolean should say
|
|
14912
14392
|
* `flag === true`, not `flag !== false`.
|
|
14913
14393
|
*
|
|
14914
|
-
*
|
|
14915
|
-
*
|
|
14916
|
-
*
|
|
14917
|
-
* `
|
|
14394
|
+
* `markForTrain` is the WIRE FACE of {@link RetrainStatusSchema}, not a column:
|
|
14395
|
+
* it is exactly `retrainStatus === 'staging'`, in both directions. Writing
|
|
14396
|
+
* `true` moves `none → staging`, writing `false` moves `staging → none`, and a
|
|
14397
|
+
* `trained` track reports `false` while refusing both writes. The boolean is
|
|
14398
|
+
* kept because three surfaces drive a toggle off it; anything that needs to tell
|
|
14399
|
+
* "never marked" from "already trained" must read `retrainStatus`.
|
|
14400
|
+
*
|
|
14401
|
+
* `debug` does NOT pin; it is attention, not durability.
|
|
14918
14402
|
*/
|
|
14919
14403
|
var TrackFlagFields = {
|
|
14920
|
-
/** Operator marked this track as training material.
|
|
14404
|
+
/** Operator marked this track as training material — i.e. `retrainStatus` is
|
|
14405
|
+
* `'staging'`. */
|
|
14921
14406
|
markForTrain: boolean().optional(),
|
|
14922
14407
|
/** Operator marked this track for diagnostic attention. */
|
|
14923
14408
|
debug: boolean().optional()
|
|
14924
14409
|
};
|
|
14925
14410
|
/**
|
|
14411
|
+
* The lifecycle field itself, on the READ surfaces only (`Track`, `KeyEvent`).
|
|
14412
|
+
* Deliberately NOT part of {@link TrackFlagFields}: that group also builds the
|
|
14413
|
+
* write patch, and the status is not something the toggle sets — it is what the
|
|
14414
|
+
* toggle's boolean is derived from. Absent on an in-RAM track never touched;
|
|
14415
|
+
* always present on a persisted row (the column default materialises `'none'`).
|
|
14416
|
+
*/
|
|
14417
|
+
var TrackRetrainFields = { retrainStatus: RetrainStatusSchema.optional() };
|
|
14418
|
+
/**
|
|
14926
14419
|
* The write half: a PARTIAL patch. An omitted key is left untouched, so setting
|
|
14927
14420
|
* one flag can never clear the other — the toggles are independent and are
|
|
14928
14421
|
* driven from three surfaces that do not know about each other.
|
|
@@ -14936,13 +14429,92 @@ var TrackFlagsPatchSchema = object(TrackFlagFields);
|
|
|
14936
14429
|
var TrackFlagsSchema = object({
|
|
14937
14430
|
trackId: string(),
|
|
14938
14431
|
markForTrain: boolean(),
|
|
14939
|
-
debug: boolean()
|
|
14432
|
+
debug: boolean(),
|
|
14433
|
+
/** The lifecycle state the boolean was derived from. Required here (unlike on
|
|
14434
|
+
* a track row) because this shape is only ever produced by the write body,
|
|
14435
|
+
* which always knows it — and a surface that has just written needs to render
|
|
14436
|
+
* `trained` without a re-fetch. */
|
|
14437
|
+
retrainStatus: RetrainStatusSchema
|
|
14438
|
+
});
|
|
14439
|
+
union([literal(1), literal(2)]);
|
|
14440
|
+
/**
|
|
14441
|
+
* WHO decided a label, and when. Carried per tier so a value can be traced to
|
|
14442
|
+
* the step and model that produced it — which is what makes the write rule
|
|
14443
|
+
* arguable after the fact ("why is 592's label `dog` and not `Canis lupus`?")
|
|
14444
|
+
* and what lets a migrated, UNATTRIBUTED value be told apart from a real one.
|
|
14445
|
+
*
|
|
14446
|
+
* `stepId` is the pipeline step id (`animal-classifier`, `bird-classifier`,
|
|
14447
|
+
* `plate-ocr`, `face-embedding`, `object-detection`), or the sentinel
|
|
14448
|
+
* `migration:4g` for a value the 4g migration moved from the single-slot era —
|
|
14449
|
+
* that value has no provenance, and the write rule lets ANY properly-attributed
|
|
14450
|
+
* write of the same tier replace it regardless of score.
|
|
14451
|
+
*/
|
|
14452
|
+
var LabelAttributionSchema = object({
|
|
14453
|
+
stepId: string(),
|
|
14454
|
+
modelId: string().optional(),
|
|
14455
|
+
decidedAt: number()
|
|
14456
|
+
});
|
|
14457
|
+
/**
|
|
14458
|
+
* The TIERED label model (roadmap 4g), spread into `TrackSchema` and
|
|
14459
|
+
* `ObjectEventSchema` from ONE place so the two surfaces cannot drift — a
|
|
14460
|
+
* track and its events always answer the same question the same way.
|
|
14461
|
+
*
|
|
14462
|
+
* Two scalar columns, not an array: every consumer wants "the coarse one" or
|
|
14463
|
+
* "the fine one", and an array made both a scan. `label` is tier 1, `subLabel`
|
|
14464
|
+
* is tier 2, and each carries its own score + attribution.
|
|
14465
|
+
*
|
|
14466
|
+
* **Reading it.** What a human should be shown is `subLabel ?? label` — the
|
|
14467
|
+
* finest thing known. Before 4g the single `label` column held the finest
|
|
14468
|
+
* value, so a consumer that has not been updated reads the tier-1 slot and
|
|
14469
|
+
* shows nothing on a species-only row; that is why the migration puts every
|
|
14470
|
+
* pre-4g value in tier 2 (it cannot regress a display that reads the fallback)
|
|
14471
|
+
* and why the read surfaces were changed in the same train.
|
|
14472
|
+
*
|
|
14473
|
+
* **Writing it.** The slots are independent, which is the whole point: a
|
|
14474
|
+
* tier-1 write (`bird`) can never overwrite a tier-2 value (`Turdus
|
|
14475
|
+
* migratorius`), so fineness cannot regress by construction. Within a tier the
|
|
14476
|
+
* higher score wins. One rule, one implementation — see
|
|
14477
|
+
* `pipeline/label-tier.ts` in addon-post-analysis.
|
|
14478
|
+
*/
|
|
14479
|
+
var TieredLabelFields = {
|
|
14480
|
+
/** Tier 1 — the sub-class. See {@link LabelTierSchema}. */
|
|
14481
|
+
label: string().optional(),
|
|
14482
|
+
/** Confidence of the tier-1 value, as reported by the deciding step. */
|
|
14483
|
+
labelScore: number().optional(),
|
|
14484
|
+
/** Provenance of the tier-1 value. See {@link LabelAttributionSchema}. */
|
|
14485
|
+
labelMeta: LabelAttributionSchema.optional(),
|
|
14486
|
+
/** Tier 2 — the instance. See {@link LabelTierSchema}. */
|
|
14487
|
+
subLabel: string().optional(),
|
|
14488
|
+
/** Confidence of the tier-2 value, as reported by the deciding step. */
|
|
14489
|
+
subLabelScore: number().optional(),
|
|
14490
|
+
/** Provenance of the tier-2 value. See {@link LabelAttributionSchema}. */
|
|
14491
|
+
subLabelMeta: LabelAttributionSchema.optional()
|
|
14492
|
+
};
|
|
14493
|
+
/** Per-camera slice of a training-export estimate. */
|
|
14494
|
+
var TrainingExportDeviceTotalsSchema = object({
|
|
14495
|
+
deviceId: number(),
|
|
14496
|
+
tracks: number().int(),
|
|
14497
|
+
files: number().int(),
|
|
14498
|
+
bytes: number().int()
|
|
14499
|
+
});
|
|
14500
|
+
/**
|
|
14501
|
+
* What a training export WOULD contain. Computed from media index rows only —
|
|
14502
|
+
* no blob is read to produce this.
|
|
14503
|
+
*/
|
|
14504
|
+
var TrainingExportSummarySchema = object({
|
|
14505
|
+
generatedAt: number(),
|
|
14506
|
+
trackCount: number().int(),
|
|
14507
|
+
fileCount: number().int(),
|
|
14508
|
+
byteCount: number().int(),
|
|
14509
|
+
/** More marked tracks exist than a single pass carries. */
|
|
14510
|
+
truncated: boolean(),
|
|
14511
|
+
devices: array(TrainingExportDeviceTotalsSchema).readonly()
|
|
14940
14512
|
});
|
|
14941
14513
|
var TrackSchema = object({
|
|
14942
14514
|
trackId: string(),
|
|
14943
14515
|
deviceId: number(),
|
|
14944
14516
|
className: string(),
|
|
14945
|
-
|
|
14517
|
+
...TieredLabelFields,
|
|
14946
14518
|
producingDeviceName: string().optional(),
|
|
14947
14519
|
/** Track provenance. Absent ⇒ `pipeline` (legacy rows). */
|
|
14948
14520
|
source: TrackSourceSchema.optional(),
|
|
@@ -14981,7 +14553,26 @@ var TrackSchema = object({
|
|
|
14981
14553
|
* Populated from the persisted envelope columns on historical reads;
|
|
14982
14554
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
14983
14555
|
envelope: TrackEnvelopeSchema.optional(),
|
|
14984
|
-
|
|
14556
|
+
/**
|
|
14557
|
+
* A face DETECTOR found a face on this track — nothing more. It says the
|
|
14558
|
+
* detail plane produced a `face` detail; it does NOT say the face was
|
|
14559
|
+
* embedded, matched, above `minFacePx`, or that the recognizer was even
|
|
14560
|
+
* enabled. Set once and never cleared.
|
|
14561
|
+
*
|
|
14562
|
+
* **This exists so "face present but not recognised" is expressible.** A
|
|
14563
|
+
* recognised identity lands in `subLabel` (attributed to the face chain via
|
|
14564
|
+
* `subLabelMeta.stepId`), so before this field a track with an unmatched face
|
|
14565
|
+
* and a track with no face at all were byte-identical on the wire and no
|
|
14566
|
+
* surface could tell them apart. The read is `hasFace === true && subLabel
|
|
14567
|
+
* === undefined`.
|
|
14568
|
+
*
|
|
14569
|
+
* **Absent ≠ false.** Every row written before the column existed omits it,
|
|
14570
|
+
* and so does every server that predates the field — a consumer must test
|
|
14571
|
+
* `=== true` and render nothing otherwise, never infer "no face".
|
|
14572
|
+
*/
|
|
14573
|
+
hasFace: boolean().optional(),
|
|
14574
|
+
...TrackFlagFields,
|
|
14575
|
+
...TrackRetrainFields
|
|
14985
14576
|
});
|
|
14986
14577
|
var BaseEventFields = {
|
|
14987
14578
|
id: string(),
|
|
@@ -15054,7 +14645,7 @@ var ObjectEventSchema = object({
|
|
|
15054
14645
|
/** Omitted in slim projection. */
|
|
15055
14646
|
trackId: string().optional(),
|
|
15056
14647
|
className: string(),
|
|
15057
|
-
|
|
14648
|
+
...TieredLabelFields,
|
|
15058
14649
|
/** Omitted in slim projection. */
|
|
15059
14650
|
confidence: number().optional(),
|
|
15060
14651
|
/** Heavy JSON — omitted in slim projection. */
|
|
@@ -15135,6 +14726,173 @@ var MediaFileSchema = object({
|
|
|
15135
14726
|
* stored blob and a `?variant=thumb` rendering without fetching either.
|
|
15136
14727
|
*/
|
|
15137
14728
|
var MediaFileInfoSchema = MediaFileSchema.omit({ base64: true });
|
|
14729
|
+
/**
|
|
14730
|
+
* The MACRO tier of an annotation — a CLOSED set.
|
|
14731
|
+
*
|
|
14732
|
+
* This is what the exported detector predicts, so a typo here is a new class
|
|
14733
|
+
* with one example in it. `label` and `subLabel` are open strings by contrast:
|
|
14734
|
+
* the whole point of the page is teaching the model things it does not know
|
|
14735
|
+
* yet, and constraining that vocabulary would make it useless.
|
|
14736
|
+
*
|
|
14737
|
+
* A macro class is NEVER a label. The provider refuses a write whose `label` or
|
|
14738
|
+
* `subLabel` is one of these values, in any casing, because once `person`
|
|
14739
|
+
* exists in both tiers "every person box" stops being answerable without
|
|
14740
|
+
* knowing every string anyone ever typed — and the damage is retroactive.
|
|
14741
|
+
*/
|
|
14742
|
+
var RetrainMacroClassSchema = _enum([
|
|
14743
|
+
"person",
|
|
14744
|
+
"vehicle",
|
|
14745
|
+
"animal",
|
|
14746
|
+
"package",
|
|
14747
|
+
"face",
|
|
14748
|
+
"plate"
|
|
14749
|
+
]);
|
|
14750
|
+
/** A subject to learn, or a phantom to unlearn (taught by OMISSION). */
|
|
14751
|
+
var RetrainAnnotationKindSchema = _enum(["subject", "model_error"]);
|
|
14752
|
+
/** Did a human draw this box, or did the assist propose it? */
|
|
14753
|
+
var RetrainAnnotationSourceSchema = _enum(["operator", "assist"]);
|
|
14754
|
+
/** Normalised `[0,1]` rectangle against the FULL frame — the canonical form. */
|
|
14755
|
+
var RetrainBboxSchema = object({
|
|
14756
|
+
x: number(),
|
|
14757
|
+
y: number(),
|
|
14758
|
+
w: number(),
|
|
14759
|
+
h: number()
|
|
14760
|
+
});
|
|
14761
|
+
/**
|
|
14762
|
+
* One annotated subject.
|
|
14763
|
+
*
|
|
14764
|
+
* `bbox` is normalised against the full frame, ALWAYS. The per-model shapes
|
|
14765
|
+
* (letterboxed root / zone-cropped package / subject-cropped classifier) are
|
|
14766
|
+
* derived from it at export and never stored — storing them is how one feature
|
|
14767
|
+
* space ends up holding two crops of the same subject (D52).
|
|
14768
|
+
*/
|
|
14769
|
+
var RetrainAnnotationSchema = object({
|
|
14770
|
+
id: string(),
|
|
14771
|
+
trackId: string(),
|
|
14772
|
+
deviceId: number(),
|
|
14773
|
+
/** The COPY in retrain storage — never the source track's media key. */
|
|
14774
|
+
mediaKey: string(),
|
|
14775
|
+
bbox: RetrainBboxSchema,
|
|
14776
|
+
macroClass: RetrainMacroClassSchema,
|
|
14777
|
+
label: string().optional(),
|
|
14778
|
+
subLabel: string().optional(),
|
|
14779
|
+
kind: RetrainAnnotationKindSchema,
|
|
14780
|
+
source: RetrainAnnotationSourceSchema,
|
|
14781
|
+
/** Which model proposed this box — or, on a `model_error`, drew the phantom. */
|
|
14782
|
+
assistModelId: string().optional(),
|
|
14783
|
+
assistScore: number().optional(),
|
|
14784
|
+
exportedInBatch: string().optional(),
|
|
14785
|
+
createdAt: number()
|
|
14786
|
+
});
|
|
14787
|
+
/** The write form — the server owns `id`, `createdAt` and the frame binding. */
|
|
14788
|
+
var RetrainAnnotationDraftSchema = RetrainAnnotationSchema.omit({
|
|
14789
|
+
id: true,
|
|
14790
|
+
trackId: true,
|
|
14791
|
+
deviceId: true,
|
|
14792
|
+
mediaKey: true,
|
|
14793
|
+
createdAt: true,
|
|
14794
|
+
exportedInBatch: true
|
|
14795
|
+
});
|
|
14796
|
+
/** A track sitting in `staging`, with everything the worklist needs to rank it. */
|
|
14797
|
+
var RetrainTrackSchema = object({
|
|
14798
|
+
trackId: string(),
|
|
14799
|
+
deviceId: number(),
|
|
14800
|
+
className: string(),
|
|
14801
|
+
label: string().optional(),
|
|
14802
|
+
firstSeen: number(),
|
|
14803
|
+
lastSeen: number(),
|
|
14804
|
+
/** How many frames the dataset already holds from this track. */
|
|
14805
|
+
frameCount: number().int(),
|
|
14806
|
+
/** How many subjects have been annotated on those frames. `0` with
|
|
14807
|
+
* `frameCount: 0` is exactly "staging, still to work". */
|
|
14808
|
+
annotationCount: number().int()
|
|
14809
|
+
});
|
|
14810
|
+
/** A frame the picker may offer — an index row, no blob was read to produce it. */
|
|
14811
|
+
var RetrainFrameCandidateSchema = object({
|
|
14812
|
+
mediaKey: string(),
|
|
14813
|
+
kind: MediaFileKindEnum,
|
|
14814
|
+
timestamp: number(),
|
|
14815
|
+
sizeBytes: number().int(),
|
|
14816
|
+
/** A copy of this original already exists — selecting it is free and cannot
|
|
14817
|
+
* fail, whatever became of the original. */
|
|
14818
|
+
copied: boolean()
|
|
14819
|
+
});
|
|
14820
|
+
/** A frame the dataset OWNS: bytes copied at selection time. */
|
|
14821
|
+
var RetrainFrameSchema = object({
|
|
14822
|
+
frameId: string(),
|
|
14823
|
+
deviceId: number(),
|
|
14824
|
+
trackId: string(),
|
|
14825
|
+
/** Provenance only. It may already point at nothing — that is expected. */
|
|
14826
|
+
sourceMediaKey: string(),
|
|
14827
|
+
sourceKind: MediaFileKindEnum,
|
|
14828
|
+
sizeBytes: number().int(),
|
|
14829
|
+
width: number().int(),
|
|
14830
|
+
height: number().int(),
|
|
14831
|
+
copiedAt: number()
|
|
14832
|
+
});
|
|
14833
|
+
/** Why a copy-on-select could not be honoured — named, never a silent skip. */
|
|
14834
|
+
var RetrainCopyRefusalSchema = _enum([
|
|
14835
|
+
"source-missing",
|
|
14836
|
+
"unreadable-image",
|
|
14837
|
+
"write-failed"
|
|
14838
|
+
]);
|
|
14839
|
+
var RetrainFrameSelectionSchema = object({
|
|
14840
|
+
copied: array(RetrainFrameSchema).readonly(),
|
|
14841
|
+
refused: array(object({
|
|
14842
|
+
sourceMediaKey: string(),
|
|
14843
|
+
reason: RetrainCopyRefusalSchema
|
|
14844
|
+
})).readonly()
|
|
14845
|
+
});
|
|
14846
|
+
var RetrainFrameListSchema = object({
|
|
14847
|
+
candidates: array(RetrainFrameCandidateSchema).readonly(),
|
|
14848
|
+
copies: array(RetrainFrameSchema).readonly(),
|
|
14849
|
+
/** What the page pre-selects — the native key frame when one survives. */
|
|
14850
|
+
autoPickMediaKey: string().optional()
|
|
14851
|
+
});
|
|
14852
|
+
/** What the operator asked the assist to look for. */
|
|
14853
|
+
var RetrainAssistSubjectSchema = discriminatedUnion("kind", [object({
|
|
14854
|
+
kind: literal("package"),
|
|
14855
|
+
zone: RetrainBboxSchema.optional()
|
|
14856
|
+
}), object({
|
|
14857
|
+
kind: literal("objects"),
|
|
14858
|
+
modelId: string(),
|
|
14859
|
+
minScore: number().optional()
|
|
14860
|
+
})]);
|
|
14861
|
+
/**
|
|
14862
|
+
* The assist's answer — a discriminated union, because "the model saw nothing"
|
|
14863
|
+
* and "this node cannot run that model" lead to different next moves and a
|
|
14864
|
+
* nullable result cannot tell them apart.
|
|
14865
|
+
*/
|
|
14866
|
+
var RetrainAssistResultSchema = discriminatedUnion("kind", [object({
|
|
14867
|
+
kind: literal("proposed"),
|
|
14868
|
+
modelId: string(),
|
|
14869
|
+
stepId: string(),
|
|
14870
|
+
minScore: number(),
|
|
14871
|
+
/** Drafts, ready to edit. `source: 'assist'` until the operator touches one. */
|
|
14872
|
+
proposals: array(RetrainAnnotationDraftSchema).readonly(),
|
|
14873
|
+
/** Returned by the runner but removed by the threshold. */
|
|
14874
|
+
belowThreshold: number().int()
|
|
14875
|
+
}), object({
|
|
14876
|
+
kind: literal("refused"),
|
|
14877
|
+
/** `no-zone` is ours; the rest are the runner's own refusal vocabulary. */
|
|
14878
|
+
reason: string(),
|
|
14879
|
+
detail: string().optional()
|
|
14880
|
+
})]);
|
|
14881
|
+
/** The outcome of a lifecycle move owned by the retrain page. */
|
|
14882
|
+
var RetrainTransitionResultSchema = object({
|
|
14883
|
+
trackId: string(),
|
|
14884
|
+
/** Where the track ended up, whatever happened. */
|
|
14885
|
+
retrainStatus: RetrainStatusSchema,
|
|
14886
|
+
/** `false` ⇒ the move was refused or was a no-op; `reason` says which. */
|
|
14887
|
+
changed: boolean(),
|
|
14888
|
+
reason: _enum([
|
|
14889
|
+
"unknown-track",
|
|
14890
|
+
"no-frames-copied",
|
|
14891
|
+
"not-staging",
|
|
14892
|
+
"not-trained",
|
|
14893
|
+
"unchanged"
|
|
14894
|
+
]).optional()
|
|
14895
|
+
});
|
|
15138
14896
|
var DEFAULT_EVENT_QUERY_LIMIT = 1e3;
|
|
15139
14897
|
var MAX_EVENT_QUERY_LIMIT = 5e3;
|
|
15140
14898
|
var DeviceEventQueryInput = object({
|
|
@@ -15189,13 +14947,14 @@ var KeyEventSchema = object({
|
|
|
15189
14947
|
/** Track start time (firstSeen). */
|
|
15190
14948
|
timestamp: number(),
|
|
15191
14949
|
className: string(),
|
|
15192
|
-
|
|
14950
|
+
...TieredLabelFields,
|
|
15193
14951
|
importance: number(),
|
|
15194
14952
|
/** Highest-confidence ObjectEvent id for the track (empty when none). */
|
|
15195
14953
|
bestEventId: string(),
|
|
15196
14954
|
/** Track lifetime in ms (lastSeen - firstSeen). */
|
|
15197
14955
|
windowMs: number().optional(),
|
|
15198
|
-
...TrackFlagFields
|
|
14956
|
+
...TrackFlagFields,
|
|
14957
|
+
...TrackRetrainFields
|
|
15199
14958
|
});
|
|
15200
14959
|
object({
|
|
15201
14960
|
trackId: string(),
|
|
@@ -15456,6 +15215,85 @@ DeviceType.Camera, method(object({ deviceId: number() }), array(TrackSchema).rea
|
|
|
15456
15215
|
}), method(OpsLogQueryInputSchema, array(OpsLogEntrySchema).readonly(), {
|
|
15457
15216
|
kind: "query",
|
|
15458
15217
|
auth: "admin"
|
|
15218
|
+
}), method(object({ deviceIds: array(number()).optional() }), TrainingExportSummarySchema, {
|
|
15219
|
+
kind: "query",
|
|
15220
|
+
auth: "admin"
|
|
15221
|
+
}), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
|
|
15222
|
+
kind: "query",
|
|
15223
|
+
auth: "admin"
|
|
15224
|
+
}), method(object({
|
|
15225
|
+
/** Empty ⇒ every camera that has staging tracks. A LIST, not a single
|
|
15226
|
+
* `deviceId`, deliberately: `deviceId` would make this device-bound and
|
|
15227
|
+
* route it at one camera's owner, and "every camera" would stop being
|
|
15228
|
+
* expressible at all. */
|
|
15229
|
+
deviceIds: array(number()).optional(),
|
|
15230
|
+
limit: number().int().min(1).max(500).optional()
|
|
15231
|
+
}), array(RetrainTrackSchema).readonly(), {
|
|
15232
|
+
kind: "query",
|
|
15233
|
+
auth: "admin"
|
|
15234
|
+
}), method(object({ trackId: string() }), RetrainFrameListSchema, {
|
|
15235
|
+
kind: "query",
|
|
15236
|
+
auth: "admin"
|
|
15237
|
+
}), method(object({
|
|
15238
|
+
deviceId: number(),
|
|
15239
|
+
trackId: string(),
|
|
15240
|
+
mediaKeys: array(string()).min(1)
|
|
15241
|
+
}), RetrainFrameSelectionSchema, {
|
|
15242
|
+
kind: "mutation",
|
|
15243
|
+
auth: "admin"
|
|
15244
|
+
}), method(object({
|
|
15245
|
+
deviceId: number(),
|
|
15246
|
+
trackId: string(),
|
|
15247
|
+
frameId: string()
|
|
15248
|
+
}), object({
|
|
15249
|
+
removed: boolean(),
|
|
15250
|
+
removedAnnotations: number().int()
|
|
15251
|
+
}), {
|
|
15252
|
+
kind: "mutation",
|
|
15253
|
+
auth: "admin"
|
|
15254
|
+
}), method(object({ frameId: string() }), object({
|
|
15255
|
+
base64: string(),
|
|
15256
|
+
width: number().int(),
|
|
15257
|
+
height: number().int()
|
|
15258
|
+
}), {
|
|
15259
|
+
kind: "query",
|
|
15260
|
+
auth: "admin"
|
|
15261
|
+
}), method(object({
|
|
15262
|
+
deviceId: number(),
|
|
15263
|
+
trackId: string(),
|
|
15264
|
+
frameId: string(),
|
|
15265
|
+
subject: RetrainAssistSubjectSchema,
|
|
15266
|
+
/** Which node runs it. Absent ⇒ wherever an unowned call lands. */
|
|
15267
|
+
nodeId: string().optional()
|
|
15268
|
+
}), RetrainAssistResultSchema, {
|
|
15269
|
+
kind: "mutation",
|
|
15270
|
+
auth: "admin"
|
|
15271
|
+
}), method(object({ trackId: string() }), array(RetrainAnnotationSchema).readonly(), {
|
|
15272
|
+
kind: "query",
|
|
15273
|
+
auth: "admin"
|
|
15274
|
+
}), method(object({
|
|
15275
|
+
deviceId: number(),
|
|
15276
|
+
trackId: string(),
|
|
15277
|
+
frameId: string(),
|
|
15278
|
+
annotations: array(RetrainAnnotationDraftSchema)
|
|
15279
|
+
}), array(RetrainAnnotationSchema).readonly(), {
|
|
15280
|
+
kind: "mutation",
|
|
15281
|
+
auth: "admin"
|
|
15282
|
+
}), method(object({
|
|
15283
|
+
deviceId: number(),
|
|
15284
|
+
trackId: string()
|
|
15285
|
+
}), RetrainTransitionResultSchema, {
|
|
15286
|
+
kind: "mutation",
|
|
15287
|
+
auth: "admin"
|
|
15288
|
+
}), method(object({
|
|
15289
|
+
deviceId: number(),
|
|
15290
|
+
trackId: string()
|
|
15291
|
+
}), RetrainTransitionResultSchema, {
|
|
15292
|
+
kind: "mutation",
|
|
15293
|
+
auth: "admin"
|
|
15294
|
+
}), method(object({ deviceIds: array(number()).optional() }), object({ url: string() }), {
|
|
15295
|
+
kind: "query",
|
|
15296
|
+
auth: "admin"
|
|
15459
15297
|
}), method(object({
|
|
15460
15298
|
eventId: string(),
|
|
15461
15299
|
kind: MediaFileKindEnum.optional()
|
|
@@ -16068,6 +15906,22 @@ var DetailResultSchema = object({
|
|
|
16068
15906
|
bbox: NativeCropBboxSchema.optional(),
|
|
16069
15907
|
embedding: string().optional(),
|
|
16070
15908
|
label: string().optional(),
|
|
15909
|
+
/**
|
|
15910
|
+
* The tier `label` occupies, copied VERBATIM from the producing step's
|
|
15911
|
+
* `StepDefinition.labelTier` (roadmap 4g). Present only when `label` is.
|
|
15912
|
+
*
|
|
15913
|
+
* It rides the wire rather than being resolved by the consumer because the
|
|
15914
|
+
* declaration lives with the step definition, which only the executing node
|
|
15915
|
+
* has: post-analysis holds no step registry, and re-deriving the tier from
|
|
15916
|
+
* `className` there would be exactly the inference this model exists to
|
|
15917
|
+
* forbid. A `label` that arrives WITHOUT this field is refused by the write
|
|
15918
|
+
* rule and logged (`label tier undeclared`) — an older runner therefore
|
|
15919
|
+
* stops enriching rather than guessing, which is why addon-pipeline is
|
|
15920
|
+
* deployed BEFORE addon-post-analysis.
|
|
15921
|
+
*/
|
|
15922
|
+
labelTier: union([literal(1), literal(2)]).optional(),
|
|
15923
|
+
/** Model that produced `label` — carried into the tier's attribution. */
|
|
15924
|
+
labelModelId: string().optional(),
|
|
16071
15925
|
alignedCropJpeg: string().optional(),
|
|
16072
15926
|
/** Face short side (px) measured on the NATIVE crop surface. The `bbox`
|
|
16073
15927
|
* above is detection-frame px (≈6× smaller on a 4K camera) — min-face-size
|
|
@@ -16775,6 +16629,23 @@ var CameraRecordingStatusSchema = object({
|
|
|
16775
16629
|
active: boolean(),
|
|
16776
16630
|
storageBytes: number()
|
|
16777
16631
|
});
|
|
16632
|
+
/** One stage of the fan-out that could NOT be read, and how long it cost. */
|
|
16633
|
+
var CameraStatusDegradationSchema = object({
|
|
16634
|
+
stage: _enum([
|
|
16635
|
+
"source",
|
|
16636
|
+
"broker",
|
|
16637
|
+
"detection",
|
|
16638
|
+
"recording",
|
|
16639
|
+
"switches"
|
|
16640
|
+
]),
|
|
16641
|
+
reason: _enum([
|
|
16642
|
+
"timeout",
|
|
16643
|
+
"error",
|
|
16644
|
+
"partial"
|
|
16645
|
+
]),
|
|
16646
|
+
/** Wall-clock ms spent on the stage before it was abandoned. */
|
|
16647
|
+
elapsedMs: number()
|
|
16648
|
+
});
|
|
16778
16649
|
/**
|
|
16779
16650
|
* Aggregated per-camera pipeline status — server-composed, single call.
|
|
16780
16651
|
*
|
|
@@ -16805,9 +16676,28 @@ var CameraStatusSchema = object({
|
|
|
16805
16676
|
* differently — a quiet camera that looks identical to a dead one is the
|
|
16806
16677
|
* silence-reads-as-never-happened trap this repo keeps paying for.
|
|
16807
16678
|
*
|
|
16808
|
-
* Empty when nothing is off
|
|
16679
|
+
* Empty when nothing is off, and never contains a switch no provider offers
|
|
16680
|
+
* — but an empty list is only a POSITIVE claim when `degraded` does not name
|
|
16681
|
+
* `'switches'`. When it does, the switch set could not be read and nothing
|
|
16682
|
+
* here may be rendered as "the operator turned nothing off": that is the
|
|
16683
|
+
* D62 failure (a camera we could not read painted as broken) in the very
|
|
16684
|
+
* field that exists to prevent it.
|
|
16809
16685
|
*/
|
|
16810
16686
|
switchedOff: array(CameraSwitchIdSchema).readonly(),
|
|
16687
|
+
/**
|
|
16688
|
+
* Stages of the bounded fan-out that were CUT SHORT — a timeout or a
|
|
16689
|
+
* rejection — and whose block is therefore `null` because we could not
|
|
16690
|
+
* READ it, not because there is nothing there.
|
|
16691
|
+
*
|
|
16692
|
+
* Without this, three different facts arrive as the same `null`: "the stage
|
|
16693
|
+
* timed out", "the stage failed", and "this camera legitimately has no
|
|
16694
|
+
* decoder / no recording". Every surface that draws a conclusion from a null
|
|
16695
|
+
* block (or from an empty `switchedOff`) must consult this first; a stage
|
|
16696
|
+
* named here supports no conclusion at all, only "unknown".
|
|
16697
|
+
*
|
|
16698
|
+
* Empty on a clean read — the overwhelmingly common case.
|
|
16699
|
+
*/
|
|
16700
|
+
degraded: array(CameraStatusDegradationSchema).readonly(),
|
|
16811
16701
|
/** Unix timestamp (ms) when this snapshot was composed server-side. */
|
|
16812
16702
|
fetchedAt: number()
|
|
16813
16703
|
});
|
|
@@ -17415,6 +17305,10 @@ var SsoBridgeClaimsSchema = object({
|
|
|
17415
17305
|
integrationId: string().optional(),
|
|
17416
17306
|
/** JWT ID — unique per issued code; consumed-set enforces single-use. */
|
|
17417
17307
|
jti: string().optional(),
|
|
17308
|
+
/** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
|
|
17309
|
+
* client. Its PRESENCE is what makes the verifier mandatory at exchange,
|
|
17310
|
+
* so the requirement travels with the code and not with mutable config. */
|
|
17311
|
+
codeChallenge: string().optional(),
|
|
17418
17312
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
17419
17313
|
* tokens so the verify path can check the session is not revoked. */
|
|
17420
17314
|
sessionId: string().optional()
|
|
@@ -17967,7 +17861,7 @@ var ClipPlaybackSchema = object({
|
|
|
17967
17861
|
playbackEndpoints: array(string()).optional(),
|
|
17968
17862
|
token: string().optional()
|
|
17969
17863
|
});
|
|
17970
|
-
method(object({
|
|
17864
|
+
DeviceType.Camera, method(object({
|
|
17971
17865
|
deviceId: number(),
|
|
17972
17866
|
since: number(),
|
|
17973
17867
|
until: number(),
|
|
@@ -20036,7 +19930,29 @@ var FaceInfoSchema = object({
|
|
|
20036
19930
|
recognizedIdentityId: string().optional(),
|
|
20037
19931
|
identityName: string().optional(),
|
|
20038
19932
|
assigned: boolean(),
|
|
19933
|
+
/**
|
|
19934
|
+
* The crop, inline, base64.
|
|
19935
|
+
*
|
|
19936
|
+
* **Prefer {@link cropUrl}.** At the 500 rows the Faces view asks for this
|
|
19937
|
+
* field alone is ~2.87 MiB, re-sent in full on every operator assign and
|
|
19938
|
+
* every 30 s poll, base64-inflated over the msgpack socket and held in the
|
|
19939
|
+
* query heap. It stays for callers that have not migrated; `includeCrops:
|
|
19940
|
+
* false` turns it off once they have.
|
|
19941
|
+
*/
|
|
20039
19942
|
base64: string().optional(),
|
|
19943
|
+
/**
|
|
19944
|
+
* Same crop, as a data-plane URL for `<img src>` — the move the admin
|
|
19945
|
+
* snapshot surfaces made on 2026-08-08.
|
|
19946
|
+
*
|
|
19947
|
+
* Served by the `event-media` plane, which resolves a raw MediaStore key and
|
|
19948
|
+
* is `access: 'authenticated'`: a bare `<img>` carries the `camstack_session`
|
|
19949
|
+
* cookie, so no header plumbing is needed. The bytes then ride the browser's
|
|
19950
|
+
* HTTP cache with an ETag and `immutable`, instead of the WebSocket.
|
|
19951
|
+
*
|
|
19952
|
+
* Absent when the face has no stored crop, or when the addon has no data
|
|
19953
|
+
* plane — callers fall back to {@link base64}.
|
|
19954
|
+
*/
|
|
19955
|
+
cropUrl: string().optional(),
|
|
20040
19956
|
/** Design B: the face bbox (pixel space) on the key frame — lets a detail
|
|
20041
19957
|
* view draw the box over the native `keyFrameMediaKey` frame. Absent on
|
|
20042
19958
|
* legacy rows written before design B. */
|
|
@@ -20101,7 +20017,23 @@ method(_void(), array(IdentitySchema).readonly()), method(object({ name: string(
|
|
|
20101
20017
|
auth: "admin"
|
|
20102
20018
|
}), method(object({
|
|
20103
20019
|
limit: number().int().positive().optional(),
|
|
20104
|
-
filter: FaceFilterEnum.optional()
|
|
20020
|
+
filter: FaceFilterEnum.optional(),
|
|
20021
|
+
/**
|
|
20022
|
+
* Inline the base64 crop on every row. Default `true` — the existing
|
|
20023
|
+
* behaviour, kept so no caller breaks.
|
|
20024
|
+
*
|
|
20025
|
+
* Set `false` once the caller renders {@link FaceInfo.cropUrl}: that
|
|
20026
|
+
* drops ~2.87 MiB per 500-row page to a few KiB of metadata and lets
|
|
20027
|
+
* the browser cache the images.
|
|
20028
|
+
*
|
|
20029
|
+
* **This is an INPUT field, so it does not reach the addon until the
|
|
20030
|
+
* next train.** The hub router validates cap inputs against its own
|
|
20031
|
+
* compiled Zod, which strips a key it does not know — verified today
|
|
20032
|
+
* on the OUTPUT side, where an additive field DOES arrive immediately
|
|
20033
|
+
* (`Track.hasFace`). Until the train ships, sending `false` is
|
|
20034
|
+
* harmless and simply keeps the crops inline.
|
|
20035
|
+
*/
|
|
20036
|
+
includeCrops: boolean().optional()
|
|
20105
20037
|
}).optional(), array(FaceInfoSchema).readonly()), method(object({
|
|
20106
20038
|
deviceId: number().int(),
|
|
20107
20039
|
trackId: string()
|
|
@@ -22267,6 +22199,173 @@ setOverlay: method(object({
|
|
|
22267
22199
|
}] }
|
|
22268
22200
|
};
|
|
22269
22201
|
/**
|
|
22202
|
+
* `osd-manager` — the ORCHESTRATOR over the device-scope `osd` cap.
|
|
22203
|
+
*
|
|
22204
|
+
* The `osd` cap is the firmware contract: it probes a camera's overlay
|
|
22205
|
+
* SLOTS and writes literal text into one. It has no idea WHERE that text
|
|
22206
|
+
* comes from, and it must not — a driver that grew a "show the temperature
|
|
22207
|
+
* here" feature would grow it once per vendor.
|
|
22208
|
+
*
|
|
22209
|
+
* This cap owns the other half: a per-(camera, slot) BINDING that says
|
|
22210
|
+
* which value feeds the slot, how it is formatted, and under which
|
|
22211
|
+
* conditions it is shown at all. One addon renders every binding on every
|
|
22212
|
+
* camera, so a new source costs zero driver code.
|
|
22213
|
+
*
|
|
22214
|
+
* Three deliberate choices, each with a rejected alternative:
|
|
22215
|
+
*
|
|
22216
|
+
* 1. A source is `(capName, valuePath)` over the kernel's device
|
|
22217
|
+
* runtime-state mirror — NOT a closed enum of source kinds. Every
|
|
22218
|
+
* cap-keyed slice a device publishes is bindable the day the cap
|
|
22219
|
+
* ships. The rejected alternative (one enum member per source, with
|
|
22220
|
+
* a resolver branch each) is what makes "add the humidity too" a
|
|
22221
|
+
* code change.
|
|
22222
|
+
* 2. The display gate reuses `NcConditionsSchema` verbatim — the
|
|
22223
|
+
* notification centre's condition vocabulary — rather than a parallel
|
|
22224
|
+
* model. An operator who has learned one condition editor has learned
|
|
22225
|
+
* both.
|
|
22226
|
+
* 3. Because the renderer's facts are device STATE and not a detection
|
|
22227
|
+
* record, only a SUBSET of that vocabulary can be answered here.
|
|
22228
|
+
* `setSlotBinding` REJECTS the rest at write time (see
|
|
22229
|
+
* `getConditionSupport`). It does not accept-then-fail-closed: a
|
|
22230
|
+
* condition that can never be true renders a permanently blank
|
|
22231
|
+
* overlay, and a blank overlay looks exactly like a broken camera.
|
|
22232
|
+
*/
|
|
22233
|
+
/** Where a slot's value comes from. */
|
|
22234
|
+
var OsdSourceSchema = discriminatedUnion("kind", [
|
|
22235
|
+
object({
|
|
22236
|
+
kind: literal("static"),
|
|
22237
|
+
text: string().max(64)
|
|
22238
|
+
}),
|
|
22239
|
+
object({
|
|
22240
|
+
kind: literal("clock"),
|
|
22241
|
+
/** Token pattern: `YYYY MM DD HH mm ss`. Everything else is literal. */
|
|
22242
|
+
pattern: string().min(1).max(32).default("HH:mm"),
|
|
22243
|
+
/** IANA zone. Omitted = the server's zone. */
|
|
22244
|
+
timezone: string().min(1).max(64).optional()
|
|
22245
|
+
}),
|
|
22246
|
+
object({
|
|
22247
|
+
kind: literal("device-state"),
|
|
22248
|
+
deviceId: number().int().optional(),
|
|
22249
|
+
capName: string().min(1).max(64),
|
|
22250
|
+
/** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
|
|
22251
|
+
valuePath: string().min(1).max(64)
|
|
22252
|
+
})
|
|
22253
|
+
]);
|
|
22254
|
+
var OsdSlotBindingSchema = object({
|
|
22255
|
+
/** Off = the manager stops driving this slot. It does NOT clear it. */
|
|
22256
|
+
enabled: boolean().default(true),
|
|
22257
|
+
source: OsdSourceSchema,
|
|
22258
|
+
/** `${value}` and `${unit}` are substituted; every occurrence. */
|
|
22259
|
+
template: string().max(96).default("${value}"),
|
|
22260
|
+
/** Truncate with an ellipsis past this length. Absent = no limit. */
|
|
22261
|
+
maxCharacters: number().int().min(4).max(64).optional(),
|
|
22262
|
+
/**
|
|
22263
|
+
* Decimal places for a numeric value. `0` yields an integer — the
|
|
22264
|
+
* documented workaround for firmwares that reject `.` in overlay text.
|
|
22265
|
+
*/
|
|
22266
|
+
maxDecimals: number().int().min(0).max(4).default(1),
|
|
22267
|
+
/** Appended via `${unit}`. The state mirror does not carry units. */
|
|
22268
|
+
unitLabel: string().max(8).optional(),
|
|
22269
|
+
/** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
|
|
22270
|
+
valueMap: record(string(), string()).optional(),
|
|
22271
|
+
/** Time windows in which the slot is shown. Absent = always. */
|
|
22272
|
+
schedule: NcScheduleSchema.optional(),
|
|
22273
|
+
/**
|
|
22274
|
+
* Display gate, in the notification centre's condition vocabulary.
|
|
22275
|
+
* Only the keys reported by `getConditionSupport` are accepted.
|
|
22276
|
+
*/
|
|
22277
|
+
conditions: NcConditionsSchema.optional(),
|
|
22278
|
+
/** Rendered when the gate is closed or the value unreadable. Empty = hide. */
|
|
22279
|
+
fallbackText: string().max(64).default("")
|
|
22280
|
+
});
|
|
22281
|
+
/** One camera slot, as the operator sees it: firmware truth + our binding. */
|
|
22282
|
+
var OsdSlotViewSchema = object({
|
|
22283
|
+
slotId: string(),
|
|
22284
|
+
kind: OsdOverlayKindEnum,
|
|
22285
|
+
/** Firmware refuses text edits (a timestamp, the channel name). */
|
|
22286
|
+
readOnly: boolean(),
|
|
22287
|
+
cameraEnabled: boolean(),
|
|
22288
|
+
cameraText: string().optional(),
|
|
22289
|
+
binding: OsdSlotBindingSchema.nullable()
|
|
22290
|
+
});
|
|
22291
|
+
/**
|
|
22292
|
+
* What happened to one slot on one render pass. `unchanged` exists so the
|
|
22293
|
+
* operator can tell "we are driving this and the value is steady" from
|
|
22294
|
+
* "we never got there" — and so the loop can prove it is not rewriting
|
|
22295
|
+
* identical text to the camera every tick.
|
|
22296
|
+
*/
|
|
22297
|
+
var OsdRenderOutcomeEnum = _enum([
|
|
22298
|
+
"written",
|
|
22299
|
+
"unchanged",
|
|
22300
|
+
"gated",
|
|
22301
|
+
"unreadable",
|
|
22302
|
+
"disabled",
|
|
22303
|
+
"unbound",
|
|
22304
|
+
"failed"
|
|
22305
|
+
]);
|
|
22306
|
+
var OsdRenderResultSchema = object({
|
|
22307
|
+
slotId: string(),
|
|
22308
|
+
outcome: OsdRenderOutcomeEnum,
|
|
22309
|
+
/** The text the slot should carry. Empty = the slot is switched off. */
|
|
22310
|
+
text: string(),
|
|
22311
|
+
/** Why, whenever the outcome is not a plain write. Never silent. */
|
|
22312
|
+
reason: string().optional()
|
|
22313
|
+
});
|
|
22314
|
+
var OsdSourceValueTypeEnum = _enum([
|
|
22315
|
+
"number",
|
|
22316
|
+
"boolean",
|
|
22317
|
+
"string",
|
|
22318
|
+
"enum"
|
|
22319
|
+
]);
|
|
22320
|
+
/**
|
|
22321
|
+
* One bindable value, derived from a cap's `runtimeState` schema — never
|
|
22322
|
+
* hand-listed. The editor renders from this, so a cap that ships a new
|
|
22323
|
+
* state field becomes bindable with no UI change.
|
|
22324
|
+
*/
|
|
22325
|
+
var OsdSourceOptionSchema = object({
|
|
22326
|
+
deviceId: number().int(),
|
|
22327
|
+
deviceName: string(),
|
|
22328
|
+
capName: string(),
|
|
22329
|
+
valuePath: string(),
|
|
22330
|
+
label: string(),
|
|
22331
|
+
valueType: OsdSourceValueTypeEnum,
|
|
22332
|
+
/** Present for `enum`; the editor offers these as `valueMap` keys. */
|
|
22333
|
+
enumValues: array(string()).readonly().optional()
|
|
22334
|
+
});
|
|
22335
|
+
method(object({ deviceId: number().int() }), object({
|
|
22336
|
+
supported: boolean(),
|
|
22337
|
+
slots: array(OsdSlotViewSchema)
|
|
22338
|
+
}), { auth: "admin" }), method(object({ deviceId: number().int() }), object({ sources: array(OsdSourceOptionSchema) }), { auth: "admin" }), method(object({}), object({
|
|
22339
|
+
supported: array(string()),
|
|
22340
|
+
catalog: array(NcConditionDescriptorSchema)
|
|
22341
|
+
}), { auth: "admin" }), method(object({
|
|
22342
|
+
deviceId: number().int(),
|
|
22343
|
+
slotId: string().min(1),
|
|
22344
|
+
binding: OsdSlotBindingSchema
|
|
22345
|
+
}), object({
|
|
22346
|
+
slot: OsdSlotViewSchema,
|
|
22347
|
+
render: OsdRenderResultSchema
|
|
22348
|
+
}), {
|
|
22349
|
+
kind: "mutation",
|
|
22350
|
+
auth: "admin"
|
|
22351
|
+
}), method(object({
|
|
22352
|
+
deviceId: number().int(),
|
|
22353
|
+
slotId: string().min(1)
|
|
22354
|
+
}), object({ success: literal(true) }), {
|
|
22355
|
+
kind: "mutation",
|
|
22356
|
+
auth: "admin"
|
|
22357
|
+
}), method(object({
|
|
22358
|
+
deviceId: number().int(),
|
|
22359
|
+
slotId: string().min(1),
|
|
22360
|
+
binding: OsdSlotBindingSchema.optional()
|
|
22361
|
+
}), OsdRenderResultSchema, {
|
|
22362
|
+
kind: "mutation",
|
|
22363
|
+
auth: "admin"
|
|
22364
|
+
}), method(object({ deviceId: number().int() }), object({ results: array(OsdRenderResultSchema) }), {
|
|
22365
|
+
kind: "mutation",
|
|
22366
|
+
auth: "admin"
|
|
22367
|
+
});
|
|
22368
|
+
/**
|
|
22270
22369
|
* Feeder connectivity / power status — mirrors the HA petkit device-status
|
|
22271
22370
|
* enum: `normal` (online, mains), `offline` (not reaching PetKit cloud),
|
|
22272
22371
|
* `on_batteries` (running on battery backup). `null` until first reported.
|
|
@@ -24631,13 +24730,18 @@ method(_void(), array(UserSummarySchema), { auth: "admin" }), method(CreateUserI
|
|
|
24631
24730
|
username: string(),
|
|
24632
24731
|
scopes: array(TokenScopeSchema),
|
|
24633
24732
|
redirectUri: string(),
|
|
24634
|
-
hubUrl: string()
|
|
24733
|
+
hubUrl: string(),
|
|
24734
|
+
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
24735
|
+
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
24736
|
+
codeChallenge: string().optional()
|
|
24635
24737
|
}), object({ code: string() }), {
|
|
24636
24738
|
kind: "mutation",
|
|
24637
24739
|
access: "create"
|
|
24638
24740
|
}), method(object({
|
|
24639
24741
|
code: string(),
|
|
24640
|
-
redirectUri: string()
|
|
24742
|
+
redirectUri: string(),
|
|
24743
|
+
/** PKCE verifier. REQUIRED when the code carries a challenge. */
|
|
24744
|
+
codeVerifier: string().optional()
|
|
24641
24745
|
}), object({
|
|
24642
24746
|
accessToken: string(),
|
|
24643
24747
|
refreshToken: string(),
|
|
@@ -25978,426 +26082,1096 @@ var BaseDevice = class {
|
|
|
25978
26082
|
* DO probe override this and write their own `feature-probe` slice (including
|
|
25979
26083
|
* `lastProbedAt`) once their probe actually succeeds.
|
|
25980
26084
|
*/
|
|
25981
|
-
async onProbe() {
|
|
25982
|
-
const base = this.runtimeState.getCapState("feature-probe") ?? {
|
|
25983
|
-
flags: {},
|
|
25984
|
-
deviceType: null,
|
|
25985
|
-
model: null,
|
|
25986
|
-
channelCount: null,
|
|
25987
|
-
lastProbedAt: 0,
|
|
25988
|
-
lastFetchedAt: 0
|
|
26085
|
+
async onProbe() {
|
|
26086
|
+
const base = this.runtimeState.getCapState("feature-probe") ?? {
|
|
26087
|
+
flags: {},
|
|
26088
|
+
deviceType: null,
|
|
26089
|
+
model: null,
|
|
26090
|
+
channelCount: null,
|
|
26091
|
+
lastProbedAt: 0,
|
|
26092
|
+
lastFetchedAt: 0
|
|
26093
|
+
};
|
|
26094
|
+
this.runtimeState.setCapState("feature-probe", {
|
|
26095
|
+
...base,
|
|
26096
|
+
lastProbedAt: Date.now()
|
|
26097
|
+
});
|
|
26098
|
+
}
|
|
26099
|
+
/**
|
|
26100
|
+
* Phase 5 — fired after the device + its accessories are registered.
|
|
26101
|
+
* Drivers publish streams to the broker, kick off background tasks,
|
|
26102
|
+
* or subscribe to lib events that need a fully-registered device id.
|
|
26103
|
+
*
|
|
26104
|
+
* Default: no-op.
|
|
26105
|
+
*
|
|
26106
|
+
* RENAMED FROM `onCreated` (which still exists for back-compat in this
|
|
26107
|
+
* pass). The new name reflects the post-probe, post-accessory contract.
|
|
26108
|
+
*/
|
|
26109
|
+
async onActivate() {}
|
|
26110
|
+
/**
|
|
26111
|
+
* Re-run the probe + reconcile accessories + refresh features meta.
|
|
26112
|
+
* Drivers call this when device-side state changes (battery cam wakes,
|
|
26113
|
+
* firmware update, manual operator trigger).
|
|
26114
|
+
*
|
|
26115
|
+
* The kernel injects `_kernelReprobe` on registration so this method
|
|
26116
|
+
* delegates to the same orchestrator that runs the boot-time phase
|
|
26117
|
+
* 3 + 4 sequence. Drivers should NOT override this — they override
|
|
26118
|
+
* `onProbe()` instead.
|
|
26119
|
+
*/
|
|
26120
|
+
async reprobe() {
|
|
26121
|
+
if (this._kernelReprobe) await this._kernelReprobe();
|
|
26122
|
+
else await this.onProbe();
|
|
26123
|
+
}
|
|
26124
|
+
/**
|
|
26125
|
+
* Kernel-injected callback that runs the full post-probe orchestration
|
|
26126
|
+
* (onProbe → registerDevice meta refresh → accessory reconciliation).
|
|
26127
|
+
* Set by `device-cap-proxy.register()`. Drivers should not touch this
|
|
26128
|
+
* directly — call `reprobe()` instead.
|
|
26129
|
+
*/
|
|
26130
|
+
_kernelReprobe;
|
|
26131
|
+
/**
|
|
26132
|
+
* Declare accessory child devices the kernel should auto-spawn
|
|
26133
|
+
* after `onProbe()` resolves. Each spec fully describes one child
|
|
26134
|
+
* — stableId suffix (deterministic per kind for restore-safety),
|
|
26135
|
+
* meta (type / name / location), config (initial blob the child
|
|
26136
|
+
* self-hydrates), and a factory that constructs the concrete
|
|
26137
|
+
* class with whatever closure-captured refs it needs (typically
|
|
26138
|
+
* `this` for the parent reference).
|
|
26139
|
+
*
|
|
26140
|
+
* The kernel handles the rest: allocateDeviceId, persistInitialConfig
|
|
26141
|
+
* (skipped on restore when the row already exists),
|
|
26142
|
+
* persistInitialMeta, createContext, factory invocation, register,
|
|
26143
|
+
* and recursive lifecycle (probe + accessories + activate).
|
|
26144
|
+
*
|
|
26145
|
+
* Implementations should derive children from
|
|
26146
|
+
* `this.runtimeState.getCapState('feature-probe')` (post-probe truth).
|
|
26147
|
+
* Drivers can use the `getProbeFlags()` helper to read the flag bag
|
|
26148
|
+
* with a typed cast.
|
|
26149
|
+
*
|
|
26150
|
+
* Default: no children.
|
|
26151
|
+
*/
|
|
26152
|
+
getAccessoryChildren() {
|
|
26153
|
+
return [];
|
|
26154
|
+
}
|
|
26155
|
+
/**
|
|
26156
|
+
* Read the current feature-probe flag bag with a typed cast. Helper
|
|
26157
|
+
* for `getAccessoryChildren()` and `features` getters that derive
|
|
26158
|
+
* outputs from the probe results.
|
|
26159
|
+
*/
|
|
26160
|
+
getProbeFlags() {
|
|
26161
|
+
return this.runtimeState.getCapState("feature-probe")?.flags ?? {};
|
|
26162
|
+
}
|
|
26163
|
+
/**
|
|
26164
|
+
* Returns true once `onProbe` has completed at least once
|
|
26165
|
+
* (`lastProbedAt > 0`). Drivers gate `getAccessoryChildren()` on this
|
|
26166
|
+
* to avoid spawning stale accessories on a fresh device whose probe
|
|
26167
|
+
* hasn't landed yet.
|
|
26168
|
+
*/
|
|
26169
|
+
hasProbed() {
|
|
26170
|
+
return (this.runtimeState.getCapState("feature-probe")?.lastProbedAt ?? 0) > 0;
|
|
26171
|
+
}
|
|
26172
|
+
};
|
|
26173
|
+
/**
|
|
26174
|
+
* Convert an IDevice to the flat DeviceSummary shape expected by the
|
|
26175
|
+
* device-provider cap router. Shared across all providers.
|
|
26176
|
+
*/
|
|
26177
|
+
function toDeviceSummary(device, addonId) {
|
|
26178
|
+
const config = {};
|
|
26179
|
+
for (const entry of device.config.entries()) config[entry.key] = entry.value;
|
|
26180
|
+
return {
|
|
26181
|
+
id: device.id,
|
|
26182
|
+
stableId: device.stableId,
|
|
26183
|
+
addonId,
|
|
26184
|
+
type: String(device.type),
|
|
26185
|
+
name: device.name,
|
|
26186
|
+
parentDeviceId: device.parentDeviceId,
|
|
26187
|
+
online: device.online,
|
|
26188
|
+
features: [...device.features],
|
|
26189
|
+
config,
|
|
26190
|
+
sourceInfo: device.sourceInfo
|
|
26191
|
+
};
|
|
26192
|
+
}
|
|
26193
|
+
/**
|
|
26194
|
+
* Base class for device-provider addons (rtsp, onvif, frigate).
|
|
26195
|
+
*
|
|
26196
|
+
* Provides default implementations for the common device-provider cap
|
|
26197
|
+
* methods (`start`, `stop`, `getStatus`, `getDevices`, `supportsDiscovery`,
|
|
26198
|
+
* `supportsManualCreation`, `toDeviceSummary`). Subclasses override the
|
|
26199
|
+
* methods that differ per provider.
|
|
26200
|
+
*
|
|
26201
|
+
* @example
|
|
26202
|
+
* ```ts
|
|
26203
|
+
* class RtspProvider extends BaseDeviceProvider {
|
|
26204
|
+
* protected readonly addonId = 'provider-rtsp'
|
|
26205
|
+
* protected readonly providerName = 'RTSP'
|
|
26206
|
+
*
|
|
26207
|
+
* protected async onCreateDevice(input) { ... }
|
|
26208
|
+
* protected async onGetCreationSchema(type) { ... }
|
|
26209
|
+
* protected async onRestoreDevices(saved) { ... }
|
|
26210
|
+
* }
|
|
26211
|
+
* ```
|
|
26212
|
+
*/
|
|
26213
|
+
var BaseDeviceProvider = class extends BaseAddon {
|
|
26214
|
+
async onInitialize() {
|
|
26215
|
+
this.ctx.logger.info(`${this.providerName} Provider initialized`);
|
|
26216
|
+
return [{
|
|
26217
|
+
capability: deviceProviderCapability,
|
|
26218
|
+
provider: this
|
|
26219
|
+
}];
|
|
26220
|
+
}
|
|
26221
|
+
async onShutdown() {
|
|
26222
|
+
const devices = await this.ctx.kernel.devices?.getAll() ?? [];
|
|
26223
|
+
for (const device of devices) try {
|
|
26224
|
+
await this.ctx.kernel.devices?.decommission(device.id);
|
|
26225
|
+
} catch (err) {
|
|
26226
|
+
this.ctx.logger.warn(`${this.providerName}: decommission failed`, {
|
|
26227
|
+
tags: {
|
|
26228
|
+
deviceId: device.id,
|
|
26229
|
+
stableId: device.stableId
|
|
26230
|
+
},
|
|
26231
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
26232
|
+
});
|
|
26233
|
+
}
|
|
26234
|
+
this.ctx.logger.info(`${this.providerName} Provider shut down`, { meta: { decommissionedCount: devices.length } });
|
|
26235
|
+
}
|
|
26236
|
+
async start() {}
|
|
26237
|
+
async stop() {}
|
|
26238
|
+
async getStatus() {
|
|
26239
|
+
return {
|
|
26240
|
+
connected: true,
|
|
26241
|
+
deviceCount: (await this.ctx.kernel.devices?.getAll() ?? []).length
|
|
26242
|
+
};
|
|
26243
|
+
}
|
|
26244
|
+
async getDevices() {
|
|
26245
|
+
return (await this.ctx.kernel.devices?.getAll() ?? []).map((d) => ({
|
|
26246
|
+
id: d.stableId,
|
|
26247
|
+
name: d.name,
|
|
26248
|
+
type: String(d.type)
|
|
26249
|
+
}));
|
|
26250
|
+
}
|
|
26251
|
+
async supportsDiscovery() {
|
|
26252
|
+
return false;
|
|
26253
|
+
}
|
|
26254
|
+
async discoverDevices(_input) {
|
|
26255
|
+
return [];
|
|
26256
|
+
}
|
|
26257
|
+
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
26258
|
+
* Null = no extra params. Override in providers that support scoped scans. */
|
|
26259
|
+
async getDiscoveryParamsSchema() {
|
|
26260
|
+
return null;
|
|
26261
|
+
}
|
|
26262
|
+
/**
|
|
26263
|
+
* The DeviceType this provider creates via manual add — derived from the
|
|
26264
|
+
* `deviceClasses` map (first registered type). `null` when manual creation is
|
|
26265
|
+
* unsupported. Lets the Add-Device dialog pick the right type per provider.
|
|
26266
|
+
*/
|
|
26267
|
+
async getManualCreationType() {
|
|
26268
|
+
if (!await this.supportsManualCreation()) return { deviceType: null };
|
|
26269
|
+
return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
|
|
26270
|
+
}
|
|
26271
|
+
async adoptDiscoveredDevice(_input) {
|
|
26272
|
+
throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
|
|
26273
|
+
}
|
|
26274
|
+
async supportsManualCreation() {
|
|
26275
|
+
return true;
|
|
26276
|
+
}
|
|
26277
|
+
async getChildCreationSchema(input) {
|
|
26278
|
+
return this.onGetCreationSchema(input.type);
|
|
26279
|
+
}
|
|
26280
|
+
/**
|
|
26281
|
+
* Default kernel-orchestrated `createDevice` implementation. The
|
|
26282
|
+
* subclass's `onCreateDevice` returns a declarative
|
|
26283
|
+
* `CreateDeviceSpec` (`{meta, config}`) — this method handles
|
|
26284
|
+
* stableId generation, class lookup, kernel.devices.create
|
|
26285
|
+
* dispatch, and DeviceSummary mapping. Subclasses should NOT
|
|
26286
|
+
* override this method; override `onCreateDevice` and
|
|
26287
|
+
* `deviceClasses` instead.
|
|
26288
|
+
*/
|
|
26289
|
+
async createDevice(input) {
|
|
26290
|
+
const spec = await this.onCreateDevice(input.type, input.config);
|
|
26291
|
+
const Class = this.deviceClasses[spec.meta.type];
|
|
26292
|
+
if (!Class) throw new Error(`${this.providerName} provider: no device class registered for type "${spec.meta.type}" — add it to the deviceClasses map`);
|
|
26293
|
+
const stableId = this.generateStableId(spec.meta.type, spec.config);
|
|
26294
|
+
const device = await this.ctx.kernel.devices.create(stableId, Class, spec.config, null, spec.meta);
|
|
26295
|
+
if (spec.onAfterCreate) try {
|
|
26296
|
+
await spec.onAfterCreate(device);
|
|
26297
|
+
} catch (err) {
|
|
26298
|
+
this.ctx.logger.warn("createDevice: onAfterCreate hook threw — device is already registered", {
|
|
26299
|
+
tags: {
|
|
26300
|
+
deviceId: device.id,
|
|
26301
|
+
stableId
|
|
26302
|
+
},
|
|
26303
|
+
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
26304
|
+
});
|
|
26305
|
+
}
|
|
26306
|
+
return this.toSummary(device);
|
|
26307
|
+
}
|
|
26308
|
+
/**
|
|
26309
|
+
* Generate a stableId for a newly-created device. Default uses the
|
|
26310
|
+
* `${addonId}-${Date.now()}` pattern as a unique-but-opaque
|
|
26311
|
+
* fallback; any provider that has access to durable hardware
|
|
26312
|
+
* identity (UID, MAC, serial) should override and derive from it
|
|
26313
|
+
* so re-adding the same physical device reuses its persisted row.
|
|
26314
|
+
*
|
|
26315
|
+
* `config` is the parsed CreateDeviceSpec.config the subclass
|
|
26316
|
+
* returned from `onCreateDevice` — the override has access to
|
|
26317
|
+
* every operator-supplied + autodetect-resolved field. Optional
|
|
26318
|
+
* for back-compat: existing overrides that take only `type`
|
|
26319
|
+
* keep working unchanged.
|
|
26320
|
+
*/
|
|
26321
|
+
generateStableId(_type, _config) {
|
|
26322
|
+
return `${this.addonId}-${Date.now()}`;
|
|
26323
|
+
}
|
|
26324
|
+
async testCreationField(_input) {
|
|
26325
|
+
return {
|
|
26326
|
+
status: "ok",
|
|
26327
|
+
labels: ["probe not implemented"]
|
|
25989
26328
|
};
|
|
25990
|
-
|
|
25991
|
-
|
|
25992
|
-
|
|
25993
|
-
});
|
|
26329
|
+
}
|
|
26330
|
+
async restoreDevices(savedDevices) {
|
|
26331
|
+
await this.onRestoreDevices(savedDevices);
|
|
26332
|
+
if (savedDevices.length > 0) this.ctx.logger.info(`Restored ${savedDevices.length} ${this.providerName} device(s)`);
|
|
25994
26333
|
}
|
|
25995
26334
|
/**
|
|
25996
|
-
*
|
|
25997
|
-
* Drivers publish streams to the broker, kick off background tasks,
|
|
25998
|
-
* or subscribe to lib events that need a fully-registered device id.
|
|
26335
|
+
* Restore devices from persisted state. Two-pass:
|
|
25999
26336
|
*
|
|
26000
|
-
*
|
|
26337
|
+
* 1. **Top-level pass** — invokes `kernel.devices.create()` for every
|
|
26338
|
+
* `parentDeviceId === null` row using the `deviceClasses` map.
|
|
26339
|
+
* The kernel's register flow handles `getAccessoryChildren()` for
|
|
26340
|
+
* each parent (siren / floodlight / PIR / etc).
|
|
26001
26341
|
*
|
|
26002
|
-
*
|
|
26003
|
-
*
|
|
26004
|
-
|
|
26005
|
-
|
|
26006
|
-
|
|
26007
|
-
*
|
|
26008
|
-
*
|
|
26009
|
-
*
|
|
26342
|
+
* 2. **Hub-adopted children pass** — for rows with
|
|
26343
|
+
* `parentDeviceId !== null` whose `type` IS in `deviceClasses`
|
|
26344
|
+
* (e.g. Reolink hub-adopted cameras under an NVR), spawn them
|
|
26345
|
+
* explicitly with the persisted `parentDeviceId`. These are
|
|
26346
|
+
* NOT accessory children — they're first-class adopted devices
|
|
26347
|
+
* that just happen to have a parent. Without this pass, every
|
|
26348
|
+
* server restart would lose hub-adopted cameras (their type is
|
|
26349
|
+
* in `deviceClasses` but parent's `getAccessoryChildren` doesn't
|
|
26350
|
+
* spawn them — that callback is only for purpose-built
|
|
26351
|
+
* accessory roles).
|
|
26010
26352
|
*
|
|
26011
|
-
*
|
|
26012
|
-
*
|
|
26013
|
-
*
|
|
26014
|
-
* `
|
|
26353
|
+
* Rows whose `type` is NOT in `deviceClasses` are skipped — those
|
|
26354
|
+
* are accessory children (siren/light/sensor) that the kernel's
|
|
26355
|
+
* accessory-spawn flow handles via the parent's
|
|
26356
|
+
* `getAccessoryChildren()`. Override only when the default doesn't
|
|
26357
|
+
* fit.
|
|
26015
26358
|
*/
|
|
26016
|
-
async
|
|
26017
|
-
|
|
26018
|
-
|
|
26359
|
+
async onRestoreDevices(savedDevices) {
|
|
26360
|
+
const restored = /* @__PURE__ */ new Set();
|
|
26361
|
+
for (const saved of savedDevices) {
|
|
26362
|
+
if (saved.parentDeviceId !== null) continue;
|
|
26363
|
+
const Class = this.deviceClasses[saved.type];
|
|
26364
|
+
if (!Class) {
|
|
26365
|
+
this.ctx.logger.warn("No device class registered for restored type — skipping", {
|
|
26366
|
+
tags: { stableId: saved.stableId },
|
|
26367
|
+
meta: { type: saved.type }
|
|
26368
|
+
});
|
|
26369
|
+
continue;
|
|
26370
|
+
}
|
|
26371
|
+
try {
|
|
26372
|
+
await this.ctx.kernel.devices.create(saved.stableId, Class, {});
|
|
26373
|
+
restored.add(saved.id);
|
|
26374
|
+
} catch (err) {
|
|
26375
|
+
this.ctx.logger.warn("Failed to restore device", {
|
|
26376
|
+
tags: { stableId: saved.stableId },
|
|
26377
|
+
meta: {
|
|
26378
|
+
type: saved.type,
|
|
26379
|
+
error: err instanceof Error ? err.message : String(err)
|
|
26380
|
+
}
|
|
26381
|
+
});
|
|
26382
|
+
}
|
|
26383
|
+
}
|
|
26384
|
+
const childRows = savedDevices.filter((s) => s.parentDeviceId !== null);
|
|
26385
|
+
for (const saved of childRows) {
|
|
26386
|
+
const Class = this.deviceClasses[saved.type];
|
|
26387
|
+
if (!Class) continue;
|
|
26388
|
+
if (saved.parentDeviceId === null) continue;
|
|
26389
|
+
if (!restored.has(saved.parentDeviceId)) continue;
|
|
26390
|
+
try {
|
|
26391
|
+
await this.ctx.kernel.devices.create(saved.stableId, Class, {}, saved.parentDeviceId);
|
|
26392
|
+
restored.add(saved.id);
|
|
26393
|
+
} catch (err) {
|
|
26394
|
+
this.ctx.logger.warn("Failed to restore hub-adopted child", {
|
|
26395
|
+
tags: {
|
|
26396
|
+
stableId: saved.stableId,
|
|
26397
|
+
parentDeviceId: saved.parentDeviceId
|
|
26398
|
+
},
|
|
26399
|
+
meta: {
|
|
26400
|
+
type: saved.type,
|
|
26401
|
+
error: err instanceof Error ? err.message : String(err)
|
|
26402
|
+
}
|
|
26403
|
+
});
|
|
26404
|
+
}
|
|
26405
|
+
}
|
|
26019
26406
|
}
|
|
26407
|
+
/** Convert an IDevice to the flat DeviceSummary for the cap router. */
|
|
26408
|
+
toSummary(device) {
|
|
26409
|
+
return toDeviceSummary(device, this.addonId);
|
|
26410
|
+
}
|
|
26411
|
+
};
|
|
26412
|
+
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;
|
|
26413
|
+
new Set(Object.values(DeviceType));
|
|
26414
|
+
DeviceFeature.BatteryOperated;
|
|
26415
|
+
/** Reject after `ms`; always clears its own timer. */
|
|
26416
|
+
async function withTimeout$1(promise, ms, label) {
|
|
26417
|
+
let timer;
|
|
26418
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
26419
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error(`${label} timed out after ${String(ms)}ms`)), ms);
|
|
26420
|
+
});
|
|
26421
|
+
try {
|
|
26422
|
+
return await Promise.race([promise, timeout]);
|
|
26423
|
+
} finally {
|
|
26424
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
26425
|
+
}
|
|
26426
|
+
}
|
|
26427
|
+
/**
|
|
26428
|
+
* Start the reachability poll loop. Returns a handle whose `stop()` clears the
|
|
26429
|
+
* timer and prevents any further ticks. Start on device activation, stop on
|
|
26430
|
+
* device teardown (`removeDevice`) so no timer leaks.
|
|
26431
|
+
*/
|
|
26432
|
+
function startReachabilityPoll(options) {
|
|
26433
|
+
const intervalMs = options.intervalMs ?? 3e4;
|
|
26434
|
+
const failuresToOffline = options.failuresToOffline ?? 3;
|
|
26435
|
+
const probeTimeoutMs = options.probeTimeoutMs ?? 1e4;
|
|
26436
|
+
const runImmediately = options.runImmediately ?? true;
|
|
26437
|
+
let stopped = false;
|
|
26438
|
+
let running = false;
|
|
26439
|
+
let consecutiveFailures = 0;
|
|
26440
|
+
let timer;
|
|
26441
|
+
const tick = async () => {
|
|
26442
|
+
if (stopped) return;
|
|
26443
|
+
if (running) return;
|
|
26444
|
+
if (options.isEnabled && !options.isEnabled()) return;
|
|
26445
|
+
running = true;
|
|
26446
|
+
try {
|
|
26447
|
+
const reachable = await withTimeout$1(Promise.resolve().then(options.probe), probeTimeoutMs, "reachability probe");
|
|
26448
|
+
if (stopped) return;
|
|
26449
|
+
if (reachable) {
|
|
26450
|
+
consecutiveFailures = 0;
|
|
26451
|
+
options.setOnline(true);
|
|
26452
|
+
} else registerFailure("probe resolved unreachable");
|
|
26453
|
+
} catch (error) {
|
|
26454
|
+
if (stopped) return;
|
|
26455
|
+
registerFailure(error instanceof Error ? error.message : "probe threw");
|
|
26456
|
+
} finally {
|
|
26457
|
+
running = false;
|
|
26458
|
+
}
|
|
26459
|
+
};
|
|
26460
|
+
const registerFailure = (reason) => {
|
|
26461
|
+
consecutiveFailures += 1;
|
|
26462
|
+
options.logger?.debug("reachability probe failed", {
|
|
26463
|
+
reason,
|
|
26464
|
+
consecutiveFailures,
|
|
26465
|
+
failuresToOffline
|
|
26466
|
+
});
|
|
26467
|
+
if (consecutiveFailures >= failuresToOffline) options.setOnline(false);
|
|
26468
|
+
};
|
|
26469
|
+
timer = setInterval(() => {
|
|
26470
|
+
tick();
|
|
26471
|
+
}, intervalMs);
|
|
26472
|
+
if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
|
|
26473
|
+
if (runImmediately) tick();
|
|
26474
|
+
return { stop: () => {
|
|
26475
|
+
if (stopped) return;
|
|
26476
|
+
stopped = true;
|
|
26477
|
+
if (timer !== void 0) clearInterval(timer);
|
|
26478
|
+
timer = void 0;
|
|
26479
|
+
} };
|
|
26480
|
+
}
|
|
26481
|
+
var LAST_FETCHED_FIELD = "lastFetchedAt";
|
|
26482
|
+
function createRuntimeStateBridge(params) {
|
|
26483
|
+
const { runtimeState, cap, ownDeviceId, refresh, staleMs, empty, logger } = params;
|
|
26484
|
+
const missCooldownMs = params.refreshMissCooldownMs ?? 6e4;
|
|
26485
|
+
/** Epoch ms until which a refresh is not re-attempted. 0 = no cooldown. */
|
|
26486
|
+
let missCooldownUntil = 0;
|
|
26487
|
+
const readFetchedAt = () => {
|
|
26488
|
+
const value = runtimeState.getCapState(cap.name)?.[LAST_FETCHED_FIELD];
|
|
26489
|
+
return typeof value === "number" ? value : 0;
|
|
26490
|
+
};
|
|
26020
26491
|
/**
|
|
26021
|
-
*
|
|
26022
|
-
*
|
|
26023
|
-
*
|
|
26024
|
-
*
|
|
26025
|
-
*/
|
|
26026
|
-
_kernelReprobe;
|
|
26027
|
-
/**
|
|
26028
|
-
* Declare accessory child devices the kernel should auto-spawn
|
|
26029
|
-
* after `onProbe()` resolves. Each spec fully describes one child
|
|
26030
|
-
* — stableId suffix (deterministic per kind for restore-safety),
|
|
26031
|
-
* meta (type / name / location), config (initial blob the child
|
|
26032
|
-
* self-hydrates), and a factory that constructs the concrete
|
|
26033
|
-
* class with whatever closure-captured refs it needs (typically
|
|
26034
|
-
* `this` for the parent reference).
|
|
26035
|
-
*
|
|
26036
|
-
* The kernel handles the rest: allocateDeviceId, persistInitialConfig
|
|
26037
|
-
* (skipped on restore when the row already exists),
|
|
26038
|
-
* persistInitialMeta, createContext, factory invocation, register,
|
|
26039
|
-
* and recursive lifecycle (probe + accessories + activate).
|
|
26040
|
-
*
|
|
26041
|
-
* Implementations should derive children from
|
|
26042
|
-
* `this.runtimeState.getCapState('feature-probe')` (post-probe truth).
|
|
26043
|
-
* Drivers can use the `getProbeFlags()` helper to read the flag bag
|
|
26044
|
-
* with a typed cast.
|
|
26045
|
-
*
|
|
26046
|
-
* Default: no children.
|
|
26492
|
+
* Stop re-reading this camera for `missCooldownMs`, and say so. The
|
|
26493
|
+
* warn is the ONLY line an operator gets for a cap that has silently
|
|
26494
|
+
* been answering with defaults, so it names the cap and carries
|
|
26495
|
+
* `tags.deviceId` — a miss is always asked per-camera.
|
|
26047
26496
|
*/
|
|
26048
|
-
|
|
26049
|
-
|
|
26497
|
+
const openMissCooldown = (err) => {
|
|
26498
|
+
missCooldownUntil = Date.now() + missCooldownMs;
|
|
26499
|
+
logger?.warn(`${cap.name}: refresh did not land — serving the last slice and not re-reading the camera for ${String(missCooldownMs)}ms`, {
|
|
26500
|
+
tags: { deviceId: ownDeviceId },
|
|
26501
|
+
meta: {
|
|
26502
|
+
cooldownMs: missCooldownMs,
|
|
26503
|
+
error: err === void 0 ? null : err instanceof Error ? err.message : String(err)
|
|
26504
|
+
}
|
|
26505
|
+
});
|
|
26506
|
+
};
|
|
26507
|
+
const ensureFresh = async () => {
|
|
26508
|
+
const slice = runtimeState.getCapState(cap.name);
|
|
26509
|
+
const fetchedAt = readFetchedAt();
|
|
26510
|
+
if (slice && Date.now() - fetchedAt <= staleMs) {
|
|
26511
|
+
missCooldownUntil = 0;
|
|
26512
|
+
return;
|
|
26513
|
+
}
|
|
26514
|
+
if (Date.now() < missCooldownUntil) return;
|
|
26515
|
+
try {
|
|
26516
|
+
await refresh();
|
|
26517
|
+
} catch (err) {
|
|
26518
|
+
openMissCooldown(err);
|
|
26519
|
+
throw err;
|
|
26520
|
+
}
|
|
26521
|
+
if (readFetchedAt() > fetchedAt) {
|
|
26522
|
+
if (missCooldownUntil !== 0) {
|
|
26523
|
+
missCooldownUntil = 0;
|
|
26524
|
+
logger?.info(`${cap.name}: refresh landed again — resuming normal polling`, { tags: { deviceId: ownDeviceId } });
|
|
26525
|
+
}
|
|
26526
|
+
return;
|
|
26527
|
+
}
|
|
26528
|
+
openMissCooldown(void 0);
|
|
26529
|
+
};
|
|
26530
|
+
const projectStatus = () => {
|
|
26531
|
+
const slice = runtimeState.getCapState(cap.name);
|
|
26532
|
+
if (!slice) return empty();
|
|
26533
|
+
const { [LAST_FETCHED_FIELD]: _omit, ...rest } = slice;
|
|
26534
|
+
return rest;
|
|
26535
|
+
};
|
|
26536
|
+
const getStatus = async ({ deviceId }) => {
|
|
26537
|
+
if (deviceId !== ownDeviceId) throw new Error(`${cap.name}: deviceId mismatch, expected ${ownDeviceId}, got ${deviceId}`);
|
|
26538
|
+
await ensureFresh();
|
|
26539
|
+
return projectStatus();
|
|
26540
|
+
};
|
|
26541
|
+
return {
|
|
26542
|
+
ensureFresh,
|
|
26543
|
+
getStatus
|
|
26544
|
+
};
|
|
26545
|
+
}
|
|
26546
|
+
/**
|
|
26547
|
+
* Error types for the safe expression engine. Two distinct classes so callers
|
|
26548
|
+
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
26549
|
+
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
26550
|
+
*/
|
|
26551
|
+
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
26552
|
+
* the failure is anchored to a character (author-facing inline feedback). */
|
|
26553
|
+
var ExpressionParseError = class extends Error {
|
|
26554
|
+
position;
|
|
26555
|
+
constructor(message, position) {
|
|
26556
|
+
super(message);
|
|
26557
|
+
this.name = "ExpressionParseError";
|
|
26558
|
+
this.position = position;
|
|
26050
26559
|
}
|
|
26051
|
-
|
|
26052
|
-
|
|
26053
|
-
|
|
26054
|
-
|
|
26055
|
-
|
|
26056
|
-
|
|
26057
|
-
|
|
26560
|
+
};
|
|
26561
|
+
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
26562
|
+
* result, unknown builtin, step-budget exceeded). */
|
|
26563
|
+
var ExpressionEvalError = class extends Error {
|
|
26564
|
+
constructor(message) {
|
|
26565
|
+
super(message);
|
|
26566
|
+
this.name = "ExpressionEvalError";
|
|
26058
26567
|
}
|
|
26059
|
-
|
|
26060
|
-
|
|
26061
|
-
|
|
26062
|
-
|
|
26063
|
-
|
|
26064
|
-
|
|
26065
|
-
|
|
26066
|
-
|
|
26568
|
+
};
|
|
26569
|
+
/**
|
|
26570
|
+
* Frozen, null-prototype builtin function table for the expression engine
|
|
26571
|
+
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
26572
|
+
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
26573
|
+
* own-property check against it.
|
|
26574
|
+
*
|
|
26575
|
+
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
26576
|
+
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
26577
|
+
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
26578
|
+
* (there is no `Object.prototype` in the chain), so those names are not
|
|
26579
|
+
* callable — they are simply "unknown function" at parse time.
|
|
26580
|
+
*
|
|
26581
|
+
* Every numeric argument is validated as a finite number and every numeric
|
|
26582
|
+
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
26583
|
+
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
26584
|
+
* closed rather than emitting a garbage value.
|
|
26585
|
+
*/
|
|
26586
|
+
function asFiniteNumber(value, name, index) {
|
|
26587
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
26588
|
+
return value;
|
|
26589
|
+
}
|
|
26590
|
+
function asString$1(value, name, index) {
|
|
26591
|
+
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
26592
|
+
return value;
|
|
26593
|
+
}
|
|
26594
|
+
function finiteResult(value, name) {
|
|
26595
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
26596
|
+
return value;
|
|
26597
|
+
}
|
|
26598
|
+
function allFiniteNumbers(args, name) {
|
|
26599
|
+
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
26600
|
+
}
|
|
26601
|
+
var INF = Number.POSITIVE_INFINITY;
|
|
26602
|
+
var table = {
|
|
26603
|
+
min: {
|
|
26604
|
+
minArgs: 1,
|
|
26605
|
+
maxArgs: INF,
|
|
26606
|
+
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
26607
|
+
},
|
|
26608
|
+
max: {
|
|
26609
|
+
minArgs: 1,
|
|
26610
|
+
maxArgs: INF,
|
|
26611
|
+
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
26612
|
+
},
|
|
26613
|
+
abs: {
|
|
26614
|
+
minArgs: 1,
|
|
26615
|
+
maxArgs: 1,
|
|
26616
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
26617
|
+
},
|
|
26618
|
+
floor: {
|
|
26619
|
+
minArgs: 1,
|
|
26620
|
+
maxArgs: 1,
|
|
26621
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
26622
|
+
},
|
|
26623
|
+
ceil: {
|
|
26624
|
+
minArgs: 1,
|
|
26625
|
+
maxArgs: 1,
|
|
26626
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
26627
|
+
},
|
|
26628
|
+
sqrt: {
|
|
26629
|
+
minArgs: 1,
|
|
26630
|
+
maxArgs: 1,
|
|
26631
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
26632
|
+
},
|
|
26633
|
+
round: {
|
|
26634
|
+
minArgs: 1,
|
|
26635
|
+
maxArgs: 2,
|
|
26636
|
+
apply: (args) => {
|
|
26637
|
+
const x = asFiniteNumber(args[0], "round", 0);
|
|
26638
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
26639
|
+
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
26640
|
+
const factor = 10 ** digits;
|
|
26641
|
+
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
26642
|
+
}
|
|
26643
|
+
},
|
|
26644
|
+
pow: {
|
|
26645
|
+
minArgs: 2,
|
|
26646
|
+
maxArgs: 2,
|
|
26647
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
26648
|
+
},
|
|
26649
|
+
clamp: {
|
|
26650
|
+
minArgs: 3,
|
|
26651
|
+
maxArgs: 3,
|
|
26652
|
+
apply: (args) => {
|
|
26653
|
+
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
26654
|
+
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
26655
|
+
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
26656
|
+
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
26657
|
+
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
26658
|
+
}
|
|
26659
|
+
},
|
|
26660
|
+
avg: {
|
|
26661
|
+
minArgs: 1,
|
|
26662
|
+
maxArgs: INF,
|
|
26663
|
+
apply: (args) => {
|
|
26664
|
+
const nums = allFiniteNumbers(args, "avg");
|
|
26665
|
+
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
26666
|
+
}
|
|
26667
|
+
},
|
|
26668
|
+
sum: {
|
|
26669
|
+
minArgs: 1,
|
|
26670
|
+
maxArgs: INF,
|
|
26671
|
+
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
26672
|
+
},
|
|
26673
|
+
coalesce: {
|
|
26674
|
+
minArgs: 1,
|
|
26675
|
+
maxArgs: INF,
|
|
26676
|
+
apply: (args) => {
|
|
26677
|
+
for (const a of args) if (a !== null) return a;
|
|
26678
|
+
return null;
|
|
26679
|
+
}
|
|
26680
|
+
},
|
|
26681
|
+
age: {
|
|
26682
|
+
minArgs: 2,
|
|
26683
|
+
maxArgs: 2,
|
|
26684
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
26685
|
+
},
|
|
26686
|
+
convert: {
|
|
26687
|
+
minArgs: 3,
|
|
26688
|
+
maxArgs: 3,
|
|
26689
|
+
apply: (args, hooks) => {
|
|
26690
|
+
const x = asFiniteNumber(args[0], "convert", 0);
|
|
26691
|
+
const from = asString$1(args[1], "convert", 1).trim();
|
|
26692
|
+
const to = asString$1(args[2], "convert", 2).trim();
|
|
26693
|
+
if (hooks.convert) {
|
|
26694
|
+
const out = hooks.convert(x, from, to);
|
|
26695
|
+
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
26696
|
+
return finiteResult(out, "convert");
|
|
26697
|
+
}
|
|
26698
|
+
if (from === to) return x;
|
|
26699
|
+
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
26700
|
+
}
|
|
26701
|
+
}
|
|
26702
|
+
};
|
|
26703
|
+
Object.freeze(Object.assign(Object.create(null), table));
|
|
26704
|
+
/** The set of valid builtin names — used by the parser to reject unknown
|
|
26705
|
+
* callees at parse time (immediate author feedback). */
|
|
26706
|
+
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
26707
|
+
/**
|
|
26708
|
+
* Resource-bound constants for the safe expression engine.
|
|
26709
|
+
*
|
|
26710
|
+
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
26711
|
+
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
26712
|
+
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
26713
|
+
* work a single author-supplied expression can request, so a hostile or
|
|
26714
|
+
* accidental pathological string can never spend unbounded CPU/memory.
|
|
26715
|
+
*/
|
|
26716
|
+
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
26717
|
+
* rejected without allocation. */
|
|
26718
|
+
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
26719
|
+
/** A legal binding / identifier name. */
|
|
26720
|
+
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
26721
|
+
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
26722
|
+
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
26723
|
+
var RESERVED_BINDING_NAMES = new Set([
|
|
26724
|
+
"now",
|
|
26725
|
+
"true",
|
|
26726
|
+
"false",
|
|
26727
|
+
"null"
|
|
26728
|
+
]);
|
|
26729
|
+
/**
|
|
26730
|
+
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
26731
|
+
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
26732
|
+
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
26733
|
+
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
26734
|
+
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
26735
|
+
* is a parse error with a source position, so member access / assignment /
|
|
26736
|
+
* template literals are lexically impossible.
|
|
26737
|
+
*/
|
|
26738
|
+
var KEYWORDS = new Set([
|
|
26739
|
+
"true",
|
|
26740
|
+
"false",
|
|
26741
|
+
"null"
|
|
26742
|
+
]);
|
|
26743
|
+
function isDigit(ch) {
|
|
26744
|
+
return ch >= "0" && ch <= "9";
|
|
26745
|
+
}
|
|
26746
|
+
function isIdentStart(ch) {
|
|
26747
|
+
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
26748
|
+
}
|
|
26749
|
+
function isIdentPart(ch) {
|
|
26750
|
+
return isIdentStart(ch) || isDigit(ch);
|
|
26751
|
+
}
|
|
26752
|
+
function isWhitespace(ch) {
|
|
26753
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
26754
|
+
}
|
|
26755
|
+
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
26756
|
+
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
26757
|
+
* string. */
|
|
26758
|
+
function tokenize(source) {
|
|
26759
|
+
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
26760
|
+
const tokens = [];
|
|
26761
|
+
let i = 0;
|
|
26762
|
+
const n = source.length;
|
|
26763
|
+
while (i < n) {
|
|
26764
|
+
const ch = source[i];
|
|
26765
|
+
if (isWhitespace(ch)) {
|
|
26766
|
+
i += 1;
|
|
26767
|
+
continue;
|
|
26768
|
+
}
|
|
26769
|
+
if (isDigit(ch)) {
|
|
26770
|
+
const start = i;
|
|
26771
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
26772
|
+
if (i < n && source[i] === ".") {
|
|
26773
|
+
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
26774
|
+
i += 1;
|
|
26775
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
26776
|
+
}
|
|
26777
|
+
const text = source.slice(start, i);
|
|
26778
|
+
const value = Number(text);
|
|
26779
|
+
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
26780
|
+
tokens.push({
|
|
26781
|
+
type: "number",
|
|
26782
|
+
value,
|
|
26783
|
+
pos: start
|
|
26784
|
+
});
|
|
26785
|
+
continue;
|
|
26786
|
+
}
|
|
26787
|
+
if (ch === "'" || ch === "\"") {
|
|
26788
|
+
const quote = ch;
|
|
26789
|
+
const start = i;
|
|
26790
|
+
i += 1;
|
|
26791
|
+
let out = "";
|
|
26792
|
+
let closed = false;
|
|
26793
|
+
while (i < n) {
|
|
26794
|
+
const c = source[i];
|
|
26795
|
+
if (c === "\\") {
|
|
26796
|
+
const next = i + 1 < n ? source[i + 1] : "";
|
|
26797
|
+
if (next === "\\" || next === "'" || next === "\"") {
|
|
26798
|
+
out += next;
|
|
26799
|
+
i += 2;
|
|
26800
|
+
continue;
|
|
26801
|
+
}
|
|
26802
|
+
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
26803
|
+
}
|
|
26804
|
+
if (c === quote) {
|
|
26805
|
+
closed = true;
|
|
26806
|
+
i += 1;
|
|
26807
|
+
break;
|
|
26808
|
+
}
|
|
26809
|
+
out += c;
|
|
26810
|
+
i += 1;
|
|
26811
|
+
}
|
|
26812
|
+
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
26813
|
+
tokens.push({
|
|
26814
|
+
type: "string",
|
|
26815
|
+
value: out,
|
|
26816
|
+
pos: start
|
|
26817
|
+
});
|
|
26818
|
+
continue;
|
|
26819
|
+
}
|
|
26820
|
+
if (isIdentStart(ch)) {
|
|
26821
|
+
const start = i;
|
|
26822
|
+
while (i < n && isIdentPart(source[i])) i += 1;
|
|
26823
|
+
const text = source.slice(start, i);
|
|
26824
|
+
if (KEYWORDS.has(text)) tokens.push({
|
|
26825
|
+
type: "keyword",
|
|
26826
|
+
keyword: keywordOf(text),
|
|
26827
|
+
pos: start
|
|
26828
|
+
});
|
|
26829
|
+
else tokens.push({
|
|
26830
|
+
type: "identifier",
|
|
26831
|
+
name: text,
|
|
26832
|
+
pos: start
|
|
26833
|
+
});
|
|
26834
|
+
continue;
|
|
26835
|
+
}
|
|
26836
|
+
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
26837
|
+
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
26838
|
+
tokens.push({
|
|
26839
|
+
type: "punct",
|
|
26840
|
+
punct: two,
|
|
26841
|
+
pos: i
|
|
26842
|
+
});
|
|
26843
|
+
i += 2;
|
|
26844
|
+
continue;
|
|
26845
|
+
}
|
|
26846
|
+
if (isSinglePunct(ch)) {
|
|
26847
|
+
tokens.push({
|
|
26848
|
+
type: "punct",
|
|
26849
|
+
punct: ch,
|
|
26850
|
+
pos: i
|
|
26851
|
+
});
|
|
26852
|
+
i += 1;
|
|
26853
|
+
continue;
|
|
26854
|
+
}
|
|
26855
|
+
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
26067
26856
|
}
|
|
26068
|
-
|
|
26069
|
-
|
|
26070
|
-
|
|
26071
|
-
|
|
26072
|
-
|
|
26073
|
-
|
|
26074
|
-
|
|
26075
|
-
|
|
26076
|
-
return
|
|
26077
|
-
|
|
26078
|
-
|
|
26079
|
-
|
|
26080
|
-
|
|
26081
|
-
name: device.name,
|
|
26082
|
-
parentDeviceId: device.parentDeviceId,
|
|
26083
|
-
online: device.online,
|
|
26084
|
-
features: [...device.features],
|
|
26085
|
-
config,
|
|
26086
|
-
sourceInfo: device.sourceInfo
|
|
26087
|
-
};
|
|
26857
|
+
tokens.push({
|
|
26858
|
+
type: "eof",
|
|
26859
|
+
pos: n
|
|
26860
|
+
});
|
|
26861
|
+
return tokens;
|
|
26862
|
+
}
|
|
26863
|
+
function keywordOf(text) {
|
|
26864
|
+
if (text === "true") return "true";
|
|
26865
|
+
if (text === "false") return "false";
|
|
26866
|
+
return "null";
|
|
26867
|
+
}
|
|
26868
|
+
function isSinglePunct(ch) {
|
|
26869
|
+
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
26088
26870
|
}
|
|
26089
26871
|
/**
|
|
26090
|
-
*
|
|
26091
|
-
*
|
|
26092
|
-
* Provides default implementations for the common device-provider cap
|
|
26093
|
-
* methods (`start`, `stop`, `getStatus`, `getDevices`, `supportsDiscovery`,
|
|
26094
|
-
* `supportsManualCreation`, `toDeviceSummary`). Subclasses override the
|
|
26095
|
-
* methods that differ per provider.
|
|
26872
|
+
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
26096
26873
|
*
|
|
26097
|
-
*
|
|
26098
|
-
*
|
|
26099
|
-
*
|
|
26100
|
-
*
|
|
26101
|
-
*
|
|
26874
|
+
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
26875
|
+
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
26876
|
+
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
26877
|
+
* string validated against the builtin table at parse time, so an unknown
|
|
26878
|
+
* function is rejected immediately (author feedback) and a persisted expression
|
|
26879
|
+
* that references a since-removed builtin degrades at read.
|
|
26102
26880
|
*
|
|
26103
|
-
*
|
|
26104
|
-
*
|
|
26105
|
-
* protected async onRestoreDevices(saved) { ... }
|
|
26106
|
-
* }
|
|
26107
|
-
* ```
|
|
26881
|
+
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
26882
|
+
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
26108
26883
|
*/
|
|
26109
|
-
|
|
26110
|
-
|
|
26111
|
-
|
|
26112
|
-
|
|
26113
|
-
|
|
26114
|
-
|
|
26115
|
-
|
|
26116
|
-
|
|
26117
|
-
|
|
26118
|
-
|
|
26119
|
-
|
|
26120
|
-
|
|
26121
|
-
|
|
26122
|
-
|
|
26123
|
-
|
|
26124
|
-
|
|
26125
|
-
|
|
26126
|
-
|
|
26127
|
-
|
|
26128
|
-
|
|
26129
|
-
|
|
26130
|
-
|
|
26884
|
+
/** Binary/logical operator precedence (higher binds tighter). */
|
|
26885
|
+
var BINARY_PRECEDENCE = {
|
|
26886
|
+
"||": 1,
|
|
26887
|
+
"&&": 2,
|
|
26888
|
+
"==": 3,
|
|
26889
|
+
"!=": 3,
|
|
26890
|
+
"<": 4,
|
|
26891
|
+
"<=": 4,
|
|
26892
|
+
">": 4,
|
|
26893
|
+
">=": 4,
|
|
26894
|
+
"+": 5,
|
|
26895
|
+
"-": 5,
|
|
26896
|
+
"*": 6,
|
|
26897
|
+
"/": 6,
|
|
26898
|
+
"%": 6
|
|
26899
|
+
};
|
|
26900
|
+
function isLogicalOp(op) {
|
|
26901
|
+
return op === "&&" || op === "||";
|
|
26902
|
+
}
|
|
26903
|
+
function isBinaryOp(op) {
|
|
26904
|
+
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
26905
|
+
}
|
|
26906
|
+
var Parser = class {
|
|
26907
|
+
tokens;
|
|
26908
|
+
pos = 0;
|
|
26909
|
+
nodeCount = 0;
|
|
26910
|
+
identifiers = /* @__PURE__ */ new Set();
|
|
26911
|
+
callees = /* @__PURE__ */ new Set();
|
|
26912
|
+
constructor(tokens) {
|
|
26913
|
+
this.tokens = tokens;
|
|
26131
26914
|
}
|
|
26132
|
-
|
|
26133
|
-
|
|
26134
|
-
|
|
26915
|
+
parse() {
|
|
26916
|
+
const ast = this.parseTernary();
|
|
26917
|
+
const tok = this.peek();
|
|
26918
|
+
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
26135
26919
|
return {
|
|
26136
|
-
|
|
26137
|
-
|
|
26920
|
+
ast,
|
|
26921
|
+
identifiers: this.identifiers,
|
|
26922
|
+
callees: this.callees,
|
|
26923
|
+
nodeCount: this.nodeCount
|
|
26138
26924
|
};
|
|
26139
26925
|
}
|
|
26140
|
-
|
|
26141
|
-
return
|
|
26142
|
-
id: d.stableId,
|
|
26143
|
-
name: d.name,
|
|
26144
|
-
type: String(d.type)
|
|
26145
|
-
}));
|
|
26146
|
-
}
|
|
26147
|
-
async supportsDiscovery() {
|
|
26148
|
-
return false;
|
|
26149
|
-
}
|
|
26150
|
-
async discoverDevices(_input) {
|
|
26151
|
-
return [];
|
|
26152
|
-
}
|
|
26153
|
-
/** Extra per-scan input form (e.g. a broadcast address for another subnet).
|
|
26154
|
-
* Null = no extra params. Override in providers that support scoped scans. */
|
|
26155
|
-
async getDiscoveryParamsSchema() {
|
|
26156
|
-
return null;
|
|
26157
|
-
}
|
|
26158
|
-
/**
|
|
26159
|
-
* The DeviceType this provider creates via manual add — derived from the
|
|
26160
|
-
* `deviceClasses` map (first registered type). `null` when manual creation is
|
|
26161
|
-
* unsupported. Lets the Add-Device dialog pick the right type per provider.
|
|
26162
|
-
*/
|
|
26163
|
-
async getManualCreationType() {
|
|
26164
|
-
if (!await this.supportsManualCreation()) return { deviceType: null };
|
|
26165
|
-
return { deviceType: Object.values(DeviceType).find((t) => this.deviceClasses[t] !== void 0) ?? null };
|
|
26166
|
-
}
|
|
26167
|
-
async adoptDiscoveredDevice(_input) {
|
|
26168
|
-
throw new Error(`${this.providerName} provider does not support discovery-based adoption`);
|
|
26169
|
-
}
|
|
26170
|
-
async supportsManualCreation() {
|
|
26171
|
-
return true;
|
|
26926
|
+
peek() {
|
|
26927
|
+
return this.tokens[this.pos];
|
|
26172
26928
|
}
|
|
26173
|
-
|
|
26174
|
-
return this.
|
|
26929
|
+
next() {
|
|
26930
|
+
return this.tokens[this.pos++];
|
|
26175
26931
|
}
|
|
26176
|
-
/**
|
|
26177
|
-
|
|
26178
|
-
|
|
26179
|
-
|
|
26180
|
-
|
|
26181
|
-
|
|
26182
|
-
|
|
26183
|
-
|
|
26184
|
-
|
|
26185
|
-
|
|
26186
|
-
|
|
26187
|
-
const Class = this.deviceClasses[spec.meta.type];
|
|
26188
|
-
if (!Class) throw new Error(`${this.providerName} provider: no device class registered for type "${spec.meta.type}" — add it to the deviceClasses map`);
|
|
26189
|
-
const stableId = this.generateStableId(spec.meta.type, spec.config);
|
|
26190
|
-
const device = await this.ctx.kernel.devices.create(stableId, Class, spec.config, null, spec.meta);
|
|
26191
|
-
if (spec.onAfterCreate) try {
|
|
26192
|
-
await spec.onAfterCreate(device);
|
|
26193
|
-
} catch (err) {
|
|
26194
|
-
this.ctx.logger.warn("createDevice: onAfterCreate hook threw — device is already registered", {
|
|
26195
|
-
tags: {
|
|
26196
|
-
deviceId: device.id,
|
|
26197
|
-
stableId
|
|
26198
|
-
},
|
|
26199
|
-
meta: { error: err instanceof Error ? err.message : String(err) }
|
|
26200
|
-
});
|
|
26932
|
+
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
26933
|
+
expectPunct(punct) {
|
|
26934
|
+
const tok = this.peek();
|
|
26935
|
+
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
26936
|
+
this.pos += 1;
|
|
26937
|
+
}
|
|
26938
|
+
matchPunct(punct) {
|
|
26939
|
+
const tok = this.peek();
|
|
26940
|
+
if (tok.type === "punct" && tok.punct === punct) {
|
|
26941
|
+
this.pos += 1;
|
|
26942
|
+
return true;
|
|
26201
26943
|
}
|
|
26202
|
-
return
|
|
26944
|
+
return false;
|
|
26203
26945
|
}
|
|
26204
|
-
|
|
26205
|
-
|
|
26206
|
-
|
|
26207
|
-
* fallback; any provider that has access to durable hardware
|
|
26208
|
-
* identity (UID, MAC, serial) should override and derive from it
|
|
26209
|
-
* so re-adding the same physical device reuses its persisted row.
|
|
26210
|
-
*
|
|
26211
|
-
* `config` is the parsed CreateDeviceSpec.config the subclass
|
|
26212
|
-
* returned from `onCreateDevice` — the override has access to
|
|
26213
|
-
* every operator-supplied + autodetect-resolved field. Optional
|
|
26214
|
-
* for back-compat: existing overrides that take only `type`
|
|
26215
|
-
* keep working unchanged.
|
|
26216
|
-
*/
|
|
26217
|
-
generateStableId(_type, _config) {
|
|
26218
|
-
return `${this.addonId}-${Date.now()}`;
|
|
26946
|
+
countNode() {
|
|
26947
|
+
this.nodeCount += 1;
|
|
26948
|
+
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
26219
26949
|
}
|
|
26220
|
-
|
|
26221
|
-
|
|
26222
|
-
|
|
26223
|
-
|
|
26224
|
-
|
|
26950
|
+
parseTernary() {
|
|
26951
|
+
const test = this.parseBinary(1);
|
|
26952
|
+
if (this.matchPunct("?")) {
|
|
26953
|
+
const consequent = this.parseTernary();
|
|
26954
|
+
this.expectPunct(":");
|
|
26955
|
+
const alternate = this.parseTernary();
|
|
26956
|
+
this.countNode();
|
|
26957
|
+
return {
|
|
26958
|
+
kind: "conditional",
|
|
26959
|
+
test,
|
|
26960
|
+
consequent,
|
|
26961
|
+
alternate
|
|
26962
|
+
};
|
|
26963
|
+
}
|
|
26964
|
+
return test;
|
|
26225
26965
|
}
|
|
26226
|
-
|
|
26227
|
-
|
|
26228
|
-
|
|
26966
|
+
parseBinary(minPrec) {
|
|
26967
|
+
let left = this.parseUnary();
|
|
26968
|
+
for (;;) {
|
|
26969
|
+
const tok = this.peek();
|
|
26970
|
+
if (tok.type !== "punct") break;
|
|
26971
|
+
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
26972
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
26973
|
+
const op = tok.punct;
|
|
26974
|
+
this.pos += 1;
|
|
26975
|
+
const right = this.parseBinary(prec + 1);
|
|
26976
|
+
this.countNode();
|
|
26977
|
+
if (isLogicalOp(op)) left = {
|
|
26978
|
+
kind: "logical",
|
|
26979
|
+
op,
|
|
26980
|
+
left,
|
|
26981
|
+
right
|
|
26982
|
+
};
|
|
26983
|
+
else if (isBinaryOp(op)) left = {
|
|
26984
|
+
kind: "binary",
|
|
26985
|
+
op,
|
|
26986
|
+
left,
|
|
26987
|
+
right
|
|
26988
|
+
};
|
|
26989
|
+
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
26990
|
+
}
|
|
26991
|
+
return left;
|
|
26229
26992
|
}
|
|
26230
|
-
|
|
26231
|
-
|
|
26232
|
-
|
|
26233
|
-
|
|
26234
|
-
|
|
26235
|
-
|
|
26236
|
-
|
|
26237
|
-
|
|
26238
|
-
|
|
26239
|
-
|
|
26240
|
-
|
|
26241
|
-
|
|
26242
|
-
* NOT accessory children — they're first-class adopted devices
|
|
26243
|
-
* that just happen to have a parent. Without this pass, every
|
|
26244
|
-
* server restart would lose hub-adopted cameras (their type is
|
|
26245
|
-
* in `deviceClasses` but parent's `getAccessoryChildren` doesn't
|
|
26246
|
-
* spawn them — that callback is only for purpose-built
|
|
26247
|
-
* accessory roles).
|
|
26248
|
-
*
|
|
26249
|
-
* Rows whose `type` is NOT in `deviceClasses` are skipped — those
|
|
26250
|
-
* are accessory children (siren/light/sensor) that the kernel's
|
|
26251
|
-
* accessory-spawn flow handles via the parent's
|
|
26252
|
-
* `getAccessoryChildren()`. Override only when the default doesn't
|
|
26253
|
-
* fit.
|
|
26254
|
-
*/
|
|
26255
|
-
async onRestoreDevices(savedDevices) {
|
|
26256
|
-
const restored = /* @__PURE__ */ new Set();
|
|
26257
|
-
for (const saved of savedDevices) {
|
|
26258
|
-
if (saved.parentDeviceId !== null) continue;
|
|
26259
|
-
const Class = this.deviceClasses[saved.type];
|
|
26260
|
-
if (!Class) {
|
|
26261
|
-
this.ctx.logger.warn("No device class registered for restored type — skipping", {
|
|
26262
|
-
tags: { stableId: saved.stableId },
|
|
26263
|
-
meta: { type: saved.type }
|
|
26264
|
-
});
|
|
26265
|
-
continue;
|
|
26266
|
-
}
|
|
26267
|
-
try {
|
|
26268
|
-
await this.ctx.kernel.devices.create(saved.stableId, Class, {});
|
|
26269
|
-
restored.add(saved.id);
|
|
26270
|
-
} catch (err) {
|
|
26271
|
-
this.ctx.logger.warn("Failed to restore device", {
|
|
26272
|
-
tags: { stableId: saved.stableId },
|
|
26273
|
-
meta: {
|
|
26274
|
-
type: saved.type,
|
|
26275
|
-
error: err instanceof Error ? err.message : String(err)
|
|
26276
|
-
}
|
|
26277
|
-
});
|
|
26278
|
-
}
|
|
26993
|
+
parseUnary() {
|
|
26994
|
+
const tok = this.peek();
|
|
26995
|
+
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
26996
|
+
const op = tok.punct;
|
|
26997
|
+
this.pos += 1;
|
|
26998
|
+
const operand = this.parseUnary();
|
|
26999
|
+
this.countNode();
|
|
27000
|
+
return {
|
|
27001
|
+
kind: "unary",
|
|
27002
|
+
op,
|
|
27003
|
+
operand
|
|
27004
|
+
};
|
|
26279
27005
|
}
|
|
26280
|
-
|
|
26281
|
-
|
|
26282
|
-
|
|
26283
|
-
|
|
26284
|
-
|
|
26285
|
-
|
|
26286
|
-
|
|
26287
|
-
|
|
26288
|
-
|
|
26289
|
-
|
|
26290
|
-
|
|
26291
|
-
|
|
26292
|
-
|
|
26293
|
-
|
|
26294
|
-
|
|
26295
|
-
|
|
26296
|
-
|
|
26297
|
-
|
|
26298
|
-
|
|
26299
|
-
|
|
27006
|
+
return this.parsePrimary();
|
|
27007
|
+
}
|
|
27008
|
+
parsePrimary() {
|
|
27009
|
+
const tok = this.next();
|
|
27010
|
+
switch (tok.type) {
|
|
27011
|
+
case "number":
|
|
27012
|
+
this.countNode();
|
|
27013
|
+
return {
|
|
27014
|
+
kind: "literal",
|
|
27015
|
+
value: tok.value
|
|
27016
|
+
};
|
|
27017
|
+
case "string":
|
|
27018
|
+
this.countNode();
|
|
27019
|
+
return {
|
|
27020
|
+
kind: "literal",
|
|
27021
|
+
value: tok.value
|
|
27022
|
+
};
|
|
27023
|
+
case "keyword":
|
|
27024
|
+
this.countNode();
|
|
27025
|
+
return {
|
|
27026
|
+
kind: "literal",
|
|
27027
|
+
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
27028
|
+
};
|
|
27029
|
+
case "identifier": {
|
|
27030
|
+
const nextTok = this.peek();
|
|
27031
|
+
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
27032
|
+
this.identifiers.add(tok.name);
|
|
27033
|
+
this.countNode();
|
|
27034
|
+
return {
|
|
27035
|
+
kind: "identifier",
|
|
27036
|
+
name: tok.name
|
|
27037
|
+
};
|
|
26300
27038
|
}
|
|
27039
|
+
case "punct":
|
|
27040
|
+
if (tok.punct === "(") {
|
|
27041
|
+
const inner = this.parseTernary();
|
|
27042
|
+
this.expectPunct(")");
|
|
27043
|
+
return inner;
|
|
27044
|
+
}
|
|
27045
|
+
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
27046
|
+
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
26301
27047
|
}
|
|
26302
27048
|
}
|
|
26303
|
-
|
|
26304
|
-
|
|
26305
|
-
|
|
27049
|
+
parseCall(callee, pos) {
|
|
27050
|
+
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
27051
|
+
this.expectPunct("(");
|
|
27052
|
+
const args = [];
|
|
27053
|
+
if (!this.matchPunct(")")) for (;;) {
|
|
27054
|
+
args.push(this.parseTernary());
|
|
27055
|
+
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
27056
|
+
if (this.matchPunct(",")) continue;
|
|
27057
|
+
this.expectPunct(")");
|
|
27058
|
+
break;
|
|
27059
|
+
}
|
|
27060
|
+
this.callees.add(callee);
|
|
27061
|
+
this.countNode();
|
|
27062
|
+
return {
|
|
27063
|
+
kind: "call",
|
|
27064
|
+
callee,
|
|
27065
|
+
args
|
|
27066
|
+
};
|
|
26306
27067
|
}
|
|
26307
27068
|
};
|
|
26308
|
-
|
|
26309
|
-
|
|
26310
|
-
|
|
26311
|
-
|
|
26312
|
-
|
|
26313
|
-
|
|
26314
|
-
|
|
26315
|
-
|
|
26316
|
-
|
|
27069
|
+
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
27070
|
+
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
27071
|
+
function parseExpression(source) {
|
|
27072
|
+
return new Parser(tokenize(source)).parse();
|
|
27073
|
+
}
|
|
27074
|
+
/**
|
|
27075
|
+
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
27076
|
+
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
27077
|
+
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
27078
|
+
* one per read on a hot resolve path.
|
|
27079
|
+
*
|
|
27080
|
+
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
27081
|
+
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
27082
|
+
* callers is safe and maximises hit rate.
|
|
27083
|
+
*/
|
|
27084
|
+
var cache = /* @__PURE__ */ new Map();
|
|
27085
|
+
function getCached(source) {
|
|
27086
|
+
const hit = cache.get(source);
|
|
27087
|
+
if (hit !== void 0) {
|
|
27088
|
+
cache.delete(source);
|
|
27089
|
+
cache.set(source, hit);
|
|
27090
|
+
return hit;
|
|
27091
|
+
}
|
|
27092
|
+
let result;
|
|
26317
27093
|
try {
|
|
26318
|
-
|
|
26319
|
-
|
|
26320
|
-
|
|
27094
|
+
result = {
|
|
27095
|
+
ok: true,
|
|
27096
|
+
parsed: parseExpression(source)
|
|
27097
|
+
};
|
|
27098
|
+
} catch (err) {
|
|
27099
|
+
result = {
|
|
27100
|
+
ok: false,
|
|
27101
|
+
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
27102
|
+
};
|
|
27103
|
+
}
|
|
27104
|
+
cache.set(source, result);
|
|
27105
|
+
if (cache.size > 256) {
|
|
27106
|
+
const oldest = cache.keys().next().value;
|
|
27107
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
26321
27108
|
}
|
|
27109
|
+
return result;
|
|
27110
|
+
}
|
|
27111
|
+
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
27112
|
+
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
27113
|
+
function compileExpressionSafe(source) {
|
|
27114
|
+
return getCached(source);
|
|
26322
27115
|
}
|
|
27116
|
+
Object.freeze({});
|
|
26323
27117
|
/**
|
|
26324
|
-
*
|
|
26325
|
-
*
|
|
26326
|
-
*
|
|
27118
|
+
* Author-time validation. Returns `null` when the source is valid, else a
|
|
27119
|
+
* human-readable error message. Checks: the expression compiles; binding count
|
|
27120
|
+
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
27121
|
+
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
27122
|
+
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
26327
27123
|
*/
|
|
26328
|
-
function
|
|
26329
|
-
const
|
|
26330
|
-
|
|
26331
|
-
const
|
|
26332
|
-
|
|
26333
|
-
|
|
26334
|
-
|
|
26335
|
-
|
|
26336
|
-
|
|
26337
|
-
|
|
26338
|
-
|
|
26339
|
-
|
|
26340
|
-
if (
|
|
26341
|
-
|
|
26342
|
-
|
|
26343
|
-
|
|
26344
|
-
if (stopped) return;
|
|
26345
|
-
if (reachable) {
|
|
26346
|
-
consecutiveFailures = 0;
|
|
26347
|
-
options.setOnline(true);
|
|
26348
|
-
} else registerFailure("probe resolved unreachable");
|
|
26349
|
-
} catch (error) {
|
|
26350
|
-
if (stopped) return;
|
|
26351
|
-
registerFailure(error instanceof Error ? error.message : "probe threw");
|
|
26352
|
-
} finally {
|
|
26353
|
-
running = false;
|
|
26354
|
-
}
|
|
26355
|
-
};
|
|
26356
|
-
const registerFailure = (reason) => {
|
|
26357
|
-
consecutiveFailures += 1;
|
|
26358
|
-
options.logger?.debug("reachability probe failed", {
|
|
26359
|
-
reason,
|
|
26360
|
-
consecutiveFailures,
|
|
26361
|
-
failuresToOffline
|
|
26362
|
-
});
|
|
26363
|
-
if (consecutiveFailures >= failuresToOffline) options.setOnline(false);
|
|
26364
|
-
};
|
|
26365
|
-
timer = setInterval(() => {
|
|
26366
|
-
tick();
|
|
26367
|
-
}, intervalMs);
|
|
26368
|
-
if (typeof timer === "object" && timer !== null && "unref" in timer) timer.unref();
|
|
26369
|
-
if (runImmediately) tick();
|
|
26370
|
-
return { stop: () => {
|
|
26371
|
-
if (stopped) return;
|
|
26372
|
-
stopped = true;
|
|
26373
|
-
if (timer !== void 0) clearInterval(timer);
|
|
26374
|
-
timer = void 0;
|
|
26375
|
-
} };
|
|
26376
|
-
}
|
|
26377
|
-
var LAST_FETCHED_FIELD = "lastFetchedAt";
|
|
26378
|
-
function createRuntimeStateBridge(params) {
|
|
26379
|
-
const { runtimeState, cap, ownDeviceId, refresh, staleMs, empty } = params;
|
|
26380
|
-
const ensureFresh = async () => {
|
|
26381
|
-
const slice = runtimeState.getCapState(cap.name);
|
|
26382
|
-
const fetchedAt = typeof slice?.[LAST_FETCHED_FIELD] === "number" ? slice[LAST_FETCHED_FIELD] : 0;
|
|
26383
|
-
if (!slice || Date.now() - fetchedAt > staleMs) await refresh();
|
|
26384
|
-
};
|
|
26385
|
-
const projectStatus = () => {
|
|
26386
|
-
const slice = runtimeState.getCapState(cap.name);
|
|
26387
|
-
if (!slice) return empty();
|
|
26388
|
-
const { [LAST_FETCHED_FIELD]: _omit, ...rest } = slice;
|
|
26389
|
-
return rest;
|
|
26390
|
-
};
|
|
26391
|
-
const getStatus = async ({ deviceId }) => {
|
|
26392
|
-
if (deviceId !== ownDeviceId) throw new Error(`${cap.name}: deviceId mismatch, expected ${ownDeviceId}, got ${deviceId}`);
|
|
26393
|
-
await ensureFresh();
|
|
26394
|
-
return projectStatus();
|
|
26395
|
-
};
|
|
26396
|
-
return {
|
|
26397
|
-
ensureFresh,
|
|
26398
|
-
getStatus
|
|
26399
|
-
};
|
|
27124
|
+
function validateExpressionSource(src) {
|
|
27125
|
+
const names = Object.keys(src.bindings);
|
|
27126
|
+
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
27127
|
+
for (const name of names) {
|
|
27128
|
+
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
27129
|
+
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
27130
|
+
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
27131
|
+
}
|
|
27132
|
+
const compiled = compileExpressionSafe(src.expr);
|
|
27133
|
+
if (!compiled.ok) return compiled.error;
|
|
27134
|
+
const bound = new Set(names);
|
|
27135
|
+
for (const id of compiled.parsed.identifiers) {
|
|
27136
|
+
if (id === "now") continue;
|
|
27137
|
+
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
27138
|
+
}
|
|
27139
|
+
return null;
|
|
26400
27140
|
}
|
|
27141
|
+
var ExpressionBindingSourceSchema = union([
|
|
27142
|
+
object({
|
|
27143
|
+
kind: literal("field").optional(),
|
|
27144
|
+
sourceKey: string(),
|
|
27145
|
+
cap: string(),
|
|
27146
|
+
fieldPath: string()
|
|
27147
|
+
}),
|
|
27148
|
+
object({
|
|
27149
|
+
kind: literal("literal"),
|
|
27150
|
+
value: union([
|
|
27151
|
+
string(),
|
|
27152
|
+
number(),
|
|
27153
|
+
boolean(),
|
|
27154
|
+
_null()
|
|
27155
|
+
])
|
|
27156
|
+
}),
|
|
27157
|
+
object({
|
|
27158
|
+
kind: literal("global"),
|
|
27159
|
+
sourceStableId: string(),
|
|
27160
|
+
cap: string(),
|
|
27161
|
+
fieldPath: string()
|
|
27162
|
+
})
|
|
27163
|
+
]);
|
|
27164
|
+
object({
|
|
27165
|
+
expr: string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
27166
|
+
bindings: record(string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
27167
|
+
}).superRefine((src, ctx) => {
|
|
27168
|
+
const err = validateExpressionSource(src);
|
|
27169
|
+
if (err !== null) ctx.addIssue({
|
|
27170
|
+
code: "custom",
|
|
27171
|
+
message: err,
|
|
27172
|
+
path: ["expr"]
|
|
27173
|
+
});
|
|
27174
|
+
});
|
|
26401
27175
|
Object.freeze({
|
|
26402
27176
|
"accessories.setChildHidden": {
|
|
26403
27177
|
capName: "accessories",
|
|
@@ -27221,6 +27995,12 @@ Object.freeze({
|
|
|
27221
27995
|
addonId: null,
|
|
27222
27996
|
access: "view"
|
|
27223
27997
|
},
|
|
27998
|
+
"coreBlocks.restart": {
|
|
27999
|
+
capName: "core-blocks",
|
|
28000
|
+
capScope: "system",
|
|
28001
|
+
addonId: null,
|
|
28002
|
+
access: "create"
|
|
28003
|
+
},
|
|
27224
28004
|
"coreBlocks.setEnabled": {
|
|
27225
28005
|
capName: "core-blocks",
|
|
27226
28006
|
capScope: "system",
|
|
@@ -27857,12 +28637,6 @@ Object.freeze({
|
|
|
27857
28637
|
addonId: null,
|
|
27858
28638
|
access: "create"
|
|
27859
28639
|
},
|
|
27860
|
-
"deviceManager.setDeviceLinks": {
|
|
27861
|
-
capName: "device-manager",
|
|
27862
|
-
capScope: "system",
|
|
27863
|
-
addonId: null,
|
|
27864
|
-
access: "create"
|
|
27865
|
-
},
|
|
27866
28640
|
"deviceManager.setDisabled": {
|
|
27867
28641
|
capName: "device-manager",
|
|
27868
28642
|
capScope: "system",
|
|
@@ -29261,6 +30035,48 @@ Object.freeze({
|
|
|
29261
30035
|
addonId: null,
|
|
29262
30036
|
access: "create"
|
|
29263
30037
|
},
|
|
30038
|
+
"osdManager.clearSlotBinding": {
|
|
30039
|
+
capName: "osd-manager",
|
|
30040
|
+
capScope: "system",
|
|
30041
|
+
addonId: null,
|
|
30042
|
+
access: "delete"
|
|
30043
|
+
},
|
|
30044
|
+
"osdManager.getConditionSupport": {
|
|
30045
|
+
capName: "osd-manager",
|
|
30046
|
+
capScope: "system",
|
|
30047
|
+
addonId: null,
|
|
30048
|
+
access: "view"
|
|
30049
|
+
},
|
|
30050
|
+
"osdManager.getDeviceOsd": {
|
|
30051
|
+
capName: "osd-manager",
|
|
30052
|
+
capScope: "system",
|
|
30053
|
+
addonId: null,
|
|
30054
|
+
access: "view"
|
|
30055
|
+
},
|
|
30056
|
+
"osdManager.getSourceCatalog": {
|
|
30057
|
+
capName: "osd-manager",
|
|
30058
|
+
capScope: "system",
|
|
30059
|
+
addonId: null,
|
|
30060
|
+
access: "view"
|
|
30061
|
+
},
|
|
30062
|
+
"osdManager.previewSlot": {
|
|
30063
|
+
capName: "osd-manager",
|
|
30064
|
+
capScope: "system",
|
|
30065
|
+
addonId: null,
|
|
30066
|
+
access: "create"
|
|
30067
|
+
},
|
|
30068
|
+
"osdManager.renderDevice": {
|
|
30069
|
+
capName: "osd-manager",
|
|
30070
|
+
capScope: "system",
|
|
30071
|
+
addonId: null,
|
|
30072
|
+
access: "create"
|
|
30073
|
+
},
|
|
30074
|
+
"osdManager.setSlotBinding": {
|
|
30075
|
+
capName: "osd-manager",
|
|
30076
|
+
capScope: "system",
|
|
30077
|
+
addonId: null,
|
|
30078
|
+
access: "create"
|
|
30079
|
+
},
|
|
29264
30080
|
"petFeeder.callPet": {
|
|
29265
30081
|
capName: "pet-feeder",
|
|
29266
30082
|
capScope: "device",
|
|
@@ -29333,6 +30149,12 @@ Object.freeze({
|
|
|
29333
30149
|
addonId: null,
|
|
29334
30150
|
access: "delete"
|
|
29335
30151
|
},
|
|
30152
|
+
"pipelineAnalytics.completeRetrainTrack": {
|
|
30153
|
+
capName: "pipeline-analytics",
|
|
30154
|
+
capScope: "device",
|
|
30155
|
+
addonId: null,
|
|
30156
|
+
access: "create"
|
|
30157
|
+
},
|
|
29336
30158
|
"pipelineAnalytics.deleteDeviceEvents": {
|
|
29337
30159
|
capName: "pipeline-analytics",
|
|
29338
30160
|
capScope: "device",
|
|
@@ -29345,6 +30167,12 @@ Object.freeze({
|
|
|
29345
30167
|
addonId: null,
|
|
29346
30168
|
access: "delete"
|
|
29347
30169
|
},
|
|
30170
|
+
"pipelineAnalytics.deselectRetrainFrame": {
|
|
30171
|
+
capName: "pipeline-analytics",
|
|
30172
|
+
capScope: "device",
|
|
30173
|
+
addonId: null,
|
|
30174
|
+
access: "create"
|
|
30175
|
+
},
|
|
29348
30176
|
"pipelineAnalytics.getActiveTracks": {
|
|
29349
30177
|
capName: "pipeline-analytics",
|
|
29350
30178
|
capScope: "device",
|
|
@@ -29405,6 +30233,18 @@ Object.freeze({
|
|
|
29405
30233
|
addonId: null,
|
|
29406
30234
|
access: "view"
|
|
29407
30235
|
},
|
|
30236
|
+
"pipelineAnalytics.getRetrainExportUrl": {
|
|
30237
|
+
capName: "pipeline-analytics",
|
|
30238
|
+
capScope: "device",
|
|
30239
|
+
addonId: null,
|
|
30240
|
+
access: "view"
|
|
30241
|
+
},
|
|
30242
|
+
"pipelineAnalytics.getRetrainFrameImage": {
|
|
30243
|
+
capName: "pipeline-analytics",
|
|
30244
|
+
capScope: "device",
|
|
30245
|
+
addonId: null,
|
|
30246
|
+
access: "view"
|
|
30247
|
+
},
|
|
29408
30248
|
"pipelineAnalytics.getSensorEvents": {
|
|
29409
30249
|
capName: "pipeline-analytics",
|
|
29410
30250
|
capScope: "device",
|
|
@@ -29423,6 +30263,18 @@ Object.freeze({
|
|
|
29423
30263
|
addonId: null,
|
|
29424
30264
|
access: "view"
|
|
29425
30265
|
},
|
|
30266
|
+
"pipelineAnalytics.getTrainingExportSummary": {
|
|
30267
|
+
capName: "pipeline-analytics",
|
|
30268
|
+
capScope: "device",
|
|
30269
|
+
addonId: null,
|
|
30270
|
+
access: "view"
|
|
30271
|
+
},
|
|
30272
|
+
"pipelineAnalytics.getTrainingExportUrl": {
|
|
30273
|
+
capName: "pipeline-analytics",
|
|
30274
|
+
capScope: "device",
|
|
30275
|
+
addonId: null,
|
|
30276
|
+
access: "view"
|
|
30277
|
+
},
|
|
29426
30278
|
"pipelineAnalytics.listEventKinds": {
|
|
29427
30279
|
capName: "pipeline-analytics",
|
|
29428
30280
|
capScope: "device",
|
|
@@ -29447,6 +30299,24 @@ Object.freeze({
|
|
|
29447
30299
|
addonId: null,
|
|
29448
30300
|
access: "view"
|
|
29449
30301
|
},
|
|
30302
|
+
"pipelineAnalytics.listRetrainAnnotations": {
|
|
30303
|
+
capName: "pipeline-analytics",
|
|
30304
|
+
capScope: "device",
|
|
30305
|
+
addonId: null,
|
|
30306
|
+
access: "view"
|
|
30307
|
+
},
|
|
30308
|
+
"pipelineAnalytics.listRetrainFrames": {
|
|
30309
|
+
capName: "pipeline-analytics",
|
|
30310
|
+
capScope: "device",
|
|
30311
|
+
addonId: null,
|
|
30312
|
+
access: "view"
|
|
30313
|
+
},
|
|
30314
|
+
"pipelineAnalytics.listRetrainStaging": {
|
|
30315
|
+
capName: "pipeline-analytics",
|
|
30316
|
+
capScope: "device",
|
|
30317
|
+
addonId: null,
|
|
30318
|
+
access: "view"
|
|
30319
|
+
},
|
|
29450
30320
|
"pipelineAnalytics.listTrackMedia": {
|
|
29451
30321
|
capName: "pipeline-analytics",
|
|
29452
30322
|
capScope: "device",
|
|
@@ -29459,6 +30329,12 @@ Object.freeze({
|
|
|
29459
30329
|
addonId: null,
|
|
29460
30330
|
access: "view"
|
|
29461
30331
|
},
|
|
30332
|
+
"pipelineAnalytics.proposeRetrainAnnotations": {
|
|
30333
|
+
capName: "pipeline-analytics",
|
|
30334
|
+
capScope: "device",
|
|
30335
|
+
addonId: null,
|
|
30336
|
+
access: "create"
|
|
30337
|
+
},
|
|
29462
30338
|
"pipelineAnalytics.pruneEvents": {
|
|
29463
30339
|
capName: "pipeline-analytics",
|
|
29464
30340
|
capScope: "device",
|
|
@@ -29489,12 +30365,30 @@ Object.freeze({
|
|
|
29489
30365
|
addonId: null,
|
|
29490
30366
|
access: "create"
|
|
29491
30367
|
},
|
|
30368
|
+
"pipelineAnalytics.restageRetrainTrack": {
|
|
30369
|
+
capName: "pipeline-analytics",
|
|
30370
|
+
capScope: "device",
|
|
30371
|
+
addonId: null,
|
|
30372
|
+
access: "create"
|
|
30373
|
+
},
|
|
30374
|
+
"pipelineAnalytics.saveRetrainAnnotations": {
|
|
30375
|
+
capName: "pipeline-analytics",
|
|
30376
|
+
capScope: "device",
|
|
30377
|
+
addonId: null,
|
|
30378
|
+
access: "create"
|
|
30379
|
+
},
|
|
29492
30380
|
"pipelineAnalytics.searchObjectEvents": {
|
|
29493
30381
|
capName: "pipeline-analytics",
|
|
29494
30382
|
capScope: "device",
|
|
29495
30383
|
addonId: null,
|
|
29496
30384
|
access: "view"
|
|
29497
30385
|
},
|
|
30386
|
+
"pipelineAnalytics.selectRetrainFrames": {
|
|
30387
|
+
capName: "pipeline-analytics",
|
|
30388
|
+
capScope: "device",
|
|
30389
|
+
addonId: null,
|
|
30390
|
+
access: "create"
|
|
30391
|
+
},
|
|
29498
30392
|
"pipelineAnalytics.setTrackFlags": {
|
|
29499
30393
|
capName: "pipeline-analytics",
|
|
29500
30394
|
capScope: "device",
|
|
@@ -30887,6 +31781,12 @@ Object.freeze({
|
|
|
30887
31781
|
addonId: null,
|
|
30888
31782
|
access: "create"
|
|
30889
31783
|
},
|
|
31784
|
+
"streamBroker.fetchEventMedia": {
|
|
31785
|
+
capName: "stream-broker",
|
|
31786
|
+
capScope: "system",
|
|
31787
|
+
addonId: null,
|
|
31788
|
+
access: "create"
|
|
31789
|
+
},
|
|
30890
31790
|
"streamBroker.getAllRtspEntries": {
|
|
30891
31791
|
capName: "stream-broker",
|
|
30892
31792
|
capScope: "system",
|
|
@@ -30899,6 +31799,12 @@ Object.freeze({
|
|
|
30899
31799
|
addonId: null,
|
|
30900
31800
|
access: "view"
|
|
30901
31801
|
},
|
|
31802
|
+
"streamBroker.getDeviceAudioMute": {
|
|
31803
|
+
capName: "stream-broker",
|
|
31804
|
+
capScope: "system",
|
|
31805
|
+
addonId: null,
|
|
31806
|
+
access: "view"
|
|
31807
|
+
},
|
|
30902
31808
|
"streamBroker.getPreBufferInfo": {
|
|
30903
31809
|
capName: "stream-broker",
|
|
30904
31810
|
capScope: "system",
|
|
@@ -30965,6 +31871,12 @@ Object.freeze({
|
|
|
30965
31871
|
addonId: null,
|
|
30966
31872
|
access: "create"
|
|
30967
31873
|
},
|
|
31874
|
+
"streamBroker.produceEventMedia": {
|
|
31875
|
+
capName: "stream-broker",
|
|
31876
|
+
capScope: "system",
|
|
31877
|
+
addonId: null,
|
|
31878
|
+
access: "create"
|
|
31879
|
+
},
|
|
30968
31880
|
"streamBroker.publishCameraStream": {
|
|
30969
31881
|
capName: "stream-broker",
|
|
30970
31882
|
capScope: "system",
|
|
@@ -31019,6 +31931,12 @@ Object.freeze({
|
|
|
31019
31931
|
addonId: null,
|
|
31020
31932
|
access: "create"
|
|
31021
31933
|
},
|
|
31934
|
+
"streamBroker.setDeviceAudioMute": {
|
|
31935
|
+
capName: "stream-broker",
|
|
31936
|
+
capScope: "system",
|
|
31937
|
+
addonId: null,
|
|
31938
|
+
access: "create"
|
|
31939
|
+
},
|
|
31022
31940
|
"streamBroker.setPreBufferDuration": {
|
|
31023
31941
|
capName: "stream-broker",
|
|
31024
31942
|
capScope: "system",
|
|
@@ -33133,6 +34051,7 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
33133
34051
|
ownDeviceId: this.id,
|
|
33134
34052
|
refresh: refreshFromCamera,
|
|
33135
34053
|
staleMs: STALE_MS,
|
|
34054
|
+
logger: this.ctx.logger,
|
|
33136
34055
|
empty: () => ({ lastFetchedAt: 0 })
|
|
33137
34056
|
});
|
|
33138
34057
|
const provider = {
|
|
@@ -33224,6 +34143,7 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
33224
34143
|
ownDeviceId: this.id,
|
|
33225
34144
|
refresh: refreshFromCamera,
|
|
33226
34145
|
staleMs: STALE_MS,
|
|
34146
|
+
logger: this.ctx.logger,
|
|
33227
34147
|
empty: () => ({
|
|
33228
34148
|
enabled: false,
|
|
33229
34149
|
sensitivity: 0,
|
|
@@ -33326,6 +34246,7 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
33326
34246
|
ownDeviceId: this.id,
|
|
33327
34247
|
refresh: refreshFromCamera,
|
|
33328
34248
|
staleMs: STALE_MS,
|
|
34249
|
+
logger: this.ctx.logger,
|
|
33329
34250
|
empty: () => ({
|
|
33330
34251
|
enabled: false,
|
|
33331
34252
|
regions: [],
|
|
@@ -33482,6 +34403,7 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
33482
34403
|
ownDeviceId: this.id,
|
|
33483
34404
|
refresh: refreshFromCamera,
|
|
33484
34405
|
staleMs: STALE_MS,
|
|
34406
|
+
logger: this.ctx.logger,
|
|
33485
34407
|
empty: () => ({
|
|
33486
34408
|
mode: "auto",
|
|
33487
34409
|
lastFetchedAt: 0
|
|
@@ -33572,6 +34494,7 @@ var AmcrestCamera = class AmcrestCamera extends BaseDevice {
|
|
|
33572
34494
|
ownDeviceId: this.id,
|
|
33573
34495
|
refresh: refreshFromCamera,
|
|
33574
34496
|
staleMs: STALE_MS,
|
|
34497
|
+
logger: this.ctx.logger,
|
|
33575
34498
|
empty: () => ({ lastFetchedAt: 0 })
|
|
33576
34499
|
}).getStatus,
|
|
33577
34500
|
getOptions: async ({ deviceId }) => {
|