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