@camstack/types 1.2.46 → 1.2.47
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 +4 -3
- package/dist/addon.mjs +4 -3
- package/dist/{fmp4-box-splitter-B53u9-Nu.mjs → canonical-hash-rO1sRmEK.mjs} +34 -34
- package/dist/capabilities/core-blocks.cap.d.ts +52 -0
- package/dist/capabilities/device-manager.cap.d.ts +11 -427
- package/dist/capabilities/index.d.ts +2 -2
- package/dist/capabilities/motion-detection.cap.d.ts +6 -1
- package/dist/capabilities/notification-rules.cap.d.ts +24 -24
- package/dist/capabilities/oauth-integration.cap.d.ts +4 -0
- package/dist/capabilities/osd-manager.cap.d.ts +12 -12
- package/dist/capabilities/pipeline-analytics.cap.d.ts +7 -1
- package/dist/capabilities/sso-bridge.cap.d.ts +3 -0
- package/dist/capabilities/stream-broker.cap.d.ts +1 -0
- package/dist/capabilities/user-management.cap.d.ts +3 -1
- package/dist/capabilities/videoclips.cap.d.ts +5 -0
- package/dist/device/declared-device.d.ts +197 -0
- package/dist/device/device-binding.d.ts +15 -9
- package/dist/device/device-management.d.ts +1 -83
- package/dist/device/index.d.ts +20 -19
- package/dist/expression/binding-source.d.ts +85 -0
- package/dist/expression/{link-expression.d.ts → expression-source.d.ts} +22 -13
- package/dist/expression/index.d.ts +17 -14
- package/dist/expression/limits.d.ts +1 -1
- package/dist/generated/addon-api.d.ts +7 -7
- package/dist/generated/device-proxy.d.ts +1 -1
- package/dist/generated/system-proxy.d.ts +1 -1
- package/dist/index.d.ts +9 -8
- package/dist/index.js +2322 -2057
- package/dist/index.mjs +2297 -2043
- package/dist/node.js +7 -7
- package/dist/node.mjs +1 -1
- package/dist/{sleep-BbYwFLG6.mjs → sleep-7WqNZVcL.mjs} +0 -1
- package/dist/{sleep-CyN9nHr_.js → sleep-ocMLM2o5.js} +0 -1
- package/package.json +1 -1
- package/dist/device/device-link-transform.d.ts +0 -5
- package/dist/{fmp4-box-splitter-BkWH7O3L.js → canonical-hash-DNV8S5ET.js} +33 -33
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { t as EventCategory } from "./event-category-41fKf-q9.mjs";
|
|
2
|
-
import { $ as FrameHandleSchema, A as method, B as readinessKey, C as DeviceType, Ct as collectHydratedFieldEntries, D as event, E as DEVICE_STATUS_METHOD, Et as resolveHydratedFieldValue, F as readNodePin, G as CamProfileSchema, H as BrokerStatsSchema, I as toNodeId, J as CameraStreamSchema, K as CamStreamKindSchema, L as ReadinessRegistry, M as systemMethod, N as CAP_NODE_PIN_CONTEXT_KEY, O as expandCapMethods, P as nodePin, Q as FrameHandleFormatSchema, R as ReadinessTimeoutError, S as DeviceRole, St as WELL_KNOWN_TAB_MAP, T as DEVICE_SETTINGS_CONTRIBUTION_METHODS, Tt as hydrateSchema, U as BrokerStatusSchema, V as scopeKey, W as CAM_PROFILE_ORDER, X as DecodedFrameSchema, Y as DecodedAudioChunkSchema, Z as EncodedPacketSchema, _ as RawStateResultSchema, _t as createDurableState, a as asJsonObject, at as SubscribeAudioChunksInputSchema, b as ChargingStatus, bt as isEvent, c as parseJsonArray, ct as SubscribeFramesResultSchema, d as DEVICE_SCOPED_CAPS, dt as parseProfileBrokerId, et as ProfileRtspEntrySchema, f as isDeviceScopedCap, ft as selectAssignedProfileSlots, g as createSliceHandle, gt as normalizeAddonInitResult, h as createMirrorSource, ht as BaseAddon, i as asJsonArray, it as StreamSourceSchema, j as resolveCapMount, k as isDeviceConfigCap, l as parseJsonObject, lt as makeProfileBrokerId, m as createLazyTrpcSource, mt as DisposerChain, n as sleepCancellable, nt as ProfileSlotStatusSchema, o as asNumber, ot as SubscribeAudioChunksResultSchema, p as createDeviceProxy, pt as DATAPLANE_SECRET_HEADER, q as CamStreamResolutionSchema, r as asBoolean, rt as StreamSourceEntrySchema, s as asString, st as SubscribeFramesInputSchema, t as sleep, tt as ProfileSlotSchema, u as parseJsonUnknown, ut as makeSourceBrokerId, v as deviceOpsCapability, vt as createEvent, w as adminUiCapability, wt as collectHydratedFieldValues, x as DeviceFeature, xt as WELL_KNOWN_TABS, y as viewerUiCapability, yt as emitReadiness, z as emitDownForOwnedCaps } from "./sleep-
|
|
3
|
-
import { a as buildAudioArgs, c as buildVideoArgs, d as logBannerArgs, f as pickVideoEncoder, i as audioPlanFromEncodeProfile, l as invocationFromEncodeProfile, n as
|
|
2
|
+
import { $ as FrameHandleSchema, A as method, B as readinessKey, C as DeviceType, Ct as collectHydratedFieldEntries, D as event, E as DEVICE_STATUS_METHOD, Et as resolveHydratedFieldValue, F as readNodePin, G as CamProfileSchema, H as BrokerStatsSchema, I as toNodeId, J as CameraStreamSchema, K as CamStreamKindSchema, L as ReadinessRegistry, M as systemMethod, N as CAP_NODE_PIN_CONTEXT_KEY, O as expandCapMethods, P as nodePin, Q as FrameHandleFormatSchema, R as ReadinessTimeoutError, S as DeviceRole, St as WELL_KNOWN_TAB_MAP, T as DEVICE_SETTINGS_CONTRIBUTION_METHODS, Tt as hydrateSchema, U as BrokerStatusSchema, V as scopeKey, W as CAM_PROFILE_ORDER, X as DecodedFrameSchema, Y as DecodedAudioChunkSchema, Z as EncodedPacketSchema, _ as RawStateResultSchema, _t as createDurableState, a as asJsonObject, at as SubscribeAudioChunksInputSchema, b as ChargingStatus, bt as isEvent, c as parseJsonArray, ct as SubscribeFramesResultSchema, d as DEVICE_SCOPED_CAPS, dt as parseProfileBrokerId, et as ProfileRtspEntrySchema, f as isDeviceScopedCap, ft as selectAssignedProfileSlots, g as createSliceHandle, gt as normalizeAddonInitResult, h as createMirrorSource, ht as BaseAddon, i as asJsonArray, it as StreamSourceSchema, j as resolveCapMount, k as isDeviceConfigCap, l as parseJsonObject, lt as makeProfileBrokerId, m as createLazyTrpcSource, mt as DisposerChain, n as sleepCancellable, nt as ProfileSlotStatusSchema, o as asNumber, ot as SubscribeAudioChunksResultSchema, p as createDeviceProxy, pt as DATAPLANE_SECRET_HEADER, q as CamStreamResolutionSchema, r as asBoolean, rt as StreamSourceEntrySchema, s as asString, st as SubscribeFramesInputSchema, t as sleep, tt as ProfileSlotSchema, u as parseJsonUnknown, ut as makeSourceBrokerId, v as deviceOpsCapability, vt as createEvent, w as adminUiCapability, wt as collectHydratedFieldValues, x as DeviceFeature, xt as WELL_KNOWN_TABS, y as viewerUiCapability, yt as emitReadiness, z as emitDownForOwnedCaps } from "./sleep-7WqNZVcL.mjs";
|
|
3
|
+
import { a as buildAudioArgs, c as buildVideoArgs, d as logBannerArgs, f as pickVideoEncoder, i as audioPlanFromEncodeProfile, l as invocationFromEncodeProfile, n as Fmp4BoxSplitter, o as buildFfmpegArgs, r as AUDIO_PRESETS, s as buildInputArgs, t as canonicalHash, u as isSoftwareDecode } from "./canonical-hash-rO1sRmEK.mjs";
|
|
4
4
|
import { EventSourceType } from "./enums.mjs";
|
|
5
5
|
import { t as errMsg } from "./err-msg-IQTHeDzc.mjs";
|
|
6
6
|
import { z } from "zod";
|
|
@@ -4325,12 +4325,34 @@ var streamBrokerCapability = {
|
|
|
4325
4325
|
kinds: z.array(EventMediaKindSchema).min(1).default(["mp4"]),
|
|
4326
4326
|
/** GIF geometry. The video keeps the source's own. */
|
|
4327
4327
|
gifMaxWidth: z.number().int().min(120).max(1280).default(640),
|
|
4328
|
-
gifFps: z.number().int().min(1).max(15).default(8),
|
|
4329
4328
|
/**
|
|
4330
|
-
*
|
|
4331
|
-
*
|
|
4329
|
+
* The gif's own PLAYBACK rate in frames per second — what the finished
|
|
4330
|
+
* gif runs at, not how many source frames feed it. The decimation that
|
|
4331
|
+
* feeds it samples `gifFps / gifSpeed` source frames per second, so at
|
|
4332
|
+
* the defaults a 12 fps gif is built out of 3 source frames a second.
|
|
4332
4333
|
*/
|
|
4333
|
-
|
|
4334
|
+
gifFps: z.number().int().min(1).max(15).default(12),
|
|
4335
|
+
/**
|
|
4336
|
+
* How fast the GIF plays against real time, independent of `speed`.
|
|
4337
|
+
*
|
|
4338
|
+
* 4× by default, by operator request: a notification gif is glanced at
|
|
4339
|
+
* on a lock screen, so a ~12 s window has to be over in ~3 s. It stays
|
|
4340
|
+
* a separate knob from `speed` even though both now default to 4 —
|
|
4341
|
+
* a caller wanting a real-time video and a fast gif must not have to
|
|
4342
|
+
* choose.
|
|
4343
|
+
*/
|
|
4344
|
+
gifSpeed: z.number().min(1).max(8).default(4),
|
|
4345
|
+
/**
|
|
4346
|
+
* Playback rate of the VIDEO. Also 4× by default, by operator decision.
|
|
4347
|
+
*
|
|
4348
|
+
* `1` is real time and is the ONLY value that allows the copy branch —
|
|
4349
|
+
* anything else forces `libx264` over the window. That was priced
|
|
4350
|
+
* before it was chosen: a per-event burst measured at 0.23 s and 254 KB
|
|
4351
|
+
* on a real 615 720p cut, against 922 KB for the copy it replaces. A
|
|
4352
|
+
* re-encode is capped at 720p (`EVENT_CLIP_ENCODE_MAX_WIDTH`), because
|
|
4353
|
+
* once the decode is forced the width stops being free.
|
|
4354
|
+
*/
|
|
4355
|
+
speed: z.number().min(1).max(8).default(4)
|
|
4334
4356
|
}), EventMediaProductionSchema, {
|
|
4335
4357
|
kind: "mutation",
|
|
4336
4358
|
auth: "admin"
|
|
@@ -4927,6 +4949,28 @@ function kebabToCamel(s) {
|
|
|
4927
4949
|
* instead of taking the hub with it (D6). Every method here is admin-only, and
|
|
4928
4950
|
* must stay so.
|
|
4929
4951
|
*/
|
|
4952
|
+
/**
|
|
4953
|
+
* Prefix for a block's generated addon id AND its runner id — one addon, one
|
|
4954
|
+
* process, so the two are the same string (D2).
|
|
4955
|
+
*
|
|
4956
|
+
* It lives on the CONTRACT rather than beside the code that materialises the
|
|
4957
|
+
* package, because it is not an implementation detail: the operator surface
|
|
4958
|
+
* reads a block's logs by asking `logs.query` for `addonId = core-block-<id>`,
|
|
4959
|
+
* with no cap and no store of its own. Two copies of this string is one copy
|
|
4960
|
+
* too many — a drift would show as a logs pane that is simply always empty.
|
|
4961
|
+
*/
|
|
4962
|
+
var CORE_BLOCK_ADDON_PREFIX = "core-block-";
|
|
4963
|
+
/** The addon/runner id a block's process runs under. */
|
|
4964
|
+
function coreBlockAddonId(blockId) {
|
|
4965
|
+
return `${CORE_BLOCK_ADDON_PREFIX}${blockId}`;
|
|
4966
|
+
}
|
|
4967
|
+
/** The block id behind a generated addon/runner id, or null when the id belongs
|
|
4968
|
+
* to something else. The inverse of {@link coreBlockAddonId}. */
|
|
4969
|
+
function coreBlockIdFromAddonId(addonId) {
|
|
4970
|
+
if (!addonId.startsWith("core-block-")) return null;
|
|
4971
|
+
const rest = addonId.slice(11);
|
|
4972
|
+
return rest.length > 0 ? rest : null;
|
|
4973
|
+
}
|
|
4930
4974
|
/** Where a block runs. The operator chooses — a block driving a device on an
|
|
4931
4975
|
* agent is the reason placement is not fixed to the hub. */
|
|
4932
4976
|
var CoreBlockPlacementSchema = z.union([z.literal("hub"), z.string().min(1)]);
|
|
@@ -5015,6 +5059,23 @@ var coreBlocksCapability = {
|
|
|
5015
5059
|
auth: "admin"
|
|
5016
5060
|
}),
|
|
5017
5061
|
/**
|
|
5062
|
+
* Stop this block's runner and let the supervisor bring it back.
|
|
5063
|
+
*
|
|
5064
|
+
* **It stores nothing.** There is no "restart requested" flag and no second
|
|
5065
|
+
* lifecycle field: the supervisor already stops and respawns a runner when
|
|
5066
|
+
* the code hash changes, and this exposes that same path deliberately
|
|
5067
|
+
* (D62 — a switch writes the authority that already owned the function).
|
|
5068
|
+
* The next reconcile pass remains the authority, so a restart that lost a
|
|
5069
|
+
* race is corrected within one pass rather than leaving a stale flag behind.
|
|
5070
|
+
*
|
|
5071
|
+
* Refused for a DISABLED block: nothing would come back, and a button that
|
|
5072
|
+
* silently does nothing is worse than one that says why.
|
|
5073
|
+
*/
|
|
5074
|
+
restart: method(z.object({ blockId: z.string() }), z.object({ block: CoreBlockSchema }), {
|
|
5075
|
+
kind: "mutation",
|
|
5076
|
+
auth: "admin"
|
|
5077
|
+
}),
|
|
5078
|
+
/**
|
|
5018
5079
|
* Type-check without saving — what the editor calls as the author types, so
|
|
5019
5080
|
* the compiler's verdict is the same one the server will reach.
|
|
5020
5081
|
*/
|
|
@@ -6230,1114 +6291,247 @@ var deviceExportCapability = {
|
|
|
6230
6291
|
}
|
|
6231
6292
|
};
|
|
6232
6293
|
//#endregion
|
|
6233
|
-
//#region src/
|
|
6294
|
+
//#region src/capabilities/device-provider.cap.ts
|
|
6295
|
+
var ProviderStatusSchema = z.object({
|
|
6296
|
+
connected: z.boolean(),
|
|
6297
|
+
deviceCount: z.number(),
|
|
6298
|
+
error: z.string().optional()
|
|
6299
|
+
});
|
|
6300
|
+
var DiscoveredDeviceSchema = z.object({
|
|
6301
|
+
externalId: z.string(),
|
|
6302
|
+
name: z.string(),
|
|
6303
|
+
type: z.string(),
|
|
6304
|
+
metadata: z.record(z.string(), z.unknown()).optional()
|
|
6305
|
+
});
|
|
6234
6306
|
/**
|
|
6235
|
-
*
|
|
6236
|
-
*
|
|
6237
|
-
*
|
|
6238
|
-
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
6239
|
-
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
6240
|
-
* work a single author-supplied expression can request, so a hostile or
|
|
6241
|
-
* accidental pathological string can never spend unbounded CPU/memory.
|
|
6307
|
+
* Candidate handed back from discovery and accepted by
|
|
6308
|
+
* `adoptDiscoveredDevice`. Shape mirrors the in-process
|
|
6309
|
+
* `DiscoveredDevice` interface used by `DeviceDiscovery`.
|
|
6242
6310
|
*/
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
*
|
|
6251
|
-
*
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
var MAX_EXPRESSION_CALL_ARGS = 16;
|
|
6257
|
-
/** LRU compile-cache capacity (parsed ASTs keyed by raw source string). */
|
|
6258
|
-
var EXPRESSION_COMPILE_CACHE_CAPACITY = 256;
|
|
6259
|
-
/** A legal binding / identifier name. */
|
|
6260
|
-
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
6261
|
-
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
6262
|
-
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
6263
|
-
var RESERVED_BINDING_NAMES = new Set([
|
|
6264
|
-
"now",
|
|
6265
|
-
"true",
|
|
6266
|
-
"false",
|
|
6267
|
-
"null"
|
|
6268
|
-
]);
|
|
6269
|
-
//#endregion
|
|
6270
|
-
//#region src/expression/errors.ts
|
|
6311
|
+
var DiscoveryCandidateSchema = z.object({
|
|
6312
|
+
stableId: z.string(),
|
|
6313
|
+
type: z.enum(DeviceType),
|
|
6314
|
+
suggestedName: z.string(),
|
|
6315
|
+
prefilledConfig: z.record(z.string(), z.unknown()),
|
|
6316
|
+
/**
|
|
6317
|
+
* Optional upstream-system identity (HA entity_id, vendor MAC, …).
|
|
6318
|
+
* Discovery pre-populates this for systems that know the upstream
|
|
6319
|
+
* identity ahead of adoption. Rendering metadata (unit, precision)
|
|
6320
|
+
* flows live through the cap STATUS SLICE after adoption.
|
|
6321
|
+
*/
|
|
6322
|
+
sourceInfo: SourceInfoSchema.optional()
|
|
6323
|
+
});
|
|
6271
6324
|
/**
|
|
6272
|
-
*
|
|
6273
|
-
*
|
|
6274
|
-
*
|
|
6325
|
+
* Flat device summary returned by `createDevice` / `adoptDiscoveredDevice`.
|
|
6326
|
+
* Mirrors `toDeviceShape()` output in `device-management.router.ts` so the
|
|
6327
|
+
* tRPC layer can pass it through without reshaping.
|
|
6275
6328
|
*/
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
/**
|
|
6287
|
-
*
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
}
|
|
6293
|
-
};
|
|
6294
|
-
//#endregion
|
|
6295
|
-
//#region src/expression/builtins.ts
|
|
6329
|
+
var DeviceSummarySchema = z.object({
|
|
6330
|
+
id: z.number(),
|
|
6331
|
+
stableId: z.string(),
|
|
6332
|
+
addonId: z.string(),
|
|
6333
|
+
type: z.string(),
|
|
6334
|
+
name: z.string(),
|
|
6335
|
+
parentDeviceId: z.number().nullable(),
|
|
6336
|
+
online: z.boolean(),
|
|
6337
|
+
features: z.array(z.string()),
|
|
6338
|
+
config: z.record(z.string(), z.unknown()),
|
|
6339
|
+
/** Optional upstream-system identity (dispatch key + system tag).
|
|
6340
|
+
* See `SourceInfo`. Present when the device has a non-synthetic
|
|
6341
|
+
* source identifier (HA entities, vendor MAC, …); omitted when the
|
|
6342
|
+
* synthetic backfill is in effect. */
|
|
6343
|
+
sourceInfo: SourceInfoSchema.optional()
|
|
6344
|
+
});
|
|
6296
6345
|
/**
|
|
6297
|
-
*
|
|
6298
|
-
*
|
|
6299
|
-
*
|
|
6300
|
-
*
|
|
6301
|
-
*
|
|
6302
|
-
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
6303
|
-
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
6304
|
-
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
6305
|
-
* (there is no `Object.prototype` in the chain), so those names are not
|
|
6306
|
-
* callable — they are simply "unknown function" at parse time.
|
|
6307
|
-
*
|
|
6308
|
-
* Every numeric argument is validated as a finite number and every numeric
|
|
6309
|
-
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
6310
|
-
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
6311
|
-
* closed rather than emitting a garbage value.
|
|
6346
|
+
* Result of a live field test (e.g. probing an RTSP URL during device
|
|
6347
|
+
* creation). Matches the UI-side `FieldProbeResult` in
|
|
6348
|
+
* `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
|
|
6349
|
+
* returned `labels` as chips next to the input.
|
|
6312
6350
|
*/
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
6319
|
-
return value;
|
|
6320
|
-
}
|
|
6321
|
-
function finiteResult(value, name) {
|
|
6322
|
-
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
6323
|
-
return value;
|
|
6324
|
-
}
|
|
6325
|
-
function allFiniteNumbers(args, name) {
|
|
6326
|
-
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
6327
|
-
}
|
|
6328
|
-
var INF = Number.POSITIVE_INFINITY;
|
|
6329
|
-
var table = {
|
|
6330
|
-
min: {
|
|
6331
|
-
minArgs: 1,
|
|
6332
|
-
maxArgs: INF,
|
|
6333
|
-
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
6334
|
-
},
|
|
6335
|
-
max: {
|
|
6336
|
-
minArgs: 1,
|
|
6337
|
-
maxArgs: INF,
|
|
6338
|
-
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
6339
|
-
},
|
|
6340
|
-
abs: {
|
|
6341
|
-
minArgs: 1,
|
|
6342
|
-
maxArgs: 1,
|
|
6343
|
-
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
6344
|
-
},
|
|
6345
|
-
floor: {
|
|
6346
|
-
minArgs: 1,
|
|
6347
|
-
maxArgs: 1,
|
|
6348
|
-
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
6349
|
-
},
|
|
6350
|
-
ceil: {
|
|
6351
|
-
minArgs: 1,
|
|
6352
|
-
maxArgs: 1,
|
|
6353
|
-
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
6354
|
-
},
|
|
6355
|
-
sqrt: {
|
|
6356
|
-
minArgs: 1,
|
|
6357
|
-
maxArgs: 1,
|
|
6358
|
-
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
6359
|
-
},
|
|
6360
|
-
round: {
|
|
6361
|
-
minArgs: 1,
|
|
6362
|
-
maxArgs: 2,
|
|
6363
|
-
apply: (args) => {
|
|
6364
|
-
const x = asFiniteNumber(args[0], "round", 0);
|
|
6365
|
-
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
6366
|
-
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
6367
|
-
const factor = 10 ** digits;
|
|
6368
|
-
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
6369
|
-
}
|
|
6370
|
-
},
|
|
6371
|
-
pow: {
|
|
6372
|
-
minArgs: 2,
|
|
6373
|
-
maxArgs: 2,
|
|
6374
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
6375
|
-
},
|
|
6376
|
-
clamp: {
|
|
6377
|
-
minArgs: 3,
|
|
6378
|
-
maxArgs: 3,
|
|
6379
|
-
apply: (args) => {
|
|
6380
|
-
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
6381
|
-
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
6382
|
-
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
6383
|
-
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
6384
|
-
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
6385
|
-
}
|
|
6386
|
-
},
|
|
6387
|
-
avg: {
|
|
6388
|
-
minArgs: 1,
|
|
6389
|
-
maxArgs: INF,
|
|
6390
|
-
apply: (args) => {
|
|
6391
|
-
const nums = allFiniteNumbers(args, "avg");
|
|
6392
|
-
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
6393
|
-
}
|
|
6394
|
-
},
|
|
6395
|
-
sum: {
|
|
6396
|
-
minArgs: 1,
|
|
6397
|
-
maxArgs: INF,
|
|
6398
|
-
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
6399
|
-
},
|
|
6400
|
-
coalesce: {
|
|
6401
|
-
minArgs: 1,
|
|
6402
|
-
maxArgs: INF,
|
|
6403
|
-
apply: (args) => {
|
|
6404
|
-
for (const a of args) if (a !== null) return a;
|
|
6405
|
-
return null;
|
|
6406
|
-
}
|
|
6407
|
-
},
|
|
6408
|
-
age: {
|
|
6409
|
-
minArgs: 2,
|
|
6410
|
-
maxArgs: 2,
|
|
6411
|
-
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
6412
|
-
},
|
|
6413
|
-
convert: {
|
|
6414
|
-
minArgs: 3,
|
|
6415
|
-
maxArgs: 3,
|
|
6416
|
-
apply: (args, hooks) => {
|
|
6417
|
-
const x = asFiniteNumber(args[0], "convert", 0);
|
|
6418
|
-
const from = asString$1(args[1], "convert", 1).trim();
|
|
6419
|
-
const to = asString$1(args[2], "convert", 2).trim();
|
|
6420
|
-
if (hooks.convert) {
|
|
6421
|
-
const out = hooks.convert(x, from, to);
|
|
6422
|
-
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
6423
|
-
return finiteResult(out, "convert");
|
|
6424
|
-
}
|
|
6425
|
-
if (from === to) return x;
|
|
6426
|
-
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
6427
|
-
}
|
|
6428
|
-
}
|
|
6429
|
-
};
|
|
6430
|
-
/** Frozen, null-prototype builtin table. */
|
|
6431
|
-
var EXPRESSION_BUILTINS = Object.freeze(Object.assign(Object.create(null), table));
|
|
6432
|
-
/** The set of valid builtin names — used by the parser to reject unknown
|
|
6433
|
-
* callees at parse time (immediate author feedback). */
|
|
6434
|
-
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
6435
|
-
//#endregion
|
|
6436
|
-
//#region src/expression/tokenizer.ts
|
|
6351
|
+
var FieldProbeResultSchema = z.object({
|
|
6352
|
+
status: z.enum(["ok", "error"]),
|
|
6353
|
+
labels: z.array(z.string()).optional(),
|
|
6354
|
+
error: z.string().optional()
|
|
6355
|
+
});
|
|
6437
6356
|
/**
|
|
6438
|
-
*
|
|
6439
|
-
*
|
|
6440
|
-
*
|
|
6441
|
-
*
|
|
6442
|
-
*
|
|
6443
|
-
*
|
|
6444
|
-
* template literals are lexically impossible.
|
|
6357
|
+
* The output of `getChildCreationSchema` is a UI schema tree. We store
|
|
6358
|
+
* it as `unknown` at the capability layer — the router just passes it
|
|
6359
|
+
* through and the admin UI renders it via `FormBuilder`. The actual
|
|
6360
|
+
* type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
|
|
6361
|
+
* but we deliberately avoid a Zod mirror because the union is large and
|
|
6362
|
+
* not meant for runtime validation at this seam.
|
|
6445
6363
|
*/
|
|
6446
|
-
var
|
|
6447
|
-
|
|
6448
|
-
"
|
|
6449
|
-
"
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
}
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
}
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
*
|
|
6465
|
-
*
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
|
|
6492
|
-
|
|
6493
|
-
|
|
6494
|
-
|
|
6495
|
-
|
|
6496
|
-
|
|
6497
|
-
|
|
6498
|
-
|
|
6499
|
-
|
|
6500
|
-
|
|
6501
|
-
|
|
6502
|
-
|
|
6503
|
-
|
|
6504
|
-
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
});
|
|
6526
|
-
continue;
|
|
6527
|
-
}
|
|
6528
|
-
if (isIdentStart(ch)) {
|
|
6529
|
-
const start = i;
|
|
6530
|
-
while (i < n && isIdentPart(source[i])) i += 1;
|
|
6531
|
-
const text = source.slice(start, i);
|
|
6532
|
-
if (KEYWORDS.has(text)) tokens.push({
|
|
6533
|
-
type: "keyword",
|
|
6534
|
-
keyword: keywordOf(text),
|
|
6535
|
-
pos: start
|
|
6536
|
-
});
|
|
6537
|
-
else tokens.push({
|
|
6538
|
-
type: "identifier",
|
|
6539
|
-
name: text,
|
|
6540
|
-
pos: start
|
|
6541
|
-
});
|
|
6542
|
-
continue;
|
|
6543
|
-
}
|
|
6544
|
-
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
6545
|
-
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
6546
|
-
tokens.push({
|
|
6547
|
-
type: "punct",
|
|
6548
|
-
punct: two,
|
|
6549
|
-
pos: i
|
|
6550
|
-
});
|
|
6551
|
-
i += 2;
|
|
6552
|
-
continue;
|
|
6553
|
-
}
|
|
6554
|
-
if (isSinglePunct(ch)) {
|
|
6555
|
-
tokens.push({
|
|
6556
|
-
type: "punct",
|
|
6557
|
-
punct: ch,
|
|
6558
|
-
pos: i
|
|
6559
|
-
});
|
|
6560
|
-
i += 1;
|
|
6561
|
-
continue;
|
|
6562
|
-
}
|
|
6563
|
-
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
6364
|
+
var CreationSchemaOutputSchema = z.unknown();
|
|
6365
|
+
var deviceProviderCapability = {
|
|
6366
|
+
name: "device-provider",
|
|
6367
|
+
scope: "system",
|
|
6368
|
+
mode: "collection",
|
|
6369
|
+
methods: {
|
|
6370
|
+
start: method(z.void(), z.void(), { kind: "mutation" }),
|
|
6371
|
+
stop: method(z.void(), z.void(), { kind: "mutation" }),
|
|
6372
|
+
getStatus: method(z.void(), ProviderStatusSchema),
|
|
6373
|
+
getDevices: method(z.void(), z.array(z.object({
|
|
6374
|
+
id: z.string(),
|
|
6375
|
+
name: z.string(),
|
|
6376
|
+
type: z.string()
|
|
6377
|
+
}))),
|
|
6378
|
+
supportsDiscovery: method(z.object({}), z.boolean()),
|
|
6379
|
+
/**
|
|
6380
|
+
* Run a network scan. `params` carries optional provider-specific scan
|
|
6381
|
+
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
6382
|
+
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
6383
|
+
* (provider uses its local-network default).
|
|
6384
|
+
*/
|
|
6385
|
+
discoverDevices: method(z.object({ params: z.record(z.string(), z.unknown()).optional() }), z.array(DiscoveryCandidateSchema), {
|
|
6386
|
+
kind: "mutation",
|
|
6387
|
+
auth: "admin"
|
|
6388
|
+
}),
|
|
6389
|
+
/**
|
|
6390
|
+
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
6391
|
+
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
6392
|
+
* `null` when the provider takes no extra scan params — the generic
|
|
6393
|
+
* aggregated scan never renders this; the per-integration scan does.
|
|
6394
|
+
*/
|
|
6395
|
+
getDiscoveryParamsSchema: method(z.object({}), CreationSchemaOutputSchema),
|
|
6396
|
+
/**
|
|
6397
|
+
* The DeviceType this provider creates via manual add (Camera for
|
|
6398
|
+
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
6399
|
+
* provider does not support manual creation. Lets the Add-Device dialog
|
|
6400
|
+
* pick the right type instead of assuming Camera.
|
|
6401
|
+
*/
|
|
6402
|
+
getManualCreationType: method(z.object({}), z.object({ deviceType: z.enum(DeviceType).nullable() })),
|
|
6403
|
+
adoptDiscoveredDevice: method(z.object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
6404
|
+
kind: "mutation",
|
|
6405
|
+
auth: "admin"
|
|
6406
|
+
}),
|
|
6407
|
+
supportsManualCreation: method(z.object({}), z.boolean()),
|
|
6408
|
+
/**
|
|
6409
|
+
* Fetch the creation form schema for a given DeviceType. Returns
|
|
6410
|
+
* `null` when the provider does not support manually creating
|
|
6411
|
+
* devices of that type. The output is a `ConfigUISchema` — the
|
|
6412
|
+
* router type-asserts it at the boundary.
|
|
6413
|
+
*/
|
|
6414
|
+
getChildCreationSchema: method(z.object({ type: z.enum(DeviceType) }), CreationSchemaOutputSchema),
|
|
6415
|
+
createDevice: method(z.object({
|
|
6416
|
+
type: z.enum(DeviceType),
|
|
6417
|
+
config: z.record(z.string(), z.unknown())
|
|
6418
|
+
}), DeviceSummarySchema, {
|
|
6419
|
+
kind: "mutation",
|
|
6420
|
+
auth: "admin"
|
|
6421
|
+
}),
|
|
6422
|
+
/**
|
|
6423
|
+
* Test a single field in the creation form before the device has
|
|
6424
|
+
* been persisted. Typical use: probing an RTSP URL entered by the
|
|
6425
|
+
* user. Providers that don't support field probing return
|
|
6426
|
+
* `{ success: true, message: 'Field test not supported' }`.
|
|
6427
|
+
*
|
|
6428
|
+
* `formValues` is the live snapshot of every field in the form at
|
|
6429
|
+
* the moment the user clicked Test — useful for probes that depend
|
|
6430
|
+
* on multiple fields together (e.g. Reolink autodetect needs host
|
|
6431
|
+
* + credentials + UID + transport mode in a single call). Optional
|
|
6432
|
+
* for backwards compatibility; providers free to ignore it.
|
|
6433
|
+
*/
|
|
6434
|
+
testCreationField: method(z.object({
|
|
6435
|
+
type: z.enum(DeviceType),
|
|
6436
|
+
key: z.string(),
|
|
6437
|
+
value: z.unknown(),
|
|
6438
|
+
formValues: z.record(z.string(), z.unknown()).optional()
|
|
6439
|
+
}), FieldProbeResultSchema, {
|
|
6440
|
+
kind: "mutation",
|
|
6441
|
+
auth: "admin"
|
|
6442
|
+
})
|
|
6564
6443
|
}
|
|
6565
|
-
|
|
6566
|
-
type: "eof",
|
|
6567
|
-
pos: n
|
|
6568
|
-
});
|
|
6569
|
-
return tokens;
|
|
6570
|
-
}
|
|
6571
|
-
function keywordOf(text) {
|
|
6572
|
-
if (text === "true") return "true";
|
|
6573
|
-
if (text === "false") return "false";
|
|
6574
|
-
return "null";
|
|
6575
|
-
}
|
|
6576
|
-
function isSinglePunct(ch) {
|
|
6577
|
-
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
6578
|
-
}
|
|
6444
|
+
};
|
|
6579
6445
|
//#endregion
|
|
6580
|
-
//#region src/
|
|
6446
|
+
//#region src/capabilities/device-manager.cap.ts
|
|
6581
6447
|
/**
|
|
6582
|
-
*
|
|
6448
|
+
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
6449
|
+
* live registry access, and all management operations into a single tRPC surface.
|
|
6583
6450
|
*
|
|
6584
|
-
*
|
|
6585
|
-
*
|
|
6586
|
-
*
|
|
6587
|
-
*
|
|
6588
|
-
* function is rejected immediately (author feedback) and a persisted expression
|
|
6589
|
-
* that references a since-removed builtin degrades at read.
|
|
6451
|
+
* Replaces:
|
|
6452
|
+
* - `device-persistence` capability (persistence methods absorbed here)
|
|
6453
|
+
* - `device-management.router.ts` (deleted in Phase 2)
|
|
6454
|
+
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
6590
6455
|
*
|
|
6591
|
-
*
|
|
6592
|
-
*
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
"||": 1,
|
|
6597
|
-
"&&": 2,
|
|
6598
|
-
"==": 3,
|
|
6599
|
-
"!=": 3,
|
|
6600
|
-
"<": 4,
|
|
6601
|
-
"<=": 4,
|
|
6602
|
-
">": 4,
|
|
6603
|
-
">=": 4,
|
|
6604
|
-
"+": 5,
|
|
6605
|
-
"-": 5,
|
|
6606
|
-
"*": 6,
|
|
6607
|
-
"/": 6,
|
|
6608
|
-
"%": 6
|
|
6609
|
-
};
|
|
6610
|
-
function isLogicalOp(op) {
|
|
6611
|
-
return op === "&&" || op === "||";
|
|
6612
|
-
}
|
|
6613
|
-
function isBinaryOp(op) {
|
|
6614
|
-
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
6615
|
-
}
|
|
6616
|
-
var Parser = class {
|
|
6617
|
-
tokens;
|
|
6618
|
-
pos = 0;
|
|
6619
|
-
nodeCount = 0;
|
|
6620
|
-
identifiers = /* @__PURE__ */ new Set();
|
|
6621
|
-
callees = /* @__PURE__ */ new Set();
|
|
6622
|
-
constructor(tokens) {
|
|
6623
|
-
this.tokens = tokens;
|
|
6624
|
-
}
|
|
6625
|
-
parse() {
|
|
6626
|
-
const ast = this.parseTernary();
|
|
6627
|
-
const tok = this.peek();
|
|
6628
|
-
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
6629
|
-
return {
|
|
6630
|
-
ast,
|
|
6631
|
-
identifiers: this.identifiers,
|
|
6632
|
-
callees: this.callees,
|
|
6633
|
-
nodeCount: this.nodeCount
|
|
6634
|
-
};
|
|
6635
|
-
}
|
|
6636
|
-
peek() {
|
|
6637
|
-
return this.tokens[this.pos];
|
|
6638
|
-
}
|
|
6639
|
-
next() {
|
|
6640
|
-
return this.tokens[this.pos++];
|
|
6641
|
-
}
|
|
6642
|
-
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
6643
|
-
expectPunct(punct) {
|
|
6644
|
-
const tok = this.peek();
|
|
6645
|
-
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
6646
|
-
this.pos += 1;
|
|
6647
|
-
}
|
|
6648
|
-
matchPunct(punct) {
|
|
6649
|
-
const tok = this.peek();
|
|
6650
|
-
if (tok.type === "punct" && tok.punct === punct) {
|
|
6651
|
-
this.pos += 1;
|
|
6652
|
-
return true;
|
|
6653
|
-
}
|
|
6654
|
-
return false;
|
|
6655
|
-
}
|
|
6656
|
-
countNode() {
|
|
6657
|
-
this.nodeCount += 1;
|
|
6658
|
-
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
6659
|
-
}
|
|
6660
|
-
parseTernary() {
|
|
6661
|
-
const test = this.parseBinary(1);
|
|
6662
|
-
if (this.matchPunct("?")) {
|
|
6663
|
-
const consequent = this.parseTernary();
|
|
6664
|
-
this.expectPunct(":");
|
|
6665
|
-
const alternate = this.parseTernary();
|
|
6666
|
-
this.countNode();
|
|
6667
|
-
return {
|
|
6668
|
-
kind: "conditional",
|
|
6669
|
-
test,
|
|
6670
|
-
consequent,
|
|
6671
|
-
alternate
|
|
6672
|
-
};
|
|
6673
|
-
}
|
|
6674
|
-
return test;
|
|
6675
|
-
}
|
|
6676
|
-
parseBinary(minPrec) {
|
|
6677
|
-
let left = this.parseUnary();
|
|
6678
|
-
for (;;) {
|
|
6679
|
-
const tok = this.peek();
|
|
6680
|
-
if (tok.type !== "punct") break;
|
|
6681
|
-
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
6682
|
-
if (prec === void 0 || prec < minPrec) break;
|
|
6683
|
-
const op = tok.punct;
|
|
6684
|
-
this.pos += 1;
|
|
6685
|
-
const right = this.parseBinary(prec + 1);
|
|
6686
|
-
this.countNode();
|
|
6687
|
-
if (isLogicalOp(op)) left = {
|
|
6688
|
-
kind: "logical",
|
|
6689
|
-
op,
|
|
6690
|
-
left,
|
|
6691
|
-
right
|
|
6692
|
-
};
|
|
6693
|
-
else if (isBinaryOp(op)) left = {
|
|
6694
|
-
kind: "binary",
|
|
6695
|
-
op,
|
|
6696
|
-
left,
|
|
6697
|
-
right
|
|
6698
|
-
};
|
|
6699
|
-
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
6700
|
-
}
|
|
6701
|
-
return left;
|
|
6702
|
-
}
|
|
6703
|
-
parseUnary() {
|
|
6704
|
-
const tok = this.peek();
|
|
6705
|
-
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
6706
|
-
const op = tok.punct;
|
|
6707
|
-
this.pos += 1;
|
|
6708
|
-
const operand = this.parseUnary();
|
|
6709
|
-
this.countNode();
|
|
6710
|
-
return {
|
|
6711
|
-
kind: "unary",
|
|
6712
|
-
op,
|
|
6713
|
-
operand
|
|
6714
|
-
};
|
|
6715
|
-
}
|
|
6716
|
-
return this.parsePrimary();
|
|
6717
|
-
}
|
|
6718
|
-
parsePrimary() {
|
|
6719
|
-
const tok = this.next();
|
|
6720
|
-
switch (tok.type) {
|
|
6721
|
-
case "number":
|
|
6722
|
-
this.countNode();
|
|
6723
|
-
return {
|
|
6724
|
-
kind: "literal",
|
|
6725
|
-
value: tok.value
|
|
6726
|
-
};
|
|
6727
|
-
case "string":
|
|
6728
|
-
this.countNode();
|
|
6729
|
-
return {
|
|
6730
|
-
kind: "literal",
|
|
6731
|
-
value: tok.value
|
|
6732
|
-
};
|
|
6733
|
-
case "keyword":
|
|
6734
|
-
this.countNode();
|
|
6735
|
-
return {
|
|
6736
|
-
kind: "literal",
|
|
6737
|
-
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
6738
|
-
};
|
|
6739
|
-
case "identifier": {
|
|
6740
|
-
const nextTok = this.peek();
|
|
6741
|
-
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
6742
|
-
this.identifiers.add(tok.name);
|
|
6743
|
-
this.countNode();
|
|
6744
|
-
return {
|
|
6745
|
-
kind: "identifier",
|
|
6746
|
-
name: tok.name
|
|
6747
|
-
};
|
|
6748
|
-
}
|
|
6749
|
-
case "punct":
|
|
6750
|
-
if (tok.punct === "(") {
|
|
6751
|
-
const inner = this.parseTernary();
|
|
6752
|
-
this.expectPunct(")");
|
|
6753
|
-
return inner;
|
|
6754
|
-
}
|
|
6755
|
-
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
6756
|
-
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
6757
|
-
}
|
|
6758
|
-
}
|
|
6759
|
-
parseCall(callee, pos) {
|
|
6760
|
-
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
6761
|
-
this.expectPunct("(");
|
|
6762
|
-
const args = [];
|
|
6763
|
-
if (!this.matchPunct(")")) for (;;) {
|
|
6764
|
-
args.push(this.parseTernary());
|
|
6765
|
-
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
6766
|
-
if (this.matchPunct(",")) continue;
|
|
6767
|
-
this.expectPunct(")");
|
|
6768
|
-
break;
|
|
6769
|
-
}
|
|
6770
|
-
this.callees.add(callee);
|
|
6771
|
-
this.countNode();
|
|
6772
|
-
return {
|
|
6773
|
-
kind: "call",
|
|
6774
|
-
callee,
|
|
6775
|
-
args
|
|
6776
|
-
};
|
|
6777
|
-
}
|
|
6778
|
-
};
|
|
6779
|
-
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
6780
|
-
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
6781
|
-
function parseExpression(source) {
|
|
6782
|
-
return new Parser(tokenize(source)).parse();
|
|
6783
|
-
}
|
|
6784
|
-
//#endregion
|
|
6785
|
-
//#region src/expression/compile.ts
|
|
6786
|
-
/**
|
|
6787
|
-
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
6788
|
-
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
6789
|
-
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
6790
|
-
* one per read on a hot resolve path.
|
|
6791
|
-
*
|
|
6792
|
-
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
6793
|
-
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
6794
|
-
* callers is safe and maximises hit rate.
|
|
6795
|
-
*/
|
|
6796
|
-
var cache = /* @__PURE__ */ new Map();
|
|
6797
|
-
function getCached(source) {
|
|
6798
|
-
const hit = cache.get(source);
|
|
6799
|
-
if (hit !== void 0) {
|
|
6800
|
-
cache.delete(source);
|
|
6801
|
-
cache.set(source, hit);
|
|
6802
|
-
return hit;
|
|
6803
|
-
}
|
|
6804
|
-
let result;
|
|
6805
|
-
try {
|
|
6806
|
-
result = {
|
|
6807
|
-
ok: true,
|
|
6808
|
-
parsed: parseExpression(source)
|
|
6809
|
-
};
|
|
6810
|
-
} catch (err) {
|
|
6811
|
-
result = {
|
|
6812
|
-
ok: false,
|
|
6813
|
-
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
6814
|
-
};
|
|
6815
|
-
}
|
|
6816
|
-
cache.set(source, result);
|
|
6817
|
-
if (cache.size > 256) {
|
|
6818
|
-
const oldest = cache.keys().next().value;
|
|
6819
|
-
if (oldest !== void 0) cache.delete(oldest);
|
|
6820
|
-
}
|
|
6821
|
-
return result;
|
|
6822
|
-
}
|
|
6823
|
-
/** Compile `source` to a `ParsedExpression`, throwing `ExpressionParseError`
|
|
6824
|
-
* on failure. LRU/negative-cached. */
|
|
6825
|
-
function compileExpression(source) {
|
|
6826
|
-
const result = getCached(source);
|
|
6827
|
-
if (result.ok) return result.parsed;
|
|
6828
|
-
throw new ExpressionParseError(result.error);
|
|
6829
|
-
}
|
|
6830
|
-
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
6831
|
-
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
6832
|
-
function compileExpressionSafe(source) {
|
|
6833
|
-
return getCached(source);
|
|
6834
|
-
}
|
|
6835
|
-
//#endregion
|
|
6836
|
-
//#region src/expression/evaluator.ts
|
|
6837
|
-
/**
|
|
6838
|
-
* Tree-walking evaluator for the safe expression mini-language.
|
|
6456
|
+
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
6457
|
+
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
6458
|
+
* - No nodeId routing needed — this is a pure hub singleton.
|
|
6459
|
+
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
6460
|
+
* - No shadow registry or cross-node aggregation required.
|
|
6839
6461
|
*
|
|
6840
|
-
*
|
|
6841
|
-
*
|
|
6842
|
-
* enumerable binding entries, so `name in scope` is a pure own-key check and
|
|
6843
|
-
* `constructor` / `__proto__` / `toString` are plain unknown identifiers.
|
|
6844
|
-
* - Performs ZERO I/O and never touches `globalThis` / `Date` / `Math`
|
|
6845
|
-
* directly — the only external calls are into the frozen builtin table.
|
|
6846
|
-
* - The grammar has no loops/recursion/lambdas, so a walk is O(nodeCount) by
|
|
6847
|
-
* construction; the step counter is defense-in-depth for a crafted max-size
|
|
6848
|
-
* AST. Nothing blocks: there are no timers, awaits or unbounded loops.
|
|
6849
|
-
*/
|
|
6850
|
-
var EMPTY_HOOKS = Object.freeze({});
|
|
6851
|
-
/** Build a null-prototype scope from own-enumerable binding entries. Inherited
|
|
6852
|
-
* keys of the input (e.g. from a `{__proto__: {...}}` payload) are NOT copied,
|
|
6853
|
-
* so nothing smuggles in via the prototype chain. */
|
|
6854
|
-
function createExpressionScope(bindings) {
|
|
6855
|
-
const scope = Object.create(null);
|
|
6856
|
-
for (const key of Object.keys(bindings)) if (Object.prototype.hasOwnProperty.call(bindings, key)) scope[key] = bindings[key];
|
|
6857
|
-
return scope;
|
|
6858
|
-
}
|
|
6859
|
-
function isFiniteNumber(value) {
|
|
6860
|
-
return typeof value === "number" && Number.isFinite(value);
|
|
6861
|
-
}
|
|
6862
|
-
/** JS truthiness of a primitive value. */
|
|
6863
|
-
function truthy(value) {
|
|
6864
|
-
return Boolean(value);
|
|
6865
|
-
}
|
|
6866
|
-
function requireFinite(value, context) {
|
|
6867
|
-
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${context} produced a non-finite result`);
|
|
6868
|
-
return value;
|
|
6869
|
-
}
|
|
6870
|
-
function step(ctx) {
|
|
6871
|
-
ctx.steps += 1;
|
|
6872
|
-
if (ctx.steps > ctx.maxSteps) throw new ExpressionEvalError("expression evaluation step budget exceeded");
|
|
6873
|
-
}
|
|
6874
|
-
function evalNode(node, ctx) {
|
|
6875
|
-
step(ctx);
|
|
6876
|
-
switch (node.kind) {
|
|
6877
|
-
case "literal": return node.value;
|
|
6878
|
-
case "identifier":
|
|
6879
|
-
if (!(node.name in ctx.scope)) throw new ExpressionEvalError(`unknown identifier: ${node.name}`);
|
|
6880
|
-
return ctx.scope[node.name];
|
|
6881
|
-
case "unary": return evalUnary(node.op, evalNode(node.operand, ctx));
|
|
6882
|
-
case "binary": return evalBinary(node.op, evalNode(node.left, ctx), evalNode(node.right, ctx));
|
|
6883
|
-
case "logical": {
|
|
6884
|
-
const left = evalNode(node.left, ctx);
|
|
6885
|
-
if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx) : left;
|
|
6886
|
-
return truthy(left) ? left : evalNode(node.right, ctx);
|
|
6887
|
-
}
|
|
6888
|
-
case "conditional": return truthy(evalNode(node.test, ctx)) ? evalNode(node.consequent, ctx) : evalNode(node.alternate, ctx);
|
|
6889
|
-
case "call": return evalCall(node.callee, node.args.map((a) => evalNode(a, ctx)), ctx.hooks);
|
|
6890
|
-
}
|
|
6891
|
-
}
|
|
6892
|
-
function evalUnary(op, operand) {
|
|
6893
|
-
if (op === "!") return !truthy(operand);
|
|
6894
|
-
if (!isFiniteNumber(operand)) throw new ExpressionEvalError("unary \"-\" requires a finite number");
|
|
6895
|
-
return requireFinite(-operand, "unary \"-\"");
|
|
6896
|
-
}
|
|
6897
|
-
function evalBinary(op, left, right) {
|
|
6898
|
-
switch (op) {
|
|
6899
|
-
case "==": return left === right;
|
|
6900
|
-
case "!=": return left !== right;
|
|
6901
|
-
case "+":
|
|
6902
|
-
if (typeof left === "string" && typeof right === "string") return left + right;
|
|
6903
|
-
if (isFiniteNumber(left) && isFiniteNumber(right)) return requireFinite(left + right, "\"+\"");
|
|
6904
|
-
throw new ExpressionEvalError("\"+\" requires two numbers or two strings");
|
|
6905
|
-
case "-":
|
|
6906
|
-
case "*":
|
|
6907
|
-
case "/":
|
|
6908
|
-
case "%":
|
|
6909
|
-
if (!isFiniteNumber(left) || !isFiniteNumber(right)) throw new ExpressionEvalError(`"${op}" requires two finite numbers`);
|
|
6910
|
-
return requireFinite(op === "-" ? left - right : op === "*" ? left * right : op === "/" ? left / right : left % right, `"${op}"`);
|
|
6911
|
-
case "<":
|
|
6912
|
-
case "<=":
|
|
6913
|
-
case ">":
|
|
6914
|
-
case ">=":
|
|
6915
|
-
if (isFiniteNumber(left) && isFiniteNumber(right)) return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
|
|
6916
|
-
if (typeof left === "string" && typeof right === "string") return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
|
|
6917
|
-
throw new ExpressionEvalError(`"${op}" requires two numbers or two strings`);
|
|
6918
|
-
}
|
|
6919
|
-
}
|
|
6920
|
-
function evalCall(callee, args, hooks) {
|
|
6921
|
-
if (!Object.prototype.hasOwnProperty.call(EXPRESSION_BUILTINS, callee)) throw new ExpressionEvalError(`unknown function: ${callee}`);
|
|
6922
|
-
const builtin = EXPRESSION_BUILTINS[callee];
|
|
6923
|
-
if (args.length < builtin.minArgs || args.length > builtin.maxArgs) throw new ExpressionEvalError(`${callee}: wrong number of arguments (${args.length})`);
|
|
6924
|
-
return builtin.apply(args, hooks);
|
|
6925
|
-
}
|
|
6926
|
-
/** Evaluate an AST node against a scope. Throws `ExpressionEvalError` on any
|
|
6927
|
-
* runtime failure (unknown identifier, type mismatch, non-finite result,
|
|
6928
|
-
* step-budget exhaustion). */
|
|
6929
|
-
function evaluateAst(node, scope, opts) {
|
|
6930
|
-
return evalNode(node, {
|
|
6931
|
-
scope,
|
|
6932
|
-
hooks: opts?.hooks ?? EMPTY_HOOKS,
|
|
6933
|
-
maxSteps: opts?.maxSteps ?? 4096,
|
|
6934
|
-
steps: 0
|
|
6935
|
-
});
|
|
6936
|
-
}
|
|
6937
|
-
//#endregion
|
|
6938
|
-
//#region src/expression/link-expression.ts
|
|
6939
|
-
/**
|
|
6940
|
-
* DeviceLink-facing helpers for the expression engine — the single seam both
|
|
6941
|
-
* resolver channels (async provider-read + sync mirror) and the wire-schema
|
|
6942
|
-
* `superRefine` share, so validation and evaluation semantics stay identical
|
|
6943
|
-
* everywhere.
|
|
6944
|
-
*/
|
|
6945
|
-
/** The `now` epoch-ms binding is auto-injected into every evaluation and is a
|
|
6946
|
-
* reserved binding name (authors may not rebind it). */
|
|
6947
|
-
var EXPRESSION_INJECTED_NOW = "now";
|
|
6948
|
-
/**
|
|
6949
|
-
* Coerce an untrusted `getByPath` / mirror read to an `ExpressionValue`.
|
|
6950
|
-
* Non-primitive values (objects, arrays, `undefined`, functions, bigint,
|
|
6951
|
-
* symbol) and non-finite numbers become `undefined` so the caller can apply
|
|
6952
|
-
* its binding-miss policy (→ `null`). `null` itself is a valid value.
|
|
6953
|
-
*/
|
|
6954
|
-
function toExpressionValue(raw) {
|
|
6955
|
-
if (raw === null) return null;
|
|
6956
|
-
if (typeof raw === "string") return raw;
|
|
6957
|
-
if (typeof raw === "boolean") return raw;
|
|
6958
|
-
if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
|
|
6959
|
-
}
|
|
6960
|
-
/**
|
|
6961
|
-
* Author-time validation. Returns `null` when the source is valid, else a
|
|
6962
|
-
* human-readable error message. Checks: the expression compiles; binding count
|
|
6963
|
-
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
6964
|
-
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
6965
|
-
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
6966
|
-
*/
|
|
6967
|
-
function validateExpressionSource(src) {
|
|
6968
|
-
const names = Object.keys(src.bindings);
|
|
6969
|
-
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
6970
|
-
for (const name of names) {
|
|
6971
|
-
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
6972
|
-
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
6973
|
-
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
6974
|
-
}
|
|
6975
|
-
const compiled = compileExpressionSafe(src.expr);
|
|
6976
|
-
if (!compiled.ok) return compiled.error;
|
|
6977
|
-
const bound = new Set(names);
|
|
6978
|
-
for (const id of compiled.parsed.identifiers) {
|
|
6979
|
-
if (id === "now") continue;
|
|
6980
|
-
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
6981
|
-
}
|
|
6982
|
-
return null;
|
|
6983
|
-
}
|
|
6984
|
-
/**
|
|
6985
|
-
* Shared read-path evaluation for BOTH resolver channels. Builds a null-proto
|
|
6986
|
-
* scope from `bindingValues` plus the injected `now` (supplied by the caller
|
|
6987
|
-
* for determinism/testability), compiles via the LRU, and evaluates. Any
|
|
6988
|
-
* failure (parse or eval) returns `{ ok: false }` — the caller treats that as
|
|
6989
|
-
* "skip this link".
|
|
6462
|
+
* Forked workers register devices back to the hub via `ctx.devices`
|
|
6463
|
+
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
6990
6464
|
*/
|
|
6991
|
-
|
|
6992
|
-
|
|
6993
|
-
|
|
6994
|
-
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
|
|
6998
|
-
|
|
6999
|
-
|
|
7000
|
-
|
|
7001
|
-
try {
|
|
7002
|
-
return {
|
|
7003
|
-
ok: true,
|
|
7004
|
-
value: evaluateAst(compiled.parsed.ast, scope, opts)
|
|
7005
|
-
};
|
|
7006
|
-
} catch (err) {
|
|
7007
|
-
return {
|
|
7008
|
-
ok: false,
|
|
7009
|
-
error: err instanceof ExpressionEvalError ? err.message : String(err)
|
|
7010
|
-
};
|
|
7011
|
-
}
|
|
7012
|
-
}
|
|
7013
|
-
//#endregion
|
|
7014
|
-
//#region src/capabilities/device-provider.cap.ts
|
|
7015
|
-
var ProviderStatusSchema = z.object({
|
|
7016
|
-
connected: z.boolean(),
|
|
7017
|
-
deviceCount: z.number(),
|
|
7018
|
-
error: z.string().optional()
|
|
6465
|
+
/** One child-placement directive on a container's `childLayout`. Structurally
|
|
6466
|
+
* identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
|
|
6467
|
+
* shape for the same field. The child is identified by its re-sync-stable
|
|
6468
|
+
* accessory `stableIdSuffix` (`childKey`); listed children are grouped into
|
|
6469
|
+
* named accordion sections (with optional intra-section order). */
|
|
6470
|
+
var ChildLayoutEntrySchema = z.object({
|
|
6471
|
+
childKey: z.string(),
|
|
6472
|
+
section: z.string(),
|
|
6473
|
+
order: z.number().optional(),
|
|
6474
|
+
collapsed: z.boolean().optional()
|
|
7019
6475
|
});
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
6476
|
+
/** Cap-wire shape of a per-cap display refinement — mirrors
|
|
6477
|
+
* `DeviceCapDisplayOverride` in `device-management.ts`. */
|
|
6478
|
+
var DeviceCapDisplayOverrideSchema = z.object({
|
|
6479
|
+
unit: z.string().min(1).optional(),
|
|
6480
|
+
precision: z.number().int().min(0).max(10).optional()
|
|
7025
6481
|
});
|
|
7026
|
-
/**
|
|
7027
|
-
*
|
|
7028
|
-
* `
|
|
7029
|
-
|
|
7030
|
-
|
|
7031
|
-
|
|
7032
|
-
|
|
7033
|
-
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
6482
|
+
/** Cap-wire shape of an operator-authored per-device display override —
|
|
6483
|
+
* mirrors `DeviceDisplayOverride` in `device-management.ts`. `precision`
|
|
6484
|
+
* bounds mirror `numeric-sensor.cap.ts` (`int 0-10`). */
|
|
6485
|
+
var DeviceDisplayOverrideSchema = z.object({
|
|
6486
|
+
icon: z.string().min(1).optional(),
|
|
6487
|
+
label: z.string().min(1).optional(),
|
|
6488
|
+
unit: z.string().min(1).optional(),
|
|
6489
|
+
precision: z.number().int().min(0).max(10).optional(),
|
|
6490
|
+
hidden: z.boolean().optional(),
|
|
6491
|
+
perCap: z.record(z.string(), DeviceCapDisplayOverrideSchema).optional()
|
|
6492
|
+
});
|
|
6493
|
+
/** Cap-wire shape of a per-role display default — mirrors `RoleDisplayDefault`
|
|
6494
|
+
* in `device-management.ts`. Keyed by `DeviceRole` string (role strings cross
|
|
6495
|
+
* the wire as plain strings everywhere else — cf. `DeviceInfoSchema.role`). */
|
|
6496
|
+
var RoleDisplayDefaultSchema = z.object({
|
|
6497
|
+
unit: z.string().min(1).optional(),
|
|
6498
|
+
precision: z.number().int().min(0).max(10).optional(),
|
|
6499
|
+
icon: z.string().min(1).optional()
|
|
7043
6500
|
});
|
|
7044
6501
|
/**
|
|
7045
|
-
*
|
|
7046
|
-
*
|
|
7047
|
-
*
|
|
6502
|
+
* Serializable projection of a live IDevice.
|
|
6503
|
+
* Returned by listAll, getDevice, getChildren.
|
|
6504
|
+
* Live methods (getStreamSources, getConfigSchema) are separate calls.
|
|
7048
6505
|
*/
|
|
7049
|
-
var
|
|
6506
|
+
var DeviceInfoSchema = z.object({
|
|
6507
|
+
/** Progressive, system-wide unique number. Allocated synchronously by
|
|
6508
|
+
* `device-manager.allocateDeviceId` BEFORE the owning `IDevice` is
|
|
6509
|
+
* constructed, so every live device exposes an `id` — no transient
|
|
6510
|
+
* null window. Distinct from `stableId`, which is unique per
|
|
6511
|
+
* integration. Ids are monotonic and never reissued on removal. */
|
|
7050
6512
|
id: z.number(),
|
|
7051
6513
|
stableId: z.string(),
|
|
7052
6514
|
addonId: z.string(),
|
|
7053
|
-
type: z.
|
|
6515
|
+
type: z.enum(DeviceType),
|
|
7054
6516
|
name: z.string(),
|
|
6517
|
+
/** Operator-organisational location label. `null` when unset. */
|
|
6518
|
+
location: z.string().nullable(),
|
|
6519
|
+
/** Soft-disabled flag. */
|
|
6520
|
+
disabled: z.boolean(),
|
|
7055
6521
|
parentDeviceId: z.number().nullable(),
|
|
6522
|
+
/** Optional semantic role — `DeviceRole` string. null for top-level devices. */
|
|
6523
|
+
role: z.string().nullable().optional(),
|
|
7056
6524
|
online: z.boolean(),
|
|
6525
|
+
/** True when the device's initial feature-probe has completed (or it has
|
|
6526
|
+
* no probe) — exported shape is stable; exporters gate advertise on this.
|
|
6527
|
+
* Optional for deploy-order resilience: a record produced by an older
|
|
6528
|
+
* device-manager build omits it, and consumers treat absent as "not ready"
|
|
6529
|
+
* (export gate carries forward, never advertises a partial shape). */
|
|
6530
|
+
probed: z.boolean().optional(),
|
|
7057
6531
|
features: z.array(z.string()),
|
|
7058
|
-
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
* source identifier (HA entities, vendor MAC, …); omitted when the
|
|
7062
|
-
* synthetic backfill is in effect. */
|
|
7063
|
-
sourceInfo: SourceInfoSchema.optional()
|
|
7064
|
-
});
|
|
7065
|
-
/**
|
|
7066
|
-
* Result of a live field test (e.g. probing an RTSP URL during device
|
|
7067
|
-
* creation). Matches the UI-side `FieldProbeResult` in
|
|
7068
|
-
* `interfaces/config-ui.ts` — the admin `FormBuilder` renders the
|
|
7069
|
-
* returned `labels` as chips next to the input.
|
|
7070
|
-
*/
|
|
7071
|
-
var FieldProbeResultSchema = z.object({
|
|
7072
|
-
status: z.enum(["ok", "error"]),
|
|
7073
|
-
labels: z.array(z.string()).optional(),
|
|
7074
|
-
error: z.string().optional()
|
|
7075
|
-
});
|
|
7076
|
-
/**
|
|
7077
|
-
* The output of `getChildCreationSchema` is a UI schema tree. We store
|
|
7078
|
-
* it as `unknown` at the capability layer — the router just passes it
|
|
7079
|
-
* through and the admin UI renders it via `FormBuilder`. The actual
|
|
7080
|
-
* type is `ConfigUISchema` (see `packages/types/src/interfaces/config-ui.ts`),
|
|
7081
|
-
* but we deliberately avoid a Zod mirror because the union is large and
|
|
7082
|
-
* not meant for runtime validation at this seam.
|
|
7083
|
-
*/
|
|
7084
|
-
var CreationSchemaOutputSchema = z.unknown();
|
|
7085
|
-
var deviceProviderCapability = {
|
|
7086
|
-
name: "device-provider",
|
|
7087
|
-
scope: "system",
|
|
7088
|
-
mode: "collection",
|
|
7089
|
-
methods: {
|
|
7090
|
-
start: method(z.void(), z.void(), { kind: "mutation" }),
|
|
7091
|
-
stop: method(z.void(), z.void(), { kind: "mutation" }),
|
|
7092
|
-
getStatus: method(z.void(), ProviderStatusSchema),
|
|
7093
|
-
getDevices: method(z.void(), z.array(z.object({
|
|
7094
|
-
id: z.string(),
|
|
7095
|
-
name: z.string(),
|
|
7096
|
-
type: z.string()
|
|
7097
|
-
}))),
|
|
7098
|
-
supportsDiscovery: method(z.object({}), z.boolean()),
|
|
7099
|
-
/**
|
|
7100
|
-
* Run a network scan. `params` carries optional provider-specific scan
|
|
7101
|
-
* inputs (e.g. a broadcast address / subnet for cross-subnet discovery),
|
|
7102
|
-
* shaped by `getDiscoveryParamsSchema`. Omitted for the generic scan
|
|
7103
|
-
* (provider uses its local-network default).
|
|
7104
|
-
*/
|
|
7105
|
-
discoverDevices: method(z.object({ params: z.record(z.string(), z.unknown()).optional() }), z.array(DiscoveryCandidateSchema), {
|
|
7106
|
-
kind: "mutation",
|
|
7107
|
-
auth: "admin"
|
|
7108
|
-
}),
|
|
7109
|
-
/**
|
|
7110
|
-
* Optional form schema (`ConfigUISchema`) for the EXTRA per-scan inputs a
|
|
7111
|
-
* provider accepts (e.g. Gree's broadcast address for a different subnet).
|
|
7112
|
-
* `null` when the provider takes no extra scan params — the generic
|
|
7113
|
-
* aggregated scan never renders this; the per-integration scan does.
|
|
7114
|
-
*/
|
|
7115
|
-
getDiscoveryParamsSchema: method(z.object({}), CreationSchemaOutputSchema),
|
|
7116
|
-
/**
|
|
7117
|
-
* The DeviceType this provider creates via manual add (Camera for
|
|
7118
|
-
* Reolink/ONVIF, Container for Gree, Hub for Ecowitt). `null` when the
|
|
7119
|
-
* provider does not support manual creation. Lets the Add-Device dialog
|
|
7120
|
-
* pick the right type instead of assuming Camera.
|
|
7121
|
-
*/
|
|
7122
|
-
getManualCreationType: method(z.object({}), z.object({ deviceType: z.enum(DeviceType).nullable() })),
|
|
7123
|
-
adoptDiscoveredDevice: method(z.object({ candidate: DiscoveryCandidateSchema }), DeviceSummarySchema, {
|
|
7124
|
-
kind: "mutation",
|
|
7125
|
-
auth: "admin"
|
|
7126
|
-
}),
|
|
7127
|
-
supportsManualCreation: method(z.object({}), z.boolean()),
|
|
7128
|
-
/**
|
|
7129
|
-
* Fetch the creation form schema for a given DeviceType. Returns
|
|
7130
|
-
* `null` when the provider does not support manually creating
|
|
7131
|
-
* devices of that type. The output is a `ConfigUISchema` — the
|
|
7132
|
-
* router type-asserts it at the boundary.
|
|
7133
|
-
*/
|
|
7134
|
-
getChildCreationSchema: method(z.object({ type: z.enum(DeviceType) }), CreationSchemaOutputSchema),
|
|
7135
|
-
createDevice: method(z.object({
|
|
7136
|
-
type: z.enum(DeviceType),
|
|
7137
|
-
config: z.record(z.string(), z.unknown())
|
|
7138
|
-
}), DeviceSummarySchema, {
|
|
7139
|
-
kind: "mutation",
|
|
7140
|
-
auth: "admin"
|
|
7141
|
-
}),
|
|
7142
|
-
/**
|
|
7143
|
-
* Test a single field in the creation form before the device has
|
|
7144
|
-
* been persisted. Typical use: probing an RTSP URL entered by the
|
|
7145
|
-
* user. Providers that don't support field probing return
|
|
7146
|
-
* `{ success: true, message: 'Field test not supported' }`.
|
|
7147
|
-
*
|
|
7148
|
-
* `formValues` is the live snapshot of every field in the form at
|
|
7149
|
-
* the moment the user clicked Test — useful for probes that depend
|
|
7150
|
-
* on multiple fields together (e.g. Reolink autodetect needs host
|
|
7151
|
-
* + credentials + UID + transport mode in a single call). Optional
|
|
7152
|
-
* for backwards compatibility; providers free to ignore it.
|
|
7153
|
-
*/
|
|
7154
|
-
testCreationField: method(z.object({
|
|
7155
|
-
type: z.enum(DeviceType),
|
|
7156
|
-
key: z.string(),
|
|
7157
|
-
value: z.unknown(),
|
|
7158
|
-
formValues: z.record(z.string(), z.unknown()).optional()
|
|
7159
|
-
}), FieldProbeResultSchema, {
|
|
7160
|
-
kind: "mutation",
|
|
7161
|
-
auth: "admin"
|
|
7162
|
-
})
|
|
7163
|
-
}
|
|
7164
|
-
};
|
|
7165
|
-
//#endregion
|
|
7166
|
-
//#region src/capabilities/device-manager.cap.ts
|
|
7167
|
-
/**
|
|
7168
|
-
* Device Manager capability — hub-side singleton that unifies device persistence,
|
|
7169
|
-
* live registry access, and all management operations into a single tRPC surface.
|
|
7170
|
-
*
|
|
7171
|
-
* Replaces:
|
|
7172
|
-
* - `device-persistence` capability (persistence methods absorbed here)
|
|
7173
|
-
* - `device-management.router.ts` (deleted in Phase 2)
|
|
7174
|
-
* - `device-ops.router.ts` (compat layer — deleted; device-provider ops absorbed here)
|
|
7175
|
-
*
|
|
7176
|
-
* All device provider addons (rtsp, onvif, frigate, …) are hub-local: they may
|
|
7177
|
-
* fork into separate processes but never run on remote cluster agents. Therefore:
|
|
7178
|
-
* - No nodeId routing needed — this is a pure hub singleton.
|
|
7179
|
-
* - The hub's DeviceRegistry is the single source of truth for all live devices.
|
|
7180
|
-
* - No shadow registry or cross-node aggregation required.
|
|
7181
|
-
*
|
|
7182
|
-
* Forked workers register devices back to the hub via `ctx.devices`
|
|
7183
|
-
* (DeviceManagerApi → ctx.api.deviceManager.registerDevice), same as today.
|
|
7184
|
-
*/
|
|
7185
|
-
/** One child-placement directive on a container's `childLayout`. Structurally
|
|
7186
|
-
* identical to `ChildLayoutEntry` in `device-management.ts` — the cap wire
|
|
7187
|
-
* shape for the same field. The child is identified by its re-sync-stable
|
|
7188
|
-
* accessory `stableIdSuffix` (`childKey`); listed children are grouped into
|
|
7189
|
-
* named accordion sections (with optional intra-section order). */
|
|
7190
|
-
var ChildLayoutEntrySchema = z.object({
|
|
7191
|
-
childKey: z.string(),
|
|
7192
|
-
section: z.string(),
|
|
7193
|
-
order: z.number().optional(),
|
|
7194
|
-
collapsed: z.boolean().optional()
|
|
7195
|
-
});
|
|
7196
|
-
/** Cap-wire shape of a DeviceLink — structurally mirrors `DeviceLink` in
|
|
7197
|
-
* `device-management.ts`. Source is a union: a FIELD source copies a sibling
|
|
7198
|
-
* accessory's status field (`kind` optional/absent for wire compat); a
|
|
7199
|
-
* LITERAL source carries a per-device constant (no sibling is read); a
|
|
7200
|
-
* GLOBAL source (P2e) copies ANY device's status field, addressed by the
|
|
7201
|
-
* source device's full re-sync-stable `stableId`. */
|
|
7202
|
-
var DeviceLinkFieldSourceSchema = z.object({
|
|
7203
|
-
kind: z.literal("field").optional(),
|
|
7204
|
-
sourceKey: z.string(),
|
|
7205
|
-
cap: z.string(),
|
|
7206
|
-
fieldPath: z.string()
|
|
7207
|
-
});
|
|
7208
|
-
var DeviceLinkLiteralSourceSchema = z.object({
|
|
7209
|
-
kind: z.literal("literal"),
|
|
7210
|
-
value: z.union([
|
|
7211
|
-
z.string(),
|
|
7212
|
-
z.number(),
|
|
7213
|
-
z.boolean(),
|
|
7214
|
-
z.null()
|
|
7215
|
-
])
|
|
7216
|
-
});
|
|
7217
|
-
var DeviceLinkGlobalSourceSchema = z.object({
|
|
7218
|
-
kind: z.literal("global"),
|
|
7219
|
-
sourceStableId: z.string(),
|
|
7220
|
-
cap: z.string(),
|
|
7221
|
-
fieldPath: z.string()
|
|
7222
|
-
});
|
|
7223
|
-
/** Expression source (Stage X): compute the target field from N named bindings
|
|
7224
|
-
* via the safe expression engine. Bindings are field | literal | global — never
|
|
7225
|
-
* another expression (no nesting). The `superRefine` runs the SAME author-time
|
|
7226
|
-
* validation as `validateExpressionSource` (compiles the expr, checks binding
|
|
7227
|
-
* names + identifier coverage) so every wire boundary that parses a DeviceLink
|
|
7228
|
-
* (tRPC mount, kernel create pre-seed, projection output) validates-at-write.
|
|
7229
|
-
* Compiles are LRU-cached, so repeated validation of the same expr is a hit. */
|
|
7230
|
-
var DeviceLinkExpressionSourceSchema = z.object({
|
|
7231
|
-
kind: z.literal("expression"),
|
|
7232
|
-
expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
7233
|
-
bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), z.union([
|
|
7234
|
-
DeviceLinkFieldSourceSchema,
|
|
7235
|
-
DeviceLinkLiteralSourceSchema,
|
|
7236
|
-
DeviceLinkGlobalSourceSchema
|
|
7237
|
-
]))
|
|
7238
|
-
}).superRefine((src, ctx) => {
|
|
7239
|
-
const err = validateExpressionSource(src);
|
|
7240
|
-
if (err !== null) ctx.addIssue({
|
|
7241
|
-
code: "custom",
|
|
7242
|
-
message: err,
|
|
7243
|
-
path: ["expr"]
|
|
7244
|
-
});
|
|
7245
|
-
});
|
|
7246
|
-
var DeviceLinkSchema = z.object({
|
|
7247
|
-
id: z.string(),
|
|
7248
|
-
source: z.union([
|
|
7249
|
-
DeviceLinkFieldSourceSchema,
|
|
7250
|
-
DeviceLinkLiteralSourceSchema,
|
|
7251
|
-
DeviceLinkGlobalSourceSchema,
|
|
7252
|
-
DeviceLinkExpressionSourceSchema
|
|
7253
|
-
]),
|
|
7254
|
-
target: z.object({
|
|
7255
|
-
cap: z.string(),
|
|
7256
|
-
fieldPath: z.string(),
|
|
7257
|
-
itemKey: z.string().optional()
|
|
7258
|
-
}),
|
|
7259
|
-
transform: z.discriminatedUnion("kind", [
|
|
7260
|
-
z.object({ kind: z.literal("identity") }),
|
|
7261
|
-
z.object({
|
|
7262
|
-
kind: z.literal("enum-map"),
|
|
7263
|
-
mapping: z.record(z.string(), z.union([
|
|
7264
|
-
z.string(),
|
|
7265
|
-
z.number(),
|
|
7266
|
-
z.boolean()
|
|
7267
|
-
])),
|
|
7268
|
-
fallback: z.union([
|
|
7269
|
-
z.string(),
|
|
7270
|
-
z.number(),
|
|
7271
|
-
z.boolean()
|
|
7272
|
-
]).optional()
|
|
7273
|
-
}),
|
|
7274
|
-
z.object({
|
|
7275
|
-
kind: z.literal("linear"),
|
|
7276
|
-
scale: z.number(),
|
|
7277
|
-
offset: z.number(),
|
|
7278
|
-
clamp: z.tuple([z.number(), z.number()]).readonly().optional()
|
|
7279
|
-
})
|
|
7280
|
-
]).optional()
|
|
7281
|
-
});
|
|
7282
|
-
/** Cap-wire shape of a per-cap display refinement — mirrors
|
|
7283
|
-
* `DeviceCapDisplayOverride` in `device-management.ts`. */
|
|
7284
|
-
var DeviceCapDisplayOverrideSchema = z.object({
|
|
7285
|
-
unit: z.string().min(1).optional(),
|
|
7286
|
-
precision: z.number().int().min(0).max(10).optional()
|
|
7287
|
-
});
|
|
7288
|
-
/** Cap-wire shape of an operator-authored per-device display override —
|
|
7289
|
-
* mirrors `DeviceDisplayOverride` in `device-management.ts`. `precision`
|
|
7290
|
-
* bounds mirror `numeric-sensor.cap.ts` (`int 0-10`). */
|
|
7291
|
-
var DeviceDisplayOverrideSchema = z.object({
|
|
7292
|
-
icon: z.string().min(1).optional(),
|
|
7293
|
-
label: z.string().min(1).optional(),
|
|
7294
|
-
unit: z.string().min(1).optional(),
|
|
7295
|
-
precision: z.number().int().min(0).max(10).optional(),
|
|
7296
|
-
hidden: z.boolean().optional(),
|
|
7297
|
-
perCap: z.record(z.string(), DeviceCapDisplayOverrideSchema).optional()
|
|
7298
|
-
});
|
|
7299
|
-
/** Cap-wire shape of a per-role display default — mirrors `RoleDisplayDefault`
|
|
7300
|
-
* in `device-management.ts`. Keyed by `DeviceRole` string (role strings cross
|
|
7301
|
-
* the wire as plain strings everywhere else — cf. `DeviceInfoSchema.role`). */
|
|
7302
|
-
var RoleDisplayDefaultSchema = z.object({
|
|
7303
|
-
unit: z.string().min(1).optional(),
|
|
7304
|
-
precision: z.number().int().min(0).max(10).optional(),
|
|
7305
|
-
icon: z.string().min(1).optional()
|
|
7306
|
-
});
|
|
7307
|
-
/**
|
|
7308
|
-
* Serializable projection of a live IDevice.
|
|
7309
|
-
* Returned by listAll, getDevice, getChildren.
|
|
7310
|
-
* Live methods (getStreamSources, getConfigSchema) are separate calls.
|
|
7311
|
-
*/
|
|
7312
|
-
var DeviceInfoSchema = z.object({
|
|
7313
|
-
/** Progressive, system-wide unique number. Allocated synchronously by
|
|
7314
|
-
* `device-manager.allocateDeviceId` BEFORE the owning `IDevice` is
|
|
7315
|
-
* constructed, so every live device exposes an `id` — no transient
|
|
7316
|
-
* null window. Distinct from `stableId`, which is unique per
|
|
7317
|
-
* integration. Ids are monotonic and never reissued on removal. */
|
|
7318
|
-
id: z.number(),
|
|
7319
|
-
stableId: z.string(),
|
|
7320
|
-
addonId: z.string(),
|
|
7321
|
-
type: z.enum(DeviceType),
|
|
7322
|
-
name: z.string(),
|
|
7323
|
-
/** Operator-organisational location label. `null` when unset. */
|
|
7324
|
-
location: z.string().nullable(),
|
|
7325
|
-
/** Soft-disabled flag. */
|
|
7326
|
-
disabled: z.boolean(),
|
|
7327
|
-
parentDeviceId: z.number().nullable(),
|
|
7328
|
-
/** Optional semantic role — `DeviceRole` string. null for top-level devices. */
|
|
7329
|
-
role: z.string().nullable().optional(),
|
|
7330
|
-
online: z.boolean(),
|
|
7331
|
-
/** True when the device's initial feature-probe has completed (or it has
|
|
7332
|
-
* no probe) — exported shape is stable; exporters gate advertise on this.
|
|
7333
|
-
* Optional for deploy-order resilience: a record produced by an older
|
|
7334
|
-
* device-manager build omits it, and consumers treat absent as "not ready"
|
|
7335
|
-
* (export gate carries forward, never advertises a partial shape). */
|
|
7336
|
-
probed: z.boolean().optional(),
|
|
7337
|
-
features: z.array(z.string()),
|
|
7338
|
-
/** true when the device has a getStreamSources() method (ICameraDevice) */
|
|
7339
|
-
isCamera: z.boolean(),
|
|
7340
|
-
/** Current config values — serializable snapshot */
|
|
6532
|
+
/** true when the device has a getStreamSources() method (ICameraDevice) */
|
|
6533
|
+
isCamera: z.boolean(),
|
|
6534
|
+
/** Current config values — serializable snapshot */
|
|
7341
6535
|
config: z.record(z.string(), z.unknown()),
|
|
7342
6536
|
/** Hardware + identity blob (manufacturer / model / firmware / sn /
|
|
7343
6537
|
* uid / mac / …). Populated by drivers; editable via setMetadata. */
|
|
@@ -7358,8 +6552,6 @@ var DeviceInfoSchema = z.object({
|
|
|
7358
6552
|
* named accordion sections (with optional intra-section order). See
|
|
7359
6553
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
7360
6554
|
childLayout: z.array(ChildLayoutEntrySchema).readonly().optional(),
|
|
7361
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
7362
|
-
deviceLinks: z.array(DeviceLinkSchema).readonly().optional(),
|
|
7363
6555
|
/** Operator-authored per-device display override. See `DeviceMeta.display`. */
|
|
7364
6556
|
display: DeviceDisplayOverrideSchema.optional()
|
|
7365
6557
|
});
|
|
@@ -7368,7 +6560,7 @@ var ConfigEntrySchema = z.object({
|
|
|
7368
6560
|
value: z.unknown(),
|
|
7369
6561
|
description: z.string().optional()
|
|
7370
6562
|
});
|
|
7371
|
-
var
|
|
6563
|
+
var LinkedDevicesModeSchema = z.enum(["auto", "manual"]);
|
|
7372
6564
|
/** One resolved linked device — the compact projection consumers need. */
|
|
7373
6565
|
var LinkedDeviceSchema = z.object({
|
|
7374
6566
|
deviceId: z.number(),
|
|
@@ -7431,8 +6623,6 @@ var DeviceMetaSchema = z.object({
|
|
|
7431
6623
|
* accordion sections (with optional intra-section order). See
|
|
7432
6624
|
* `DeviceMeta.childLayout`. Absent ⇒ no layout declared. */
|
|
7433
6625
|
childLayout: z.array(ChildLayoutEntrySchema).readonly().optional(),
|
|
7434
|
-
/** Operator-authored cross-device field wirings. See `DeviceMeta.deviceLinks`. */
|
|
7435
|
-
deviceLinks: z.array(DeviceLinkSchema).readonly().optional(),
|
|
7436
6626
|
/** Semantic role string (`DeviceRole`) — propagated from the spawn pre-seed.
|
|
7437
6627
|
* Optional: only present for accessory children that carry a known role. */
|
|
7438
6628
|
role: z.string().nullable().optional(),
|
|
@@ -7584,16 +6774,6 @@ var deviceManagerCapability = {
|
|
|
7584
6774
|
kind: "mutation",
|
|
7585
6775
|
auth: "admin"
|
|
7586
6776
|
}),
|
|
7587
|
-
/** Set (or replace) the cross-device field wirings on a device's meta row.
|
|
7588
|
-
* Mirrors `setChildLayout`; persisted, projected, preserved across
|
|
7589
|
-
* re-register/restore. Idempotent. */
|
|
7590
|
-
setDeviceLinks: method(z.object({
|
|
7591
|
-
deviceId: z.number(),
|
|
7592
|
-
deviceLinks: z.array(DeviceLinkSchema).readonly()
|
|
7593
|
-
}), z.void(), {
|
|
7594
|
-
kind: "mutation",
|
|
7595
|
-
auth: "admin"
|
|
7596
|
-
}),
|
|
7597
6777
|
/** Set (or clear) the per-device display override on the meta row. Mirrors
|
|
7598
6778
|
* `setChildLayout` persistence; `null` clears the override entirely. The
|
|
7599
6779
|
* override unit(s) are normalized (`normalizeUnit`) at write so the render
|
|
@@ -7617,15 +6797,15 @@ var deviceManagerCapability = {
|
|
|
7617
6797
|
kind: "mutation",
|
|
7618
6798
|
auth: "admin"
|
|
7619
6799
|
}),
|
|
7620
|
-
/**
|
|
7621
|
-
*
|
|
7622
|
-
*
|
|
7623
|
-
*
|
|
7624
|
-
*
|
|
7625
|
-
*
|
|
7626
|
-
*
|
|
7627
|
-
*
|
|
7628
|
-
*
|
|
6800
|
+
/** The per-cap catalog of readable status-schema fields for one device.
|
|
6801
|
+
* This is the SOURCE PICKER — it outlived the Wiring tab that first
|
|
6802
|
+
* needed it (deleted 2026-08-08) and becomes the picker of a composition
|
|
6803
|
+
* recipe: every field the old tab could target, a composed device can
|
|
6804
|
+
* read. Caps without a status schema are omitted. Item-array caps
|
|
6805
|
+
* (`status.itemArray`, e.g. consumables) also emit their per-item fields
|
|
6806
|
+
* tagged `item: true` plus the cap-level `itemArray` descriptor.
|
|
6807
|
+
* `includeSynthesizable: true` unions in unbound device-scoped caps that
|
|
6808
|
+
* declare `status.empty` and match the device's type. */
|
|
7629
6809
|
getWireableFields: method(z.object({
|
|
7630
6810
|
deviceId: z.number(),
|
|
7631
6811
|
includeSynthesizable: z.boolean().optional()
|
|
@@ -7767,7 +6947,7 @@ var deviceManagerCapability = {
|
|
|
7767
6947
|
* (see the module docblock) — there are no bespoke link mutations.
|
|
7768
6948
|
*/
|
|
7769
6949
|
getLinkedDevices: method(z.object({ deviceId: z.number() }), z.object({
|
|
7770
|
-
mode:
|
|
6950
|
+
mode: LinkedDevicesModeSchema,
|
|
7771
6951
|
devices: z.array(LinkedDeviceSchema)
|
|
7772
6952
|
})),
|
|
7773
6953
|
/** Get stream sources for a camera device. */
|
|
@@ -7831,11 +7011,7 @@ var deviceManagerCapability = {
|
|
|
7831
7011
|
deviceId: z.number(),
|
|
7832
7012
|
entries: z.array(z.object({
|
|
7833
7013
|
capName: z.string(),
|
|
7834
|
-
kind: z.enum([
|
|
7835
|
-
"native",
|
|
7836
|
-
"wrapped",
|
|
7837
|
-
"linked"
|
|
7838
|
-
]),
|
|
7014
|
+
kind: z.enum(["native", "wrapped"]),
|
|
7839
7015
|
providerAddonId: z.string(),
|
|
7840
7016
|
providerNodeId: z.string(),
|
|
7841
7017
|
nativeAddonId: z.string()
|
|
@@ -7853,11 +7029,7 @@ var deviceManagerCapability = {
|
|
|
7853
7029
|
deviceId: z.number(),
|
|
7854
7030
|
entries: z.array(z.object({
|
|
7855
7031
|
capName: z.string(),
|
|
7856
|
-
kind: z.enum([
|
|
7857
|
-
"native",
|
|
7858
|
-
"wrapped",
|
|
7859
|
-
"linked"
|
|
7860
|
-
]),
|
|
7032
|
+
kind: z.enum(["native", "wrapped"]),
|
|
7861
7033
|
providerAddonId: z.string(),
|
|
7862
7034
|
providerNodeId: z.string(),
|
|
7863
7035
|
nativeAddonId: z.string()
|
|
@@ -9103,6 +8275,10 @@ var motionDetectionCapability = {
|
|
|
9103
8275
|
mode: "singleton",
|
|
9104
8276
|
kind: "wrapper",
|
|
9105
8277
|
defaultActive: true,
|
|
8278
|
+
/** Frame-differencing motion analysis over a video stream — only a camera
|
|
8279
|
+
* produces the frames. Read by the `defaultActive` auto-bind in
|
|
8280
|
+
* `device-manager.getBindings` to decide which devices it may claim. */
|
|
8281
|
+
deviceTypes: [DeviceType.Camera],
|
|
9106
8282
|
exposesDeviceSettings: true,
|
|
9107
8283
|
methods: {
|
|
9108
8284
|
analyze: method(z.object({
|
|
@@ -12224,6 +11400,18 @@ var OauthIntegrationDescriptorSchema = z.object({
|
|
|
12224
11400
|
* redirect_uri that does not start with one of these. Required —
|
|
12225
11401
|
* an empty list means the integration can never complete linking. */
|
|
12226
11402
|
allowedRedirectPrefixes: z.array(z.string()).min(1),
|
|
11403
|
+
/** Paths accepted as a `redirect_uri` when the host is PRIVATE — loopback,
|
|
11404
|
+
* RFC1918, CGNAT (100.64/10, Tailscale), link-local, IPv6 ULA, or an
|
|
11405
|
+
* `.local` / `.internal` / `.ts.net` name. Exists for self-hosted clients
|
|
11406
|
+
* whose address the hub cannot know in advance (a Home Assistant at
|
|
11407
|
+
* `http://<lan-ip>:8123/auth/external/callback`). The PATH must match
|
|
11408
|
+
* exactly; a public host never satisfies this branch, so it is not a
|
|
11409
|
+
* wildcard prefix by another name. */
|
|
11410
|
+
allowedPrivateHostPaths: z.array(z.string()).optional(),
|
|
11411
|
+
/** When true this is a PUBLIC client (source is published, no secret can be
|
|
11412
|
+
* protected) and PKCE is mandatory: `/authorize` refuses without an S256
|
|
11413
|
+
* `code_challenge`, `/token` refuses without the matching `code_verifier`. */
|
|
11414
|
+
requiresPkce: z.boolean().optional(),
|
|
12227
11415
|
/** Optional public origin (no trailing slash) that this integration's
|
|
12228
11416
|
* issued codes/tokens should carry as the `hubUrl` claim — typically the
|
|
12229
11417
|
* operator-selected external-access endpoint resolved by the addon. When
|
|
@@ -12382,7 +11570,7 @@ var TrackEnvelopeSchema = z.object({
|
|
|
12382
11570
|
* `snapshots[]` references — megabytes across a page of tracks. `slim`
|
|
12383
11571
|
* keeps every scalar the list surfaces actually render (ids, class(es),
|
|
12384
11572
|
* label / audioLabels / importance enrichment, firstSeen/lastSeen, state,
|
|
12385
|
-
* zonesVisited, bestEventId, envelope) and returns `positions` /
|
|
11573
|
+
* zonesVisited, bestEventId, envelope, hasFace) and returns `positions` /
|
|
12386
11574
|
* `snapshots` as EMPTY arrays — detail views re-fetch the full row via
|
|
12387
11575
|
* `getTrack`. Mirrors the event-store `projection` convention
|
|
12388
11576
|
* (`getObjectEvents` et al.).
|
|
@@ -12635,6 +11823,24 @@ var TrackSchema = z.object({
|
|
|
12635
11823
|
* Populated from the persisted envelope columns on historical reads;
|
|
12636
11824
|
* absent on legacy rows, dims-less tracks and active (in-RAM) tracks. */
|
|
12637
11825
|
envelope: TrackEnvelopeSchema.optional(),
|
|
11826
|
+
/**
|
|
11827
|
+
* A face DETECTOR found a face on this track — nothing more. It says the
|
|
11828
|
+
* detail plane produced a `face` detail; it does NOT say the face was
|
|
11829
|
+
* embedded, matched, above `minFacePx`, or that the recognizer was even
|
|
11830
|
+
* enabled. Set once and never cleared.
|
|
11831
|
+
*
|
|
11832
|
+
* **This exists so "face present but not recognised" is expressible.** A
|
|
11833
|
+
* recognised identity lands in `subLabel` (attributed to the face chain via
|
|
11834
|
+
* `subLabelMeta.stepId`), so before this field a track with an unmatched face
|
|
11835
|
+
* and a track with no face at all were byte-identical on the wire and no
|
|
11836
|
+
* surface could tell them apart. The read is `hasFace === true && subLabel
|
|
11837
|
+
* === undefined`.
|
|
11838
|
+
*
|
|
11839
|
+
* **Absent ≠ false.** Every row written before the column existed omits it,
|
|
11840
|
+
* and so does every server that predates the field — a consumer must test
|
|
11841
|
+
* `=== true` and render nothing otherwise, never infer "no face".
|
|
11842
|
+
*/
|
|
11843
|
+
hasFace: z.boolean().optional(),
|
|
12638
11844
|
...TrackFlagFields,
|
|
12639
11845
|
...TrackRetrainFields
|
|
12640
11846
|
});
|
|
@@ -16576,6 +15782,10 @@ var SsoBridgeClaimsSchema = z.object({
|
|
|
16576
15782
|
integrationId: z.string().optional(),
|
|
16577
15783
|
/** JWT ID — unique per issued code; consumed-set enforces single-use. */
|
|
16578
15784
|
jti: z.string().optional(),
|
|
15785
|
+
/** PKCE S256 challenge — set only on `oauth-code` tokens issued to a public
|
|
15786
|
+
* client. Its PRESENCE is what makes the verifier mandatory at exchange,
|
|
15787
|
+
* so the requirement travels with the code and not with mutable config. */
|
|
15788
|
+
codeChallenge: z.string().optional(),
|
|
16579
15789
|
/** OAuth session registry id — set on `oauth-access`/`oauth-refresh`
|
|
16580
15790
|
* tokens so the verify path can check the session is not revoked. */
|
|
16581
15791
|
sessionId: z.string().optional()
|
|
@@ -17348,6 +16558,10 @@ var videoclipsCapability = {
|
|
|
17348
16558
|
mode: "singleton",
|
|
17349
16559
|
kind: "wrapper",
|
|
17350
16560
|
defaultActive: true,
|
|
16561
|
+
/** A clip is a window over a camera's footage — the cap is meaningless on a
|
|
16562
|
+
* sensor, a button or an event emitter, and the `defaultActive` auto-bind
|
|
16563
|
+
* reads this to decide which devices it may claim. */
|
|
16564
|
+
deviceTypes: [DeviceType.Camera],
|
|
17351
16565
|
methods: {
|
|
17352
16566
|
listClips: method(z.object({
|
|
17353
16567
|
deviceId: z.number(),
|
|
@@ -25488,14 +24702,19 @@ var userManagementCapability = {
|
|
|
25488
24702
|
username: z.string(),
|
|
25489
24703
|
scopes: z.array(TokenScopeSchema),
|
|
25490
24704
|
redirectUri: z.string(),
|
|
25491
|
-
hubUrl: z.string()
|
|
24705
|
+
hubUrl: z.string(),
|
|
24706
|
+
/** PKCE (RFC 7636) S256 challenge. Baked into the signed code; a code
|
|
24707
|
+
* that carries one can ONLY be exchanged with the matching verifier. */
|
|
24708
|
+
codeChallenge: z.string().optional()
|
|
25492
24709
|
}), z.object({ code: z.string() }), {
|
|
25493
24710
|
kind: "mutation",
|
|
25494
24711
|
access: "create"
|
|
25495
24712
|
}),
|
|
25496
24713
|
oauthExchangeCode: method(z.object({
|
|
25497
24714
|
code: z.string(),
|
|
25498
|
-
redirectUri: z.string()
|
|
24715
|
+
redirectUri: z.string(),
|
|
24716
|
+
/** PKCE verifier. REQUIRED when the code carries a challenge. */
|
|
24717
|
+
codeVerifier: z.string().optional()
|
|
25499
24718
|
}), z.object({
|
|
25500
24719
|
accessToken: z.string(),
|
|
25501
24720
|
refreshToken: z.string(),
|
|
@@ -27386,6 +26605,218 @@ var BaseDeviceProvider = class extends BaseAddon {
|
|
|
27386
26605
|
}
|
|
27387
26606
|
};
|
|
27388
26607
|
//#endregion
|
|
26608
|
+
//#region src/device/declared-device.ts
|
|
26609
|
+
/** Marker written to a declared integration's `info`. */
|
|
26610
|
+
var DECLARED_INTEGRATION_FIXED_KEY = "fixed";
|
|
26611
|
+
/**
|
|
26612
|
+
* Strip the `<node>/<addon>` suffix a forked child carries.
|
|
26613
|
+
*
|
|
26614
|
+
* Comparing `ctx.kernel.localNodeId` raw skipped EVERY node — including the one
|
|
26615
|
+
* that was supposed to act — because on the hub it reads `hub/<addon>`.
|
|
26616
|
+
*/
|
|
26617
|
+
function declarationOwnerNodeId(localNodeId) {
|
|
26618
|
+
const raw = localNodeId ?? "hub";
|
|
26619
|
+
if (!raw.includes("/")) return raw;
|
|
26620
|
+
return raw.split("/")[0] ?? "hub";
|
|
26621
|
+
}
|
|
26622
|
+
/** Cap on how many rows one withdrawal pass may delete. */
|
|
26623
|
+
var DECLARED_DEVICE_SWEEP_LIMIT = 32;
|
|
26624
|
+
/**
|
|
26625
|
+
* The one way an addon owns a device it declares.
|
|
26626
|
+
*
|
|
26627
|
+
* Construct once with the addon's ports, then call {@link reconcile} on boot and
|
|
26628
|
+
* on every convergence tick. There is no second get-or-create helper — a guard
|
|
26629
|
+
* in `scripts/` enforces that.
|
|
26630
|
+
*/
|
|
26631
|
+
var DeclaredDevices = class {
|
|
26632
|
+
ports;
|
|
26633
|
+
constructor(ports) {
|
|
26634
|
+
this.ports = ports;
|
|
26635
|
+
}
|
|
26636
|
+
/**
|
|
26637
|
+
* Converge the declared set. Idempotent, and safe to call repeatedly.
|
|
26638
|
+
*
|
|
26639
|
+
* Throws only what the ports throw on the FIRST index read; every other
|
|
26640
|
+
* failure is per-device and logged, so one bad declaration never takes the
|
|
26641
|
+
* others down.
|
|
26642
|
+
*/
|
|
26643
|
+
async reconcile(spec) {
|
|
26644
|
+
if ((spec.placement ?? "hub") === "hub") {
|
|
26645
|
+
const nodeId = declarationOwnerNodeId(this.ports.localNodeId);
|
|
26646
|
+
if (nodeId !== "hub") {
|
|
26647
|
+
this.ports.logger.info("declared devices are hub-owned — skipping on this node", { meta: {
|
|
26648
|
+
nodeId,
|
|
26649
|
+
rawNodeId: this.ports.localNodeId ?? null
|
|
26650
|
+
} });
|
|
26651
|
+
return {
|
|
26652
|
+
integrationId: null,
|
|
26653
|
+
devices: [],
|
|
26654
|
+
removed: [],
|
|
26655
|
+
owned: false
|
|
26656
|
+
};
|
|
26657
|
+
}
|
|
26658
|
+
}
|
|
26659
|
+
const integrationId = await this.ensureIntegration(spec.integrationName);
|
|
26660
|
+
const index = await this.readIndex();
|
|
26661
|
+
const outcomes = [];
|
|
26662
|
+
for (const declaration of spec.devices) {
|
|
26663
|
+
const outcome = await this.applyDeclaration(declaration, integrationId, index);
|
|
26664
|
+
if (outcome !== null) outcomes.push(outcome);
|
|
26665
|
+
}
|
|
26666
|
+
return {
|
|
26667
|
+
integrationId,
|
|
26668
|
+
devices: outcomes,
|
|
26669
|
+
removed: await this.sweepWithdrawn(spec.devices, integrationId, index),
|
|
26670
|
+
owned: true
|
|
26671
|
+
};
|
|
26672
|
+
}
|
|
26673
|
+
/**
|
|
26674
|
+
* Get-or-create the FIXED integration, and RE-ASSERT the flag every pass.
|
|
26675
|
+
*
|
|
26676
|
+
* The re-assertion is the fix for the defect the hand-rolled version shipped
|
|
26677
|
+
* with: writing `info.fixed` only on the create path left every pre-existing
|
|
26678
|
+
* install without it, and the kernel kept offering to delete an integration
|
|
26679
|
+
* the addon owns.
|
|
26680
|
+
*/
|
|
26681
|
+
async ensureIntegration(integrationName) {
|
|
26682
|
+
const existing = await this.ports.getIntegration(this.ports.addonId);
|
|
26683
|
+
if (existing === null) {
|
|
26684
|
+
const created = await this.ports.createIntegration({
|
|
26685
|
+
addonId: this.ports.addonId,
|
|
26686
|
+
name: integrationName,
|
|
26687
|
+
info: { [DECLARED_INTEGRATION_FIXED_KEY]: true }
|
|
26688
|
+
});
|
|
26689
|
+
this.ports.logger.info("declared a fixed integration", { meta: {
|
|
26690
|
+
integrationId: created.id,
|
|
26691
|
+
name: integrationName
|
|
26692
|
+
} });
|
|
26693
|
+
return created.id;
|
|
26694
|
+
}
|
|
26695
|
+
if (existing.info?.["fixed"] !== true) {
|
|
26696
|
+
await this.ports.updateIntegration({
|
|
26697
|
+
id: existing.id,
|
|
26698
|
+
info: { [DECLARED_INTEGRATION_FIXED_KEY]: true }
|
|
26699
|
+
});
|
|
26700
|
+
this.ports.logger.info("re-asserted `fixed` on a declared integration", { meta: { integrationId: existing.id } });
|
|
26701
|
+
}
|
|
26702
|
+
return existing.id;
|
|
26703
|
+
}
|
|
26704
|
+
async readIndex() {
|
|
26705
|
+
const rows = await this.ports.listOwnDevices();
|
|
26706
|
+
return new Map(rows.map((row) => [row.stableId, row]));
|
|
26707
|
+
}
|
|
26708
|
+
/**
|
|
26709
|
+
* One declaration: adopt what exists, create what does not.
|
|
26710
|
+
*
|
|
26711
|
+
* The create branch is the destructive one — it seeds `initialMeta`, and
|
|
26712
|
+
* `initialMeta.name` lands as an unconditional `setName`. A transiently empty
|
|
26713
|
+
* index therefore looks exactly like a first boot and would silently re-stamp
|
|
26714
|
+
* the declared name over the operator's rename. D49: that branch needs a
|
|
26715
|
+
* second read to agree.
|
|
26716
|
+
*/
|
|
26717
|
+
async applyDeclaration(declaration, integrationId, index) {
|
|
26718
|
+
try {
|
|
26719
|
+
let existing = index.get(declaration.stableId);
|
|
26720
|
+
if (existing === void 0) {
|
|
26721
|
+
existing = (await this.readIndex()).get(declaration.stableId);
|
|
26722
|
+
if (existing !== void 0) this.ports.logger.warn("device index disagreed with itself — adopting instead of re-creating", {
|
|
26723
|
+
tags: { deviceId: existing.id },
|
|
26724
|
+
meta: {
|
|
26725
|
+
stableId: declaration.stableId,
|
|
26726
|
+
addonId: this.ports.addonId
|
|
26727
|
+
}
|
|
26728
|
+
});
|
|
26729
|
+
}
|
|
26730
|
+
if (existing !== void 0) {
|
|
26731
|
+
const device = await this.ports.devices.create(declaration.stableId, declaration.DeviceClass, {}, null, void 0);
|
|
26732
|
+
this.ports.logger.info("declared device adopted", {
|
|
26733
|
+
tags: { deviceId: device.id },
|
|
26734
|
+
meta: {
|
|
26735
|
+
stableId: declaration.stableId,
|
|
26736
|
+
integrationId
|
|
26737
|
+
}
|
|
26738
|
+
});
|
|
26739
|
+
return {
|
|
26740
|
+
stableId: declaration.stableId,
|
|
26741
|
+
deviceId: device.id,
|
|
26742
|
+
device,
|
|
26743
|
+
created: false
|
|
26744
|
+
};
|
|
26745
|
+
}
|
|
26746
|
+
const device = await this.ports.devices.create(declaration.stableId, declaration.DeviceClass, declaration.config ?? {}, null, {
|
|
26747
|
+
type: declaration.type,
|
|
26748
|
+
name: declaration.name,
|
|
26749
|
+
integrationId,
|
|
26750
|
+
...declaration.role === void 0 ? {} : { role: declaration.role }
|
|
26751
|
+
});
|
|
26752
|
+
this.ports.logger.info("declared device created", {
|
|
26753
|
+
tags: { deviceId: device.id },
|
|
26754
|
+
meta: {
|
|
26755
|
+
stableId: declaration.stableId,
|
|
26756
|
+
integrationId
|
|
26757
|
+
}
|
|
26758
|
+
});
|
|
26759
|
+
return {
|
|
26760
|
+
stableId: declaration.stableId,
|
|
26761
|
+
deviceId: device.id,
|
|
26762
|
+
device,
|
|
26763
|
+
created: true
|
|
26764
|
+
};
|
|
26765
|
+
} catch (err) {
|
|
26766
|
+
this.ports.logger.warn("a declared device could not be brought up", { meta: {
|
|
26767
|
+
stableId: declaration.stableId,
|
|
26768
|
+
error: err instanceof Error ? err.message : String(err)
|
|
26769
|
+
} });
|
|
26770
|
+
return null;
|
|
26771
|
+
}
|
|
26772
|
+
}
|
|
26773
|
+
/**
|
|
26774
|
+
* Remove rows under the addon's FIXED integration whose declaration is gone.
|
|
26775
|
+
*
|
|
26776
|
+
* Bounded to that integration: a declared integration has no operator
|
|
26777
|
+
* add-flow, so every row under it got there by declaration. Devices this
|
|
26778
|
+
* addon owns OUTSIDE it (a provider's adopted devices) are never candidates.
|
|
26779
|
+
*
|
|
26780
|
+
* Bounded in count, and every deletion is logged with its `deviceId` — a
|
|
26781
|
+
* withdrawal that removes an operator-visible row silently is the failure
|
|
26782
|
+
* mode, not the removal itself.
|
|
26783
|
+
*/
|
|
26784
|
+
async sweepWithdrawn(declarations, integrationId, index) {
|
|
26785
|
+
const declared = new Set(declarations.map((d) => d.stableId));
|
|
26786
|
+
const candidates = [...index.values()].filter((row) => row.integrationId === integrationId && !declared.has(row.stableId));
|
|
26787
|
+
if (candidates.length === 0) return [];
|
|
26788
|
+
if (candidates.length > 32) {
|
|
26789
|
+
this.ports.logger.warn("withdrawal sweep exceeded its bound — removing nothing", { meta: {
|
|
26790
|
+
integrationId,
|
|
26791
|
+
candidates: candidates.length,
|
|
26792
|
+
bound: 32
|
|
26793
|
+
} });
|
|
26794
|
+
return [];
|
|
26795
|
+
}
|
|
26796
|
+
const removed = [];
|
|
26797
|
+
for (const row of candidates) try {
|
|
26798
|
+
await this.ports.devices.remove(row.id);
|
|
26799
|
+
removed.push(row.id);
|
|
26800
|
+
this.ports.logger.info("declared device removed — its declaration was withdrawn", {
|
|
26801
|
+
tags: { deviceId: row.id },
|
|
26802
|
+
meta: {
|
|
26803
|
+
stableId: row.stableId,
|
|
26804
|
+
integrationId
|
|
26805
|
+
}
|
|
26806
|
+
});
|
|
26807
|
+
} catch (err) {
|
|
26808
|
+
this.ports.logger.warn("a withdrawn declared device could not be removed", {
|
|
26809
|
+
tags: { deviceId: row.id },
|
|
26810
|
+
meta: {
|
|
26811
|
+
stableId: row.stableId,
|
|
26812
|
+
error: err instanceof Error ? err.message : String(err)
|
|
26813
|
+
}
|
|
26814
|
+
});
|
|
26815
|
+
}
|
|
26816
|
+
return removed;
|
|
26817
|
+
}
|
|
26818
|
+
};
|
|
26819
|
+
//#endregion
|
|
27389
26820
|
//#region src/device/device-control-resolution.ts
|
|
27390
26821
|
/**
|
|
27391
26822
|
* Device-control RESOLUTION LOGIC — the single, presentation-free source of
|
|
@@ -27562,30 +26993,6 @@ function resolveMutate(router, method) {
|
|
|
27562
26993
|
return typeof mutate === "function" ? mutate : null;
|
|
27563
26994
|
}
|
|
27564
26995
|
//#endregion
|
|
27565
|
-
//#region src/device/device-link-transform.ts
|
|
27566
|
-
/** Apply a link transform to a resolved source value. `identity`/undefined
|
|
27567
|
-
* pass through; `enum-map` remaps strings (fallback or null when unknown);
|
|
27568
|
-
* `linear` does scale*x+offset with optional clamp (null for non-numbers). */
|
|
27569
|
-
function applyTransform(value, transform) {
|
|
27570
|
-
if (!transform || transform.kind === "identity") return value;
|
|
27571
|
-
if (transform.kind === "enum-map") {
|
|
27572
|
-
const key = String(value);
|
|
27573
|
-
if (Object.prototype.hasOwnProperty.call(transform.mapping, key)) return transform.mapping[key];
|
|
27574
|
-
return transform.fallback ?? null;
|
|
27575
|
-
}
|
|
27576
|
-
if (transform.kind === "linear") {
|
|
27577
|
-
if (typeof value !== "number" || Number.isNaN(value)) return null;
|
|
27578
|
-
let out = value * transform.scale + transform.offset;
|
|
27579
|
-
if (transform.clamp) {
|
|
27580
|
-
const min = transform.clamp[0];
|
|
27581
|
-
const max = transform.clamp[1];
|
|
27582
|
-
out = Math.min(max, Math.max(min, out));
|
|
27583
|
-
}
|
|
27584
|
-
return out;
|
|
27585
|
-
}
|
|
27586
|
-
return value;
|
|
27587
|
-
}
|
|
27588
|
-
//#endregion
|
|
27589
26996
|
//#region src/device/device-profile.ts
|
|
27590
26997
|
/**
|
|
27591
26998
|
* Heuristic check — `device.features?.includes(profile.when.hasFeature)`.
|
|
@@ -28114,501 +27521,1347 @@ var SystemMirror = class {
|
|
|
28114
27521
|
const proxy = this.getDeviceById(deviceId);
|
|
28115
27522
|
if (proxy) out.push(proxy);
|
|
28116
27523
|
}
|
|
28117
|
-
return out;
|
|
28118
|
-
}
|
|
28119
|
-
/** Map every device's slice for `capName` to a derived value. Devices
|
|
28120
|
-
* without the cap or without a slice are skipped. */
|
|
28121
|
-
mapState(capName, mapper) {
|
|
28122
|
-
const out = [];
|
|
28123
|
-
for (const [deviceId, perCap] of this.stateMirror) {
|
|
28124
|
-
const slice = perCap.get(capName);
|
|
28125
|
-
if (!slice) continue;
|
|
28126
|
-
out.push(mapper(slice, deviceId));
|
|
27524
|
+
return out;
|
|
27525
|
+
}
|
|
27526
|
+
/** Map every device's slice for `capName` to a derived value. Devices
|
|
27527
|
+
* without the cap or without a slice are skipped. */
|
|
27528
|
+
mapState(capName, mapper) {
|
|
27529
|
+
const out = [];
|
|
27530
|
+
for (const [deviceId, perCap] of this.stateMirror) {
|
|
27531
|
+
const slice = perCap.get(capName);
|
|
27532
|
+
if (!slice) continue;
|
|
27533
|
+
out.push(mapper(slice, deviceId));
|
|
27534
|
+
}
|
|
27535
|
+
return out;
|
|
27536
|
+
}
|
|
27537
|
+
/** First device whose slice matches `predicate`, or null. */
|
|
27538
|
+
findState(capName, predicate) {
|
|
27539
|
+
for (const [deviceId, perCap] of this.stateMirror) {
|
|
27540
|
+
const slice = perCap.get(capName);
|
|
27541
|
+
if (!slice) continue;
|
|
27542
|
+
if (!predicate(slice, deviceId)) continue;
|
|
27543
|
+
return this.getDeviceById(deviceId);
|
|
27544
|
+
}
|
|
27545
|
+
return null;
|
|
27546
|
+
}
|
|
27547
|
+
/** Count devices that bind a cap. Faster than `filterByCap(...).length`. */
|
|
27548
|
+
countByCap(capName) {
|
|
27549
|
+
let n = 0;
|
|
27550
|
+
for (const binding of this.bindings.values()) if (binding.entries.some((e) => e.capName === capName)) n++;
|
|
27551
|
+
return n;
|
|
27552
|
+
}
|
|
27553
|
+
/** Count devices whose slice for `capName` matches `predicate`. */
|
|
27554
|
+
countByState(capName, predicate) {
|
|
27555
|
+
let n = 0;
|
|
27556
|
+
for (const [deviceId, perCap] of this.stateMirror) {
|
|
27557
|
+
const slice = perCap.get(capName);
|
|
27558
|
+
if (!slice) continue;
|
|
27559
|
+
if (predicate(slice, deviceId)) n++;
|
|
27560
|
+
}
|
|
27561
|
+
return n;
|
|
27562
|
+
}
|
|
27563
|
+
/**
|
|
27564
|
+
* Global listener — fires for every `device.state-changed` event the
|
|
27565
|
+
* mirror absorbs.
|
|
27566
|
+
*/
|
|
27567
|
+
listen(cb) {
|
|
27568
|
+
this.globalStateListeners.add(cb);
|
|
27569
|
+
return () => {
|
|
27570
|
+
this.globalStateListeners.delete(cb);
|
|
27571
|
+
};
|
|
27572
|
+
}
|
|
27573
|
+
/**
|
|
27574
|
+
* Per-cap listener — fires only for state changes on `capName`,
|
|
27575
|
+
* across every device. The callback receives the deviceId so the
|
|
27576
|
+
* caller can route.
|
|
27577
|
+
*/
|
|
27578
|
+
listenCap(capName, cb) {
|
|
27579
|
+
let set = this.capListeners.get(capName);
|
|
27580
|
+
if (!set) {
|
|
27581
|
+
set = /* @__PURE__ */ new Set();
|
|
27582
|
+
this.capListeners.set(capName, set);
|
|
27583
|
+
}
|
|
27584
|
+
set.add(cb);
|
|
27585
|
+
return () => {
|
|
27586
|
+
set.delete(cb);
|
|
27587
|
+
if (set.size === 0) this.capListeners.delete(capName);
|
|
27588
|
+
};
|
|
27589
|
+
}
|
|
27590
|
+
/**
|
|
27591
|
+
* Per-device listener — fires for every cap change on `deviceId`.
|
|
27592
|
+
*/
|
|
27593
|
+
listenDevice(deviceId, cb) {
|
|
27594
|
+
let set = this.deviceListeners.get(deviceId);
|
|
27595
|
+
if (!set) {
|
|
27596
|
+
set = /* @__PURE__ */ new Set();
|
|
27597
|
+
this.deviceListeners.set(deviceId, set);
|
|
27598
|
+
}
|
|
27599
|
+
set.add(cb);
|
|
27600
|
+
return () => {
|
|
27601
|
+
set.delete(cb);
|
|
27602
|
+
if (set.size === 0) this.deviceListeners.delete(deviceId);
|
|
27603
|
+
};
|
|
27604
|
+
}
|
|
27605
|
+
/** Fires when `device.registered` lands. Receives the new metadata. */
|
|
27606
|
+
onDeviceAdded(cb) {
|
|
27607
|
+
this.addedListeners.add(cb);
|
|
27608
|
+
return () => {
|
|
27609
|
+
this.addedListeners.delete(cb);
|
|
27610
|
+
};
|
|
27611
|
+
}
|
|
27612
|
+
/** Fires when `device.unregistered` lands. `info` is the LAST-known
|
|
27613
|
+
* metadata (or null if the device was never seen). */
|
|
27614
|
+
onDeviceRemoved(cb) {
|
|
27615
|
+
this.removedListeners.add(cb);
|
|
27616
|
+
return () => {
|
|
27617
|
+
this.removedListeners.delete(cb);
|
|
27618
|
+
};
|
|
27619
|
+
}
|
|
27620
|
+
/**
|
|
27621
|
+
* Resolve when `predicate` over the runtime-state slice for
|
|
27622
|
+
* `(deviceId, capName)` becomes true. Resolves immediately if the
|
|
27623
|
+
* current slice already matches. Rejects with `Error('timeout')`
|
|
27624
|
+
* after `timeoutMs` (default 30s; pass `Infinity` to wait forever).
|
|
27625
|
+
*
|
|
27626
|
+
* Returns the matching slice — caller can read it directly without
|
|
27627
|
+
* a second mirror lookup.
|
|
27628
|
+
*/
|
|
27629
|
+
waitForState(deviceId, capName, predicate, timeoutMs = 3e4) {
|
|
27630
|
+
return new Promise((resolve, reject) => {
|
|
27631
|
+
const check = () => {
|
|
27632
|
+
const slice = this.stateMirror.get(deviceId)?.get(capName);
|
|
27633
|
+
if (slice && predicate(slice)) return slice;
|
|
27634
|
+
return null;
|
|
27635
|
+
};
|
|
27636
|
+
const initial = check();
|
|
27637
|
+
if (initial) {
|
|
27638
|
+
resolve(initial);
|
|
27639
|
+
return;
|
|
27640
|
+
}
|
|
27641
|
+
let timer = null;
|
|
27642
|
+
const off = this.listenDevice(deviceId, (_id, cap, slice) => {
|
|
27643
|
+
if (cap !== capName) return;
|
|
27644
|
+
if (!slice) return;
|
|
27645
|
+
if (predicate(slice)) {
|
|
27646
|
+
if (timer) clearTimeout(timer);
|
|
27647
|
+
off();
|
|
27648
|
+
resolve(slice);
|
|
27649
|
+
}
|
|
27650
|
+
});
|
|
27651
|
+
if (Number.isFinite(timeoutMs)) timer = setTimeout(() => {
|
|
27652
|
+
off();
|
|
27653
|
+
reject(/* @__PURE__ */ new Error(`waitForState timed out after ${timeoutMs}ms (deviceId=${deviceId}, capName=${capName})`));
|
|
27654
|
+
}, timeoutMs);
|
|
27655
|
+
});
|
|
27656
|
+
}
|
|
27657
|
+
/**
|
|
27658
|
+
* Resolve when a device with `deviceId` becomes available (a
|
|
27659
|
+
* binding exists). Resolves immediately if already known. Rejects
|
|
27660
|
+
* with timeout.
|
|
27661
|
+
*/
|
|
27662
|
+
waitForDevice(deviceId, timeoutMs = 3e4) {
|
|
27663
|
+
return new Promise((resolve, reject) => {
|
|
27664
|
+
const existing = this.getDeviceById(deviceId);
|
|
27665
|
+
if (existing) {
|
|
27666
|
+
resolve(existing);
|
|
27667
|
+
return;
|
|
27668
|
+
}
|
|
27669
|
+
let timer = null;
|
|
27670
|
+
const off = this.onDeviceAdded((id) => {
|
|
27671
|
+
if (id !== deviceId) return;
|
|
27672
|
+
const proxy = this.getDeviceById(id);
|
|
27673
|
+
if (!proxy) return;
|
|
27674
|
+
if (timer) clearTimeout(timer);
|
|
27675
|
+
off();
|
|
27676
|
+
resolve(proxy);
|
|
27677
|
+
});
|
|
27678
|
+
if (Number.isFinite(timeoutMs)) timer = setTimeout(() => {
|
|
27679
|
+
off();
|
|
27680
|
+
reject(/* @__PURE__ */ new Error(`waitForDevice timed out after ${timeoutMs}ms (deviceId=${deviceId})`));
|
|
27681
|
+
}, timeoutMs);
|
|
27682
|
+
});
|
|
27683
|
+
}
|
|
27684
|
+
/**
|
|
27685
|
+
* Iterate every device that binds `capName`. Awaits each callback
|
|
27686
|
+
* sequentially — for parallel use `invokeCap` with explicit
|
|
27687
|
+
* parallelism.
|
|
27688
|
+
*/
|
|
27689
|
+
async forEachCap(capName, cb) {
|
|
27690
|
+
for (const proxy of this.filterByCap(capName)) await cb(proxy);
|
|
27691
|
+
}
|
|
27692
|
+
/**
|
|
27693
|
+
* Invoke a cap method on every device that binds the cap. Returns
|
|
27694
|
+
* one result per device, success or failure isolated. Optional
|
|
27695
|
+
* parallelism cap — useful for "snapshot all cameras but only 4
|
|
27696
|
+
* at a time so battery cams don't all wake at once".
|
|
27697
|
+
*
|
|
27698
|
+
* Example:
|
|
27699
|
+
*
|
|
27700
|
+
* const results = await sm.invokeCap('snapshot', 'getSnapshot', {}, { parallelism: 4 })
|
|
27701
|
+
* const failed = results.filter(r => !r.ok)
|
|
27702
|
+
*/
|
|
27703
|
+
async invokeCap(capName, methodName, args, opts = {}) {
|
|
27704
|
+
const targets = this.filterByCap(capName);
|
|
27705
|
+
const parallelism = Math.max(1, opts.parallelism ?? targets.length);
|
|
27706
|
+
const out = [];
|
|
27707
|
+
for (let i = 0; i < targets.length; i += parallelism) {
|
|
27708
|
+
const chunk = targets.slice(i, i + parallelism);
|
|
27709
|
+
const settled = await Promise.allSettled(chunk.map(async (proxy) => {
|
|
27710
|
+
const cap = proxy[capName];
|
|
27711
|
+
if (!cap || typeof cap[methodName] !== "function") throw new Error(`device '${proxy.deviceId}' does not expose '${capName}.${methodName}'`);
|
|
27712
|
+
return await cap[methodName](args);
|
|
27713
|
+
}));
|
|
27714
|
+
for (let j = 0; j < settled.length; j++) {
|
|
27715
|
+
const proxy = chunk[j];
|
|
27716
|
+
const r = settled[j];
|
|
27717
|
+
if (r.status === "fulfilled") out.push({
|
|
27718
|
+
deviceId: proxy.deviceId,
|
|
27719
|
+
ok: true,
|
|
27720
|
+
result: r.value
|
|
27721
|
+
});
|
|
27722
|
+
else out.push({
|
|
27723
|
+
deviceId: proxy.deviceId,
|
|
27724
|
+
ok: false,
|
|
27725
|
+
error: r.reason
|
|
27726
|
+
});
|
|
27727
|
+
}
|
|
27728
|
+
}
|
|
27729
|
+
return out;
|
|
27730
|
+
}
|
|
27731
|
+
/**
|
|
27732
|
+
* One-shot summary — fleet size, breakdown by cap / addon / type.
|
|
27733
|
+
* Designed for REPL inspection (`sm.summary()`).
|
|
27734
|
+
*/
|
|
27735
|
+
summary() {
|
|
27736
|
+
const byCap = {};
|
|
27737
|
+
const byAddon = {};
|
|
27738
|
+
const byType = {};
|
|
27739
|
+
let online = 0;
|
|
27740
|
+
let offline = 0;
|
|
27741
|
+
for (const binding of this.bindings.values()) for (const entry of binding.entries) byCap[entry.capName] = (byCap[entry.capName] ?? 0) + 1;
|
|
27742
|
+
for (const info of this.devices.values()) {
|
|
27743
|
+
byAddon[info.addonId] = (byAddon[info.addonId] ?? 0) + 1;
|
|
27744
|
+
byType[info.type] = (byType[info.type] ?? 0) + 1;
|
|
27745
|
+
if (info.online) online++;
|
|
27746
|
+
else offline++;
|
|
27747
|
+
}
|
|
27748
|
+
return {
|
|
27749
|
+
totalDevices: this.bindings.size,
|
|
27750
|
+
online,
|
|
27751
|
+
offline,
|
|
27752
|
+
byCap,
|
|
27753
|
+
byAddon,
|
|
27754
|
+
byType,
|
|
27755
|
+
statedDevices: this.stateMirror.size
|
|
27756
|
+
};
|
|
27757
|
+
}
|
|
27758
|
+
/**
|
|
27759
|
+
* Debug-friendly dump — full state + binding + metadata for one
|
|
27760
|
+
* device or all devices. Cheap deep clone so caller mutations don't
|
|
27761
|
+
* leak into the mirror.
|
|
27762
|
+
*/
|
|
27763
|
+
dump(deviceId) {
|
|
27764
|
+
const dumpOne = (id) => {
|
|
27765
|
+
const info = this.devices.get(id) ?? null;
|
|
27766
|
+
const binding = this.bindings.get(id) ?? null;
|
|
27767
|
+
const state = {};
|
|
27768
|
+
const perCap = this.stateMirror.get(id);
|
|
27769
|
+
if (perCap) for (const [cap, slice] of perCap) state[cap] = { ...slice };
|
|
27770
|
+
return {
|
|
27771
|
+
deviceId: id,
|
|
27772
|
+
info,
|
|
27773
|
+
binding: binding ? {
|
|
27774
|
+
...binding,
|
|
27775
|
+
entries: binding.entries.map((e) => ({ ...e }))
|
|
27776
|
+
} : null,
|
|
27777
|
+
state
|
|
27778
|
+
};
|
|
27779
|
+
};
|
|
27780
|
+
if (deviceId !== void 0) return dumpOne(deviceId);
|
|
27781
|
+
const out = [];
|
|
27782
|
+
for (const id of this.bindings.keys()) out.push(dumpOne(id));
|
|
27783
|
+
return out;
|
|
27784
|
+
}
|
|
27785
|
+
/**
|
|
27786
|
+
* Direct read-only access to the underlying state mirror. Use
|
|
27787
|
+
* sparingly — `getSystemState` returns a deep copy that's safer for
|
|
27788
|
+
* exploratory work; this avoids the clone cost when iterating
|
|
27789
|
+
* thousands of slices.
|
|
27790
|
+
*/
|
|
27791
|
+
getRawMirror() {
|
|
27792
|
+
return this.stateMirror;
|
|
27793
|
+
}
|
|
27794
|
+
/**
|
|
27795
|
+
* Snapshot of the full state mirror — same shape as the warm-boot
|
|
27796
|
+
* payload. Deep-cloned; safe to mutate.
|
|
27797
|
+
*/
|
|
27798
|
+
getSystemState() {
|
|
27799
|
+
const copy = /* @__PURE__ */ new Map();
|
|
27800
|
+
for (const [id, perCap] of this.stateMirror) {
|
|
27801
|
+
const dup = /* @__PURE__ */ new Map();
|
|
27802
|
+
for (const [k, v] of perCap) dup.set(k, { ...v });
|
|
27803
|
+
copy.set(id, dup);
|
|
27804
|
+
}
|
|
27805
|
+
return copy;
|
|
27806
|
+
}
|
|
27807
|
+
subscribeBus() {
|
|
27808
|
+
if (!this.api.live?.onEvent) return;
|
|
27809
|
+
const sub = this.api.live.onEvent;
|
|
27810
|
+
this.bridges.push(sub.subscribe({ category: STATE_CHANGED_CATEGORY }, { onData: (evt) => {
|
|
27811
|
+
const data = evt.data;
|
|
27812
|
+
if (!data || typeof data.deviceId !== "number" || typeof data.capName !== "string") return;
|
|
27813
|
+
this.applyStateUpdate(data.deviceId, data.capName, data.slice);
|
|
27814
|
+
} }));
|
|
27815
|
+
this.bridges.push(sub.subscribe({ category: BINDING_CHANGED_CATEGORY }, { onData: (evt) => {
|
|
27816
|
+
const data = evt.data;
|
|
27817
|
+
const deviceId = typeof data?.deviceId === "number" ? data.deviceId : data?.source?.type === "device" && typeof data.source.id === "number" ? data.source.id : null;
|
|
27818
|
+
if (deviceId === null) return;
|
|
27819
|
+
this.refreshBinding(deviceId);
|
|
27820
|
+
} }));
|
|
27821
|
+
this.bridges.push(sub.subscribe({ category: DEVICE_REGISTERED_CATEGORY }, { onData: (evt) => {
|
|
27822
|
+
const data = evt.data;
|
|
27823
|
+
if (typeof data?.deviceId !== "number") return;
|
|
27824
|
+
this.refreshDeviceMetadata(data.deviceId, "added");
|
|
27825
|
+
} }));
|
|
27826
|
+
this.bridges.push(sub.subscribe({ category: DEVICE_UNREGISTERED_CATEGORY }, { onData: (evt) => {
|
|
27827
|
+
const data = evt.data;
|
|
27828
|
+
if (typeof data?.deviceId !== "number") return;
|
|
27829
|
+
this.applyDeviceRemoval(data.deviceId);
|
|
27830
|
+
} }));
|
|
27831
|
+
this.bridges.push(sub.subscribe({ category: DEVICE_UPDATED_CATEGORY }, { onData: (evt) => {
|
|
27832
|
+
const data = evt.data;
|
|
27833
|
+
if (typeof data?.deviceId !== "number") return;
|
|
27834
|
+
this.refreshDeviceMetadata(data.deviceId, "updated");
|
|
27835
|
+
} }));
|
|
27836
|
+
}
|
|
27837
|
+
applyStateUpdate(deviceId, capName, slice) {
|
|
27838
|
+
let perCap = this.stateMirror.get(deviceId);
|
|
27839
|
+
if (!perCap) {
|
|
27840
|
+
perCap = /* @__PURE__ */ new Map();
|
|
27841
|
+
this.stateMirror.set(deviceId, perCap);
|
|
27842
|
+
}
|
|
27843
|
+
if (slice === void 0) perCap.delete(capName);
|
|
27844
|
+
else perCap.set(capName, slice);
|
|
27845
|
+
const handleKey = `${deviceId}:${capName}`;
|
|
27846
|
+
const handleSet = this.handleListeners.get(handleKey);
|
|
27847
|
+
if (handleSet) for (const cb of handleSet) try {
|
|
27848
|
+
cb(slice);
|
|
27849
|
+
} catch {}
|
|
27850
|
+
for (const cb of this.globalStateListeners) try {
|
|
27851
|
+
cb(deviceId, capName, slice);
|
|
27852
|
+
} catch {}
|
|
27853
|
+
const capSet = this.capListeners.get(capName);
|
|
27854
|
+
if (capSet) for (const cb of capSet) try {
|
|
27855
|
+
cb(deviceId, slice);
|
|
27856
|
+
} catch {}
|
|
27857
|
+
const devSet = this.deviceListeners.get(deviceId);
|
|
27858
|
+
if (devSet) for (const cb of devSet) try {
|
|
27859
|
+
cb(deviceId, capName, slice);
|
|
27860
|
+
} catch {}
|
|
27861
|
+
}
|
|
27862
|
+
applyDeviceRemoval(deviceId) {
|
|
27863
|
+
const lastInfo = this.devices.get(deviceId) ?? null;
|
|
27864
|
+
this.bindings.delete(deviceId);
|
|
27865
|
+
this.devices.delete(deviceId);
|
|
27866
|
+
this.stateMirror.delete(deviceId);
|
|
27867
|
+
for (const cb of this.removedListeners) try {
|
|
27868
|
+
cb(deviceId, lastInfo);
|
|
27869
|
+
} catch {}
|
|
27870
|
+
}
|
|
27871
|
+
async refreshBinding(deviceId) {
|
|
27872
|
+
try {
|
|
27873
|
+
const fresh = (await this.api.deviceManager.getAllBindings.query({})).find((b) => b.deviceId === deviceId);
|
|
27874
|
+
if (fresh) this.bindings.set(deviceId, fresh);
|
|
27875
|
+
else this.applyDeviceRemoval(deviceId);
|
|
27876
|
+
} catch {}
|
|
27877
|
+
}
|
|
27878
|
+
async refreshDeviceMetadata(deviceId, kind) {
|
|
27879
|
+
try {
|
|
27880
|
+
const info = await this.api.deviceManager.getDevice.query({ deviceId });
|
|
27881
|
+
if (!info) return;
|
|
27882
|
+
const wasNew = !this.devices.has(deviceId);
|
|
27883
|
+
this.devices.set(deviceId, info);
|
|
27884
|
+
if (kind === "added" && wasNew) {
|
|
27885
|
+
await this.refreshBinding(deviceId);
|
|
27886
|
+
for (const cb of this.addedListeners) try {
|
|
27887
|
+
cb(deviceId, info);
|
|
27888
|
+
} catch {}
|
|
27889
|
+
}
|
|
27890
|
+
} catch {}
|
|
27891
|
+
}
|
|
27892
|
+
};
|
|
27893
|
+
function inSet(value, set) {
|
|
27894
|
+
if (Array.isArray(set)) return set.includes(value);
|
|
27895
|
+
return value === set;
|
|
27896
|
+
}
|
|
27897
|
+
function toArray(value) {
|
|
27898
|
+
return Array.isArray(value) ? value : [value];
|
|
27899
|
+
}
|
|
27900
|
+
function matchesString(haystack, match) {
|
|
27901
|
+
if (typeof match === "string") return haystack === match;
|
|
27902
|
+
if (match instanceof RegExp) return match.test(haystack);
|
|
27903
|
+
if ("exact" in match) return haystack === match.exact;
|
|
27904
|
+
if ("contains" in match) return haystack.toLowerCase().includes(match.contains.toLowerCase());
|
|
27905
|
+
return false;
|
|
27906
|
+
}
|
|
27907
|
+
//#endregion
|
|
27908
|
+
//#region src/device/zod-to-config-ui.ts
|
|
27909
|
+
/** Access Zod v4 internal .def — not in public typings but stable at runtime */
|
|
27910
|
+
function zodDef(schema) {
|
|
27911
|
+
return schema.def;
|
|
27912
|
+
}
|
|
27913
|
+
/** Access internal properties on a Zod schema instance */
|
|
27914
|
+
function zodInternals(schema) {
|
|
27915
|
+
return schema;
|
|
27916
|
+
}
|
|
27917
|
+
/**
|
|
27918
|
+
* Convert DeviceConfig.entries() output to ConfigUISchema for the admin UI FormBuilder.
|
|
27919
|
+
*
|
|
27920
|
+
* Each entry's Zod type is inspected to determine the correct ConfigField type:
|
|
27921
|
+
* - ZodString → 'text' (or 'password' when key contains "password"/"secret"/"token"/"apikey")
|
|
27922
|
+
* - ZodNumber → 'number' (extracts min/max/step from Zod v4 checks)
|
|
27923
|
+
* - ZodBoolean → 'boolean'
|
|
27924
|
+
* - ZodEnum → 'select' (options built from enum values)
|
|
27925
|
+
* - Anything else → 'text' fallback
|
|
27926
|
+
*
|
|
27927
|
+
* Wrapper types ZodDefault, ZodOptional, and ZodNullable are unwrapped transparently.
|
|
27928
|
+
* Default values are extracted from ZodDefault wrappers.
|
|
27929
|
+
*/
|
|
27930
|
+
function zodEntriesToConfigUI(entries, sectionTitle = "Configuration", sectionId = "main") {
|
|
27931
|
+
return { sections: [{
|
|
27932
|
+
id: sectionId,
|
|
27933
|
+
title: sectionTitle,
|
|
27934
|
+
fields: entries.map((entry) => zodToConfigField(entry.key, entry.schema, entry.description))
|
|
27935
|
+
}] };
|
|
27936
|
+
}
|
|
27937
|
+
function zodToConfigField(key, schema, description) {
|
|
27938
|
+
const inner = unwrapZod(schema);
|
|
27939
|
+
const defaultValue = getZodDefault(schema);
|
|
27940
|
+
const base = {
|
|
27941
|
+
key,
|
|
27942
|
+
label: description ?? humanizeKey(key),
|
|
27943
|
+
description,
|
|
27944
|
+
default: defaultValue
|
|
27945
|
+
};
|
|
27946
|
+
if (inner instanceof z.ZodString) return buildStringField(key, base);
|
|
27947
|
+
if (inner instanceof z.ZodNumber) return buildNumberField(inner, base);
|
|
27948
|
+
if (inner instanceof z.ZodBoolean) return {
|
|
27949
|
+
...base,
|
|
27950
|
+
type: "boolean"
|
|
27951
|
+
};
|
|
27952
|
+
if (inner instanceof z.ZodEnum) return buildEnumField(inner, base);
|
|
27953
|
+
if (inner instanceof z.ZodArray || inner instanceof z.ZodObject) return {
|
|
27954
|
+
...base,
|
|
27955
|
+
type: "textarea",
|
|
27956
|
+
rows: 6,
|
|
27957
|
+
isJson: true
|
|
27958
|
+
};
|
|
27959
|
+
return {
|
|
27960
|
+
...base,
|
|
27961
|
+
type: "text"
|
|
27962
|
+
};
|
|
27963
|
+
}
|
|
27964
|
+
function buildStringField(key, base) {
|
|
27965
|
+
const lowerKey = key.toLowerCase();
|
|
27966
|
+
if (lowerKey.includes("password") || lowerKey.includes("secret") || lowerKey.includes("token") || lowerKey.includes("apikey") || lowerKey.includes("api_key")) return {
|
|
27967
|
+
...base,
|
|
27968
|
+
type: "password",
|
|
27969
|
+
showToggle: true
|
|
27970
|
+
};
|
|
27971
|
+
return {
|
|
27972
|
+
...base,
|
|
27973
|
+
type: "text"
|
|
27974
|
+
};
|
|
27975
|
+
}
|
|
27976
|
+
function buildNumberField(inner, base) {
|
|
27977
|
+
const anyInner = zodInternals(inner);
|
|
27978
|
+
const rawMin = anyInner.minValue;
|
|
27979
|
+
const rawMax = anyInner.maxValue;
|
|
27980
|
+
const min = rawMin != null && isFinite(rawMin) ? rawMin : void 0;
|
|
27981
|
+
const max = rawMax != null && isFinite(rawMax) ? rawMax : void 0;
|
|
27982
|
+
const step = getMultipleOfStep(inner);
|
|
27983
|
+
return {
|
|
27984
|
+
...base,
|
|
27985
|
+
type: "number",
|
|
27986
|
+
...min !== void 0 ? { min } : {},
|
|
27987
|
+
...max !== void 0 ? { max } : {},
|
|
27988
|
+
...step !== void 0 ? { step } : {}
|
|
27989
|
+
};
|
|
27990
|
+
}
|
|
27991
|
+
function getMultipleOfStep(inner) {
|
|
27992
|
+
const checks = zodDef(inner).checks ?? [];
|
|
27993
|
+
for (const check of checks) if (check._zod?.def?.check === "multiple_of" && check._zod.def.value !== void 0) return check._zod.def.value;
|
|
27994
|
+
}
|
|
27995
|
+
function buildEnumField(inner, base) {
|
|
27996
|
+
const values = inner.options.map((v) => String(v));
|
|
27997
|
+
return {
|
|
27998
|
+
...base,
|
|
27999
|
+
type: "select",
|
|
28000
|
+
options: values.map((v) => ({
|
|
28001
|
+
label: humanizeKey(v),
|
|
28002
|
+
value: v
|
|
28003
|
+
}))
|
|
28004
|
+
};
|
|
28005
|
+
}
|
|
28006
|
+
function unwrapZod(schema) {
|
|
28007
|
+
if (schema instanceof z.ZodDefault) return unwrapZod(zodDef(schema).innerType);
|
|
28008
|
+
if (schema instanceof z.ZodOptional) return unwrapZod(zodDef(schema).innerType);
|
|
28009
|
+
if (schema instanceof z.ZodNullable) return unwrapZod(zodDef(schema).innerType);
|
|
28010
|
+
return schema;
|
|
28011
|
+
}
|
|
28012
|
+
function getZodDefault(schema) {
|
|
28013
|
+
if (schema instanceof z.ZodDefault) return zodDef(schema).defaultValue;
|
|
28014
|
+
}
|
|
28015
|
+
function humanizeKey(key) {
|
|
28016
|
+
return key.replace(/([A-Z])/g, " $1").replace(/[_-]/g, " ").replace(/^\w/, (c) => c.toUpperCase()).trim();
|
|
28017
|
+
}
|
|
28018
|
+
//#endregion
|
|
28019
|
+
//#region src/expression/errors.ts
|
|
28020
|
+
/**
|
|
28021
|
+
* Error types for the safe expression engine. Two distinct classes so callers
|
|
28022
|
+
* can tell a compile-time (grammar) failure from a runtime (evaluation)
|
|
28023
|
+
* failure — both are non-fatal to the host: read paths degrade to "skip link".
|
|
28024
|
+
*/
|
|
28025
|
+
/** Thrown by the tokenizer / parser. Carries a 0-based source `position` when
|
|
28026
|
+
* the failure is anchored to a character (author-facing inline feedback). */
|
|
28027
|
+
var ExpressionParseError = class extends Error {
|
|
28028
|
+
position;
|
|
28029
|
+
constructor(message, position) {
|
|
28030
|
+
super(message);
|
|
28031
|
+
this.name = "ExpressionParseError";
|
|
28032
|
+
this.position = position;
|
|
28033
|
+
}
|
|
28034
|
+
};
|
|
28035
|
+
/** Thrown by the evaluator (unknown identifier, type mismatch, non-finite
|
|
28036
|
+
* result, unknown builtin, step-budget exceeded). */
|
|
28037
|
+
var ExpressionEvalError = class extends Error {
|
|
28038
|
+
constructor(message) {
|
|
28039
|
+
super(message);
|
|
28040
|
+
this.name = "ExpressionEvalError";
|
|
28041
|
+
}
|
|
28042
|
+
};
|
|
28043
|
+
//#endregion
|
|
28044
|
+
//#region src/expression/builtins.ts
|
|
28045
|
+
/**
|
|
28046
|
+
* Frozen, null-prototype builtin function table for the expression engine
|
|
28047
|
+
* (spec §4 rule 4). The table is the SOLE surface of callable functions: the
|
|
28048
|
+
* parser rejects any callee not in it, and the evaluator gates each call on an
|
|
28049
|
+
* own-property check against it.
|
|
28050
|
+
*
|
|
28051
|
+
* Because the object has a NULL prototype AND is `Object.freeze`d:
|
|
28052
|
+
* - it cannot be polluted (no `__proto__` / `constructor` write reaches it);
|
|
28053
|
+
* - a lookup for `toString` / `hasOwnProperty` / `constructor` finds NOTHING
|
|
28054
|
+
* (there is no `Object.prototype` in the chain), so those names are not
|
|
28055
|
+
* callable — they are simply "unknown function" at parse time.
|
|
28056
|
+
*
|
|
28057
|
+
* Every numeric argument is validated as a finite number and every numeric
|
|
28058
|
+
* RESULT is re-checked finite, so `/0`, `sqrt(-1)` (→ NaN) and overflow
|
|
28059
|
+
* (`pow(10,400)` → Infinity) all raise `ExpressionEvalError` and fail the link
|
|
28060
|
+
* closed rather than emitting a garbage value.
|
|
28061
|
+
*/
|
|
28062
|
+
function asFiniteNumber(value, name, index) {
|
|
28063
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a finite number`);
|
|
28064
|
+
return value;
|
|
28065
|
+
}
|
|
28066
|
+
function asString$1(value, name, index) {
|
|
28067
|
+
if (typeof value !== "string") throw new ExpressionEvalError(`${name}: argument ${index + 1} must be a string`);
|
|
28068
|
+
return value;
|
|
28069
|
+
}
|
|
28070
|
+
function finiteResult(value, name) {
|
|
28071
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${name}: produced a non-finite result`);
|
|
28072
|
+
return value;
|
|
28073
|
+
}
|
|
28074
|
+
function allFiniteNumbers(args, name) {
|
|
28075
|
+
return args.map((a, idx) => asFiniteNumber(a, name, idx));
|
|
28076
|
+
}
|
|
28077
|
+
var INF = Number.POSITIVE_INFINITY;
|
|
28078
|
+
var table = {
|
|
28079
|
+
min: {
|
|
28080
|
+
minArgs: 1,
|
|
28081
|
+
maxArgs: INF,
|
|
28082
|
+
apply: (args) => finiteResult(Math.min(...allFiniteNumbers(args, "min")), "min")
|
|
28083
|
+
},
|
|
28084
|
+
max: {
|
|
28085
|
+
minArgs: 1,
|
|
28086
|
+
maxArgs: INF,
|
|
28087
|
+
apply: (args) => finiteResult(Math.max(...allFiniteNumbers(args, "max")), "max")
|
|
28088
|
+
},
|
|
28089
|
+
abs: {
|
|
28090
|
+
minArgs: 1,
|
|
28091
|
+
maxArgs: 1,
|
|
28092
|
+
apply: (args) => finiteResult(Math.abs(asFiniteNumber(args[0], "abs", 0)), "abs")
|
|
28093
|
+
},
|
|
28094
|
+
floor: {
|
|
28095
|
+
minArgs: 1,
|
|
28096
|
+
maxArgs: 1,
|
|
28097
|
+
apply: (args) => finiteResult(Math.floor(asFiniteNumber(args[0], "floor", 0)), "floor")
|
|
28098
|
+
},
|
|
28099
|
+
ceil: {
|
|
28100
|
+
minArgs: 1,
|
|
28101
|
+
maxArgs: 1,
|
|
28102
|
+
apply: (args) => finiteResult(Math.ceil(asFiniteNumber(args[0], "ceil", 0)), "ceil")
|
|
28103
|
+
},
|
|
28104
|
+
sqrt: {
|
|
28105
|
+
minArgs: 1,
|
|
28106
|
+
maxArgs: 1,
|
|
28107
|
+
apply: (args) => finiteResult(Math.sqrt(asFiniteNumber(args[0], "sqrt", 0)), "sqrt")
|
|
28108
|
+
},
|
|
28109
|
+
round: {
|
|
28110
|
+
minArgs: 1,
|
|
28111
|
+
maxArgs: 2,
|
|
28112
|
+
apply: (args) => {
|
|
28113
|
+
const x = asFiniteNumber(args[0], "round", 0);
|
|
28114
|
+
const digits = args.length > 1 ? Math.trunc(asFiniteNumber(args[1], "round", 1)) : 0;
|
|
28115
|
+
if (digits < 0 || digits > 100) throw new ExpressionEvalError("round: digits must be between 0 and 100");
|
|
28116
|
+
const factor = 10 ** digits;
|
|
28117
|
+
return finiteResult(Math.round(x * factor) / factor, "round");
|
|
28118
|
+
}
|
|
28119
|
+
},
|
|
28120
|
+
pow: {
|
|
28121
|
+
minArgs: 2,
|
|
28122
|
+
maxArgs: 2,
|
|
28123
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "pow", 0) ** asFiniteNumber(args[1], "pow", 1), "pow")
|
|
28124
|
+
},
|
|
28125
|
+
clamp: {
|
|
28126
|
+
minArgs: 3,
|
|
28127
|
+
maxArgs: 3,
|
|
28128
|
+
apply: (args) => {
|
|
28129
|
+
const x = asFiniteNumber(args[0], "clamp", 0);
|
|
28130
|
+
const lo = asFiniteNumber(args[1], "clamp", 1);
|
|
28131
|
+
const hi = asFiniteNumber(args[2], "clamp", 2);
|
|
28132
|
+
if (lo > hi) throw new ExpressionEvalError("clamp: lower bound is greater than upper bound");
|
|
28133
|
+
return finiteResult(Math.min(hi, Math.max(lo, x)), "clamp");
|
|
28134
|
+
}
|
|
28135
|
+
},
|
|
28136
|
+
avg: {
|
|
28137
|
+
minArgs: 1,
|
|
28138
|
+
maxArgs: INF,
|
|
28139
|
+
apply: (args) => {
|
|
28140
|
+
const nums = allFiniteNumbers(args, "avg");
|
|
28141
|
+
return finiteResult(nums.reduce((acc, v) => acc + v, 0) / nums.length, "avg");
|
|
28142
|
+
}
|
|
28143
|
+
},
|
|
28144
|
+
sum: {
|
|
28145
|
+
minArgs: 1,
|
|
28146
|
+
maxArgs: INF,
|
|
28147
|
+
apply: (args) => finiteResult(allFiniteNumbers(args, "sum").reduce((acc, v) => acc + v, 0), "sum")
|
|
28148
|
+
},
|
|
28149
|
+
coalesce: {
|
|
28150
|
+
minArgs: 1,
|
|
28151
|
+
maxArgs: INF,
|
|
28152
|
+
apply: (args) => {
|
|
28153
|
+
for (const a of args) if (a !== null) return a;
|
|
28154
|
+
return null;
|
|
28155
|
+
}
|
|
28156
|
+
},
|
|
28157
|
+
age: {
|
|
28158
|
+
minArgs: 2,
|
|
28159
|
+
maxArgs: 2,
|
|
28160
|
+
apply: (args) => finiteResult(asFiniteNumber(args[0], "age", 0) - asFiniteNumber(args[1], "age", 1), "age")
|
|
28161
|
+
},
|
|
28162
|
+
convert: {
|
|
28163
|
+
minArgs: 3,
|
|
28164
|
+
maxArgs: 3,
|
|
28165
|
+
apply: (args, hooks) => {
|
|
28166
|
+
const x = asFiniteNumber(args[0], "convert", 0);
|
|
28167
|
+
const from = asString$1(args[1], "convert", 1).trim();
|
|
28168
|
+
const to = asString$1(args[2], "convert", 2).trim();
|
|
28169
|
+
if (hooks.convert) {
|
|
28170
|
+
const out = hooks.convert(x, from, to);
|
|
28171
|
+
if (out === null) throw new ExpressionEvalError(`convert: cannot convert '${from}' to '${to}'`);
|
|
28172
|
+
return finiteResult(out, "convert");
|
|
28173
|
+
}
|
|
28174
|
+
if (from === to) return x;
|
|
28175
|
+
throw new ExpressionEvalError("convert: unit conversion table not installed");
|
|
28176
|
+
}
|
|
28177
|
+
}
|
|
28178
|
+
};
|
|
28179
|
+
/** Frozen, null-prototype builtin table. */
|
|
28180
|
+
var EXPRESSION_BUILTINS = Object.freeze(Object.assign(Object.create(null), table));
|
|
28181
|
+
/** The set of valid builtin names — used by the parser to reject unknown
|
|
28182
|
+
* callees at parse time (immediate author feedback). */
|
|
28183
|
+
var EXPRESSION_BUILTIN_NAMES = new Set(Object.keys(table));
|
|
28184
|
+
//#endregion
|
|
28185
|
+
//#region src/expression/limits.ts
|
|
28186
|
+
/**
|
|
28187
|
+
* Resource-bound constants for the safe expression engine.
|
|
28188
|
+
*
|
|
28189
|
+
* Every bound is defense-in-depth: the grammar is non-Turing-complete (no
|
|
28190
|
+
* loops, recursion, lambdas or member access — see `ast.ts`), so evaluation is
|
|
28191
|
+
* O(nodeCount) by construction. These caps merely put a hard ceiling on the
|
|
28192
|
+
* work a single author-supplied expression can request, so a hostile or
|
|
28193
|
+
* accidental pathological string can never spend unbounded CPU/memory.
|
|
28194
|
+
*/
|
|
28195
|
+
/** Max source length (chars) — checked BEFORE tokenizing so a huge string is
|
|
28196
|
+
* rejected without allocation. */
|
|
28197
|
+
var MAX_EXPRESSION_SOURCE_LENGTH = 2048;
|
|
28198
|
+
/** Max AST nodes — checked during parse; a deeply nested grouping that exceeds
|
|
28199
|
+
* this is rejected as "expression too complex". */
|
|
28200
|
+
var MAX_EXPRESSION_AST_NODES = 256;
|
|
28201
|
+
/** Defense-in-depth walker step budget — one increment per node visit during
|
|
28202
|
+
* evaluation. The grammar guarantees O(nodeCount) walks, so this can only trip
|
|
28203
|
+
* on a crafted maximum-size AST. */
|
|
28204
|
+
var MAX_EXPRESSION_EVAL_STEPS = 4096;
|
|
28205
|
+
/** Max named bindings on one {@link ExpressionSource}. */
|
|
28206
|
+
var MAX_EXPRESSION_BINDINGS = 32;
|
|
28207
|
+
/** Max positional arguments to any builtin call. */
|
|
28208
|
+
var MAX_EXPRESSION_CALL_ARGS = 16;
|
|
28209
|
+
/** LRU compile-cache capacity (parsed ASTs keyed by raw source string). */
|
|
28210
|
+
var EXPRESSION_COMPILE_CACHE_CAPACITY = 256;
|
|
28211
|
+
/** A legal binding / identifier name. */
|
|
28212
|
+
var EXPRESSION_IDENTIFIER_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
28213
|
+
/** Binding names an author may NOT use: `now` is auto-injected; the literal
|
|
28214
|
+
* keywords lex as values, not identifiers, so binding to them is meaningless. */
|
|
28215
|
+
var RESERVED_BINDING_NAMES = new Set([
|
|
28216
|
+
"now",
|
|
28217
|
+
"true",
|
|
28218
|
+
"false",
|
|
28219
|
+
"null"
|
|
28220
|
+
]);
|
|
28221
|
+
//#endregion
|
|
28222
|
+
//#region src/expression/tokenizer.ts
|
|
28223
|
+
/**
|
|
28224
|
+
* Tokenizer for the safe expression mini-language. Hand-rolled, single-pass,
|
|
28225
|
+
* zero-dependency. The grammar is deliberately boring: decimal numbers,
|
|
28226
|
+
* single/double-quoted strings with a tiny escape set, identifiers, the three
|
|
28227
|
+
* value keywords (`true`/`false`/`null`) and a fixed punctuator set. Anything
|
|
28228
|
+
* outside that — a bare `.`, `=`, `[`, `]`, `{`, `}`, `;`, backtick, `&`, `|` —
|
|
28229
|
+
* is a parse error with a source position, so member access / assignment /
|
|
28230
|
+
* template literals are lexically impossible.
|
|
28231
|
+
*/
|
|
28232
|
+
var KEYWORDS = new Set([
|
|
28233
|
+
"true",
|
|
28234
|
+
"false",
|
|
28235
|
+
"null"
|
|
28236
|
+
]);
|
|
28237
|
+
function isDigit(ch) {
|
|
28238
|
+
return ch >= "0" && ch <= "9";
|
|
28239
|
+
}
|
|
28240
|
+
function isIdentStart(ch) {
|
|
28241
|
+
return ch >= "A" && ch <= "Z" || ch >= "a" && ch <= "z" || ch === "_";
|
|
28242
|
+
}
|
|
28243
|
+
function isIdentPart(ch) {
|
|
28244
|
+
return isIdentStart(ch) || isDigit(ch);
|
|
28245
|
+
}
|
|
28246
|
+
function isWhitespace(ch) {
|
|
28247
|
+
return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
|
|
28248
|
+
}
|
|
28249
|
+
/** Tokenize `source` into a flat token list ending with a single `eof` token.
|
|
28250
|
+
* Throws `ExpressionParseError` on any illegal character or unterminated
|
|
28251
|
+
* string. */
|
|
28252
|
+
function tokenize(source) {
|
|
28253
|
+
if (source.length > 2048) throw new ExpressionParseError(`expression too long (${source.length} > ${MAX_EXPRESSION_SOURCE_LENGTH} chars)`, 0);
|
|
28254
|
+
const tokens = [];
|
|
28255
|
+
let i = 0;
|
|
28256
|
+
const n = source.length;
|
|
28257
|
+
while (i < n) {
|
|
28258
|
+
const ch = source[i];
|
|
28259
|
+
if (isWhitespace(ch)) {
|
|
28260
|
+
i += 1;
|
|
28261
|
+
continue;
|
|
28262
|
+
}
|
|
28263
|
+
if (isDigit(ch)) {
|
|
28264
|
+
const start = i;
|
|
28265
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
28266
|
+
if (i < n && source[i] === ".") {
|
|
28267
|
+
if (i + 1 >= n || !isDigit(source[i + 1])) throw new ExpressionParseError("malformed number: decimal point needs a digit", i);
|
|
28268
|
+
i += 1;
|
|
28269
|
+
while (i < n && isDigit(source[i])) i += 1;
|
|
28270
|
+
}
|
|
28271
|
+
const text = source.slice(start, i);
|
|
28272
|
+
const value = Number(text);
|
|
28273
|
+
if (!Number.isFinite(value)) throw new ExpressionParseError(`malformed number: '${text}'`, start);
|
|
28274
|
+
tokens.push({
|
|
28275
|
+
type: "number",
|
|
28276
|
+
value,
|
|
28277
|
+
pos: start
|
|
28278
|
+
});
|
|
28279
|
+
continue;
|
|
28127
28280
|
}
|
|
28128
|
-
|
|
28129
|
-
|
|
28130
|
-
|
|
28131
|
-
|
|
28132
|
-
|
|
28133
|
-
|
|
28134
|
-
|
|
28135
|
-
|
|
28136
|
-
|
|
28281
|
+
if (ch === "'" || ch === "\"") {
|
|
28282
|
+
const quote = ch;
|
|
28283
|
+
const start = i;
|
|
28284
|
+
i += 1;
|
|
28285
|
+
let out = "";
|
|
28286
|
+
let closed = false;
|
|
28287
|
+
while (i < n) {
|
|
28288
|
+
const c = source[i];
|
|
28289
|
+
if (c === "\\") {
|
|
28290
|
+
const next = i + 1 < n ? source[i + 1] : "";
|
|
28291
|
+
if (next === "\\" || next === "'" || next === "\"") {
|
|
28292
|
+
out += next;
|
|
28293
|
+
i += 2;
|
|
28294
|
+
continue;
|
|
28295
|
+
}
|
|
28296
|
+
throw new ExpressionParseError(`invalid string escape: '\\${next}'`, i);
|
|
28297
|
+
}
|
|
28298
|
+
if (c === quote) {
|
|
28299
|
+
closed = true;
|
|
28300
|
+
i += 1;
|
|
28301
|
+
break;
|
|
28302
|
+
}
|
|
28303
|
+
out += c;
|
|
28304
|
+
i += 1;
|
|
28305
|
+
}
|
|
28306
|
+
if (!closed) throw new ExpressionParseError("unterminated string literal", start);
|
|
28307
|
+
tokens.push({
|
|
28308
|
+
type: "string",
|
|
28309
|
+
value: out,
|
|
28310
|
+
pos: start
|
|
28311
|
+
});
|
|
28312
|
+
continue;
|
|
28137
28313
|
}
|
|
28138
|
-
|
|
28139
|
-
|
|
28140
|
-
|
|
28141
|
-
|
|
28142
|
-
|
|
28143
|
-
|
|
28144
|
-
|
|
28145
|
-
|
|
28146
|
-
|
|
28147
|
-
|
|
28148
|
-
|
|
28149
|
-
|
|
28150
|
-
|
|
28151
|
-
|
|
28152
|
-
|
|
28314
|
+
if (isIdentStart(ch)) {
|
|
28315
|
+
const start = i;
|
|
28316
|
+
while (i < n && isIdentPart(source[i])) i += 1;
|
|
28317
|
+
const text = source.slice(start, i);
|
|
28318
|
+
if (KEYWORDS.has(text)) tokens.push({
|
|
28319
|
+
type: "keyword",
|
|
28320
|
+
keyword: keywordOf(text),
|
|
28321
|
+
pos: start
|
|
28322
|
+
});
|
|
28323
|
+
else tokens.push({
|
|
28324
|
+
type: "identifier",
|
|
28325
|
+
name: text,
|
|
28326
|
+
pos: start
|
|
28327
|
+
});
|
|
28328
|
+
continue;
|
|
28153
28329
|
}
|
|
28154
|
-
|
|
28155
|
-
|
|
28156
|
-
|
|
28157
|
-
|
|
28158
|
-
|
|
28159
|
-
|
|
28160
|
-
|
|
28161
|
-
|
|
28162
|
-
|
|
28163
|
-
this.globalStateListeners.delete(cb);
|
|
28164
|
-
};
|
|
28165
|
-
}
|
|
28166
|
-
/**
|
|
28167
|
-
* Per-cap listener — fires only for state changes on `capName`,
|
|
28168
|
-
* across every device. The callback receives the deviceId so the
|
|
28169
|
-
* caller can route.
|
|
28170
|
-
*/
|
|
28171
|
-
listenCap(capName, cb) {
|
|
28172
|
-
let set = this.capListeners.get(capName);
|
|
28173
|
-
if (!set) {
|
|
28174
|
-
set = /* @__PURE__ */ new Set();
|
|
28175
|
-
this.capListeners.set(capName, set);
|
|
28330
|
+
const two = i + 1 < n ? source.slice(i, i + 2) : "";
|
|
28331
|
+
if (two === "<=" || two === ">=" || two === "==" || two === "!=" || two === "&&" || two === "||") {
|
|
28332
|
+
tokens.push({
|
|
28333
|
+
type: "punct",
|
|
28334
|
+
punct: two,
|
|
28335
|
+
pos: i
|
|
28336
|
+
});
|
|
28337
|
+
i += 2;
|
|
28338
|
+
continue;
|
|
28176
28339
|
}
|
|
28177
|
-
|
|
28178
|
-
|
|
28179
|
-
|
|
28180
|
-
|
|
28181
|
-
|
|
28182
|
-
|
|
28183
|
-
|
|
28184
|
-
|
|
28185
|
-
*/
|
|
28186
|
-
listenDevice(deviceId, cb) {
|
|
28187
|
-
let set = this.deviceListeners.get(deviceId);
|
|
28188
|
-
if (!set) {
|
|
28189
|
-
set = /* @__PURE__ */ new Set();
|
|
28190
|
-
this.deviceListeners.set(deviceId, set);
|
|
28340
|
+
if (isSinglePunct(ch)) {
|
|
28341
|
+
tokens.push({
|
|
28342
|
+
type: "punct",
|
|
28343
|
+
punct: ch,
|
|
28344
|
+
pos: i
|
|
28345
|
+
});
|
|
28346
|
+
i += 1;
|
|
28347
|
+
continue;
|
|
28191
28348
|
}
|
|
28192
|
-
|
|
28193
|
-
return () => {
|
|
28194
|
-
set.delete(cb);
|
|
28195
|
-
if (set.size === 0) this.deviceListeners.delete(deviceId);
|
|
28196
|
-
};
|
|
28349
|
+
throw new ExpressionParseError(`unexpected character '${ch}'`, i);
|
|
28197
28350
|
}
|
|
28198
|
-
|
|
28199
|
-
|
|
28200
|
-
|
|
28201
|
-
|
|
28202
|
-
|
|
28203
|
-
|
|
28351
|
+
tokens.push({
|
|
28352
|
+
type: "eof",
|
|
28353
|
+
pos: n
|
|
28354
|
+
});
|
|
28355
|
+
return tokens;
|
|
28356
|
+
}
|
|
28357
|
+
function keywordOf(text) {
|
|
28358
|
+
if (text === "true") return "true";
|
|
28359
|
+
if (text === "false") return "false";
|
|
28360
|
+
return "null";
|
|
28361
|
+
}
|
|
28362
|
+
function isSinglePunct(ch) {
|
|
28363
|
+
return ch === "(" || ch === ")" || ch === "," || ch === "?" || ch === ":" || ch === "+" || ch === "-" || ch === "*" || ch === "/" || ch === "%" || ch === "!" || ch === "<" || ch === ">";
|
|
28364
|
+
}
|
|
28365
|
+
//#endregion
|
|
28366
|
+
//#region src/expression/parser.ts
|
|
28367
|
+
/**
|
|
28368
|
+
* Pratt (precedence-climbing) parser for the safe expression mini-language.
|
|
28369
|
+
*
|
|
28370
|
+
* Precedence (low → high): ternary `?:` (right-assoc) → `||` → `&&` → equality
|
|
28371
|
+
* → relational → additive → multiplicative → unary `! -` → call / primary.
|
|
28372
|
+
* Calls are ONLY `IDENT '(' args? ')'` at primary position — the callee is a
|
|
28373
|
+
* string validated against the builtin table at parse time, so an unknown
|
|
28374
|
+
* function is rejected immediately (author feedback) and a persisted expression
|
|
28375
|
+
* that references a since-removed builtin degrades at read.
|
|
28376
|
+
*
|
|
28377
|
+
* A node counter caps total AST size (`MAX_EXPRESSION_AST_NODES`) and call
|
|
28378
|
+
* arity is capped (`MAX_EXPRESSION_CALL_ARGS`) — both raise `ExpressionParseError`.
|
|
28379
|
+
*/
|
|
28380
|
+
/** Binary/logical operator precedence (higher binds tighter). */
|
|
28381
|
+
var BINARY_PRECEDENCE = {
|
|
28382
|
+
"||": 1,
|
|
28383
|
+
"&&": 2,
|
|
28384
|
+
"==": 3,
|
|
28385
|
+
"!=": 3,
|
|
28386
|
+
"<": 4,
|
|
28387
|
+
"<=": 4,
|
|
28388
|
+
">": 4,
|
|
28389
|
+
">=": 4,
|
|
28390
|
+
"+": 5,
|
|
28391
|
+
"-": 5,
|
|
28392
|
+
"*": 6,
|
|
28393
|
+
"/": 6,
|
|
28394
|
+
"%": 6
|
|
28395
|
+
};
|
|
28396
|
+
function isLogicalOp(op) {
|
|
28397
|
+
return op === "&&" || op === "||";
|
|
28398
|
+
}
|
|
28399
|
+
function isBinaryOp(op) {
|
|
28400
|
+
return op === "+" || op === "-" || op === "*" || op === "/" || op === "%" || op === "==" || op === "!=" || op === "<" || op === "<=" || op === ">" || op === ">=";
|
|
28401
|
+
}
|
|
28402
|
+
var Parser = class {
|
|
28403
|
+
tokens;
|
|
28404
|
+
pos = 0;
|
|
28405
|
+
nodeCount = 0;
|
|
28406
|
+
identifiers = /* @__PURE__ */ new Set();
|
|
28407
|
+
callees = /* @__PURE__ */ new Set();
|
|
28408
|
+
constructor(tokens) {
|
|
28409
|
+
this.tokens = tokens;
|
|
28204
28410
|
}
|
|
28205
|
-
|
|
28206
|
-
|
|
28207
|
-
|
|
28208
|
-
|
|
28209
|
-
return
|
|
28210
|
-
|
|
28411
|
+
parse() {
|
|
28412
|
+
const ast = this.parseTernary();
|
|
28413
|
+
const tok = this.peek();
|
|
28414
|
+
if (tok.type !== "eof") throw new ExpressionParseError("unexpected trailing input", tok.pos);
|
|
28415
|
+
return {
|
|
28416
|
+
ast,
|
|
28417
|
+
identifiers: this.identifiers,
|
|
28418
|
+
callees: this.callees,
|
|
28419
|
+
nodeCount: this.nodeCount
|
|
28211
28420
|
};
|
|
28212
28421
|
}
|
|
28213
|
-
|
|
28214
|
-
|
|
28215
|
-
* `(deviceId, capName)` becomes true. Resolves immediately if the
|
|
28216
|
-
* current slice already matches. Rejects with `Error('timeout')`
|
|
28217
|
-
* after `timeoutMs` (default 30s; pass `Infinity` to wait forever).
|
|
28218
|
-
*
|
|
28219
|
-
* Returns the matching slice — caller can read it directly without
|
|
28220
|
-
* a second mirror lookup.
|
|
28221
|
-
*/
|
|
28222
|
-
waitForState(deviceId, capName, predicate, timeoutMs = 3e4) {
|
|
28223
|
-
return new Promise((resolve, reject) => {
|
|
28224
|
-
const check = () => {
|
|
28225
|
-
const slice = this.stateMirror.get(deviceId)?.get(capName);
|
|
28226
|
-
if (slice && predicate(slice)) return slice;
|
|
28227
|
-
return null;
|
|
28228
|
-
};
|
|
28229
|
-
const initial = check();
|
|
28230
|
-
if (initial) {
|
|
28231
|
-
resolve(initial);
|
|
28232
|
-
return;
|
|
28233
|
-
}
|
|
28234
|
-
let timer = null;
|
|
28235
|
-
const off = this.listenDevice(deviceId, (_id, cap, slice) => {
|
|
28236
|
-
if (cap !== capName) return;
|
|
28237
|
-
if (!slice) return;
|
|
28238
|
-
if (predicate(slice)) {
|
|
28239
|
-
if (timer) clearTimeout(timer);
|
|
28240
|
-
off();
|
|
28241
|
-
resolve(slice);
|
|
28242
|
-
}
|
|
28243
|
-
});
|
|
28244
|
-
if (Number.isFinite(timeoutMs)) timer = setTimeout(() => {
|
|
28245
|
-
off();
|
|
28246
|
-
reject(/* @__PURE__ */ new Error(`waitForState timed out after ${timeoutMs}ms (deviceId=${deviceId}, capName=${capName})`));
|
|
28247
|
-
}, timeoutMs);
|
|
28248
|
-
});
|
|
28422
|
+
peek() {
|
|
28423
|
+
return this.tokens[this.pos];
|
|
28249
28424
|
}
|
|
28250
|
-
|
|
28251
|
-
|
|
28252
|
-
* binding exists). Resolves immediately if already known. Rejects
|
|
28253
|
-
* with timeout.
|
|
28254
|
-
*/
|
|
28255
|
-
waitForDevice(deviceId, timeoutMs = 3e4) {
|
|
28256
|
-
return new Promise((resolve, reject) => {
|
|
28257
|
-
const existing = this.getDeviceById(deviceId);
|
|
28258
|
-
if (existing) {
|
|
28259
|
-
resolve(existing);
|
|
28260
|
-
return;
|
|
28261
|
-
}
|
|
28262
|
-
let timer = null;
|
|
28263
|
-
const off = this.onDeviceAdded((id) => {
|
|
28264
|
-
if (id !== deviceId) return;
|
|
28265
|
-
const proxy = this.getDeviceById(id);
|
|
28266
|
-
if (!proxy) return;
|
|
28267
|
-
if (timer) clearTimeout(timer);
|
|
28268
|
-
off();
|
|
28269
|
-
resolve(proxy);
|
|
28270
|
-
});
|
|
28271
|
-
if (Number.isFinite(timeoutMs)) timer = setTimeout(() => {
|
|
28272
|
-
off();
|
|
28273
|
-
reject(/* @__PURE__ */ new Error(`waitForDevice timed out after ${timeoutMs}ms (deviceId=${deviceId})`));
|
|
28274
|
-
}, timeoutMs);
|
|
28275
|
-
});
|
|
28425
|
+
next() {
|
|
28426
|
+
return this.tokens[this.pos++];
|
|
28276
28427
|
}
|
|
28277
|
-
/**
|
|
28278
|
-
|
|
28279
|
-
|
|
28280
|
-
|
|
28281
|
-
|
|
28282
|
-
async forEachCap(capName, cb) {
|
|
28283
|
-
for (const proxy of this.filterByCap(capName)) await cb(proxy);
|
|
28428
|
+
/** Consume a punctuator token, erroring if the next token isn't it. */
|
|
28429
|
+
expectPunct(punct) {
|
|
28430
|
+
const tok = this.peek();
|
|
28431
|
+
if (tok.type !== "punct" || tok.punct !== punct) throw new ExpressionParseError(`expected '${punct}'`, tok.pos);
|
|
28432
|
+
this.pos += 1;
|
|
28284
28433
|
}
|
|
28285
|
-
|
|
28286
|
-
|
|
28287
|
-
|
|
28288
|
-
|
|
28289
|
-
|
|
28290
|
-
*
|
|
28291
|
-
* Example:
|
|
28292
|
-
*
|
|
28293
|
-
* const results = await sm.invokeCap('snapshot', 'getSnapshot', {}, { parallelism: 4 })
|
|
28294
|
-
* const failed = results.filter(r => !r.ok)
|
|
28295
|
-
*/
|
|
28296
|
-
async invokeCap(capName, methodName, args, opts = {}) {
|
|
28297
|
-
const targets = this.filterByCap(capName);
|
|
28298
|
-
const parallelism = Math.max(1, opts.parallelism ?? targets.length);
|
|
28299
|
-
const out = [];
|
|
28300
|
-
for (let i = 0; i < targets.length; i += parallelism) {
|
|
28301
|
-
const chunk = targets.slice(i, i + parallelism);
|
|
28302
|
-
const settled = await Promise.allSettled(chunk.map(async (proxy) => {
|
|
28303
|
-
const cap = proxy[capName];
|
|
28304
|
-
if (!cap || typeof cap[methodName] !== "function") throw new Error(`device '${proxy.deviceId}' does not expose '${capName}.${methodName}'`);
|
|
28305
|
-
return await cap[methodName](args);
|
|
28306
|
-
}));
|
|
28307
|
-
for (let j = 0; j < settled.length; j++) {
|
|
28308
|
-
const proxy = chunk[j];
|
|
28309
|
-
const r = settled[j];
|
|
28310
|
-
if (r.status === "fulfilled") out.push({
|
|
28311
|
-
deviceId: proxy.deviceId,
|
|
28312
|
-
ok: true,
|
|
28313
|
-
result: r.value
|
|
28314
|
-
});
|
|
28315
|
-
else out.push({
|
|
28316
|
-
deviceId: proxy.deviceId,
|
|
28317
|
-
ok: false,
|
|
28318
|
-
error: r.reason
|
|
28319
|
-
});
|
|
28320
|
-
}
|
|
28434
|
+
matchPunct(punct) {
|
|
28435
|
+
const tok = this.peek();
|
|
28436
|
+
if (tok.type === "punct" && tok.punct === punct) {
|
|
28437
|
+
this.pos += 1;
|
|
28438
|
+
return true;
|
|
28321
28439
|
}
|
|
28322
|
-
return
|
|
28440
|
+
return false;
|
|
28323
28441
|
}
|
|
28324
|
-
|
|
28325
|
-
|
|
28326
|
-
|
|
28327
|
-
*/
|
|
28328
|
-
summary() {
|
|
28329
|
-
const byCap = {};
|
|
28330
|
-
const byAddon = {};
|
|
28331
|
-
const byType = {};
|
|
28332
|
-
let online = 0;
|
|
28333
|
-
let offline = 0;
|
|
28334
|
-
for (const binding of this.bindings.values()) for (const entry of binding.entries) byCap[entry.capName] = (byCap[entry.capName] ?? 0) + 1;
|
|
28335
|
-
for (const info of this.devices.values()) {
|
|
28336
|
-
byAddon[info.addonId] = (byAddon[info.addonId] ?? 0) + 1;
|
|
28337
|
-
byType[info.type] = (byType[info.type] ?? 0) + 1;
|
|
28338
|
-
if (info.online) online++;
|
|
28339
|
-
else offline++;
|
|
28340
|
-
}
|
|
28341
|
-
return {
|
|
28342
|
-
totalDevices: this.bindings.size,
|
|
28343
|
-
online,
|
|
28344
|
-
offline,
|
|
28345
|
-
byCap,
|
|
28346
|
-
byAddon,
|
|
28347
|
-
byType,
|
|
28348
|
-
statedDevices: this.stateMirror.size
|
|
28349
|
-
};
|
|
28442
|
+
countNode() {
|
|
28443
|
+
this.nodeCount += 1;
|
|
28444
|
+
if (this.nodeCount > 256) throw new ExpressionParseError("expression too complex", this.peek().pos);
|
|
28350
28445
|
}
|
|
28351
|
-
|
|
28352
|
-
|
|
28353
|
-
|
|
28354
|
-
|
|
28355
|
-
|
|
28356
|
-
|
|
28357
|
-
|
|
28358
|
-
const info = this.devices.get(id) ?? null;
|
|
28359
|
-
const binding = this.bindings.get(id) ?? null;
|
|
28360
|
-
const state = {};
|
|
28361
|
-
const perCap = this.stateMirror.get(id);
|
|
28362
|
-
if (perCap) for (const [cap, slice] of perCap) state[cap] = { ...slice };
|
|
28446
|
+
parseTernary() {
|
|
28447
|
+
const test = this.parseBinary(1);
|
|
28448
|
+
if (this.matchPunct("?")) {
|
|
28449
|
+
const consequent = this.parseTernary();
|
|
28450
|
+
this.expectPunct(":");
|
|
28451
|
+
const alternate = this.parseTernary();
|
|
28452
|
+
this.countNode();
|
|
28363
28453
|
return {
|
|
28364
|
-
|
|
28365
|
-
|
|
28366
|
-
|
|
28367
|
-
|
|
28368
|
-
entries: binding.entries.map((e) => ({ ...e }))
|
|
28369
|
-
} : null,
|
|
28370
|
-
state
|
|
28454
|
+
kind: "conditional",
|
|
28455
|
+
test,
|
|
28456
|
+
consequent,
|
|
28457
|
+
alternate
|
|
28371
28458
|
};
|
|
28372
|
-
}
|
|
28373
|
-
|
|
28374
|
-
const out = [];
|
|
28375
|
-
for (const id of this.bindings.keys()) out.push(dumpOne(id));
|
|
28376
|
-
return out;
|
|
28377
|
-
}
|
|
28378
|
-
/**
|
|
28379
|
-
* Direct read-only access to the underlying state mirror. Use
|
|
28380
|
-
* sparingly — `getSystemState` returns a deep copy that's safer for
|
|
28381
|
-
* exploratory work; this avoids the clone cost when iterating
|
|
28382
|
-
* thousands of slices.
|
|
28383
|
-
*/
|
|
28384
|
-
getRawMirror() {
|
|
28385
|
-
return this.stateMirror;
|
|
28459
|
+
}
|
|
28460
|
+
return test;
|
|
28386
28461
|
}
|
|
28387
|
-
|
|
28388
|
-
|
|
28389
|
-
|
|
28390
|
-
|
|
28391
|
-
|
|
28392
|
-
|
|
28393
|
-
|
|
28394
|
-
const
|
|
28395
|
-
|
|
28396
|
-
|
|
28462
|
+
parseBinary(minPrec) {
|
|
28463
|
+
let left = this.parseUnary();
|
|
28464
|
+
for (;;) {
|
|
28465
|
+
const tok = this.peek();
|
|
28466
|
+
if (tok.type !== "punct") break;
|
|
28467
|
+
const prec = BINARY_PRECEDENCE[tok.punct];
|
|
28468
|
+
if (prec === void 0 || prec < minPrec) break;
|
|
28469
|
+
const op = tok.punct;
|
|
28470
|
+
this.pos += 1;
|
|
28471
|
+
const right = this.parseBinary(prec + 1);
|
|
28472
|
+
this.countNode();
|
|
28473
|
+
if (isLogicalOp(op)) left = {
|
|
28474
|
+
kind: "logical",
|
|
28475
|
+
op,
|
|
28476
|
+
left,
|
|
28477
|
+
right
|
|
28478
|
+
};
|
|
28479
|
+
else if (isBinaryOp(op)) left = {
|
|
28480
|
+
kind: "binary",
|
|
28481
|
+
op,
|
|
28482
|
+
left,
|
|
28483
|
+
right
|
|
28484
|
+
};
|
|
28485
|
+
else throw new ExpressionParseError(`unexpected operator '${op}'`, tok.pos);
|
|
28486
|
+
}
|
|
28487
|
+
return left;
|
|
28488
|
+
}
|
|
28489
|
+
parseUnary() {
|
|
28490
|
+
const tok = this.peek();
|
|
28491
|
+
if (tok.type === "punct" && (tok.punct === "!" || tok.punct === "-")) {
|
|
28492
|
+
const op = tok.punct;
|
|
28493
|
+
this.pos += 1;
|
|
28494
|
+
const operand = this.parseUnary();
|
|
28495
|
+
this.countNode();
|
|
28496
|
+
return {
|
|
28497
|
+
kind: "unary",
|
|
28498
|
+
op,
|
|
28499
|
+
operand
|
|
28500
|
+
};
|
|
28397
28501
|
}
|
|
28398
|
-
return
|
|
28502
|
+
return this.parsePrimary();
|
|
28399
28503
|
}
|
|
28400
|
-
|
|
28401
|
-
|
|
28402
|
-
|
|
28403
|
-
|
|
28404
|
-
|
|
28405
|
-
|
|
28406
|
-
|
|
28407
|
-
|
|
28408
|
-
|
|
28409
|
-
|
|
28410
|
-
|
|
28411
|
-
|
|
28412
|
-
|
|
28413
|
-
|
|
28414
|
-
|
|
28415
|
-
|
|
28416
|
-
|
|
28417
|
-
|
|
28418
|
-
|
|
28419
|
-
|
|
28420
|
-
|
|
28421
|
-
|
|
28422
|
-
|
|
28423
|
-
|
|
28424
|
-
|
|
28425
|
-
|
|
28426
|
-
|
|
28427
|
-
|
|
28428
|
-
|
|
28504
|
+
parsePrimary() {
|
|
28505
|
+
const tok = this.next();
|
|
28506
|
+
switch (tok.type) {
|
|
28507
|
+
case "number":
|
|
28508
|
+
this.countNode();
|
|
28509
|
+
return {
|
|
28510
|
+
kind: "literal",
|
|
28511
|
+
value: tok.value
|
|
28512
|
+
};
|
|
28513
|
+
case "string":
|
|
28514
|
+
this.countNode();
|
|
28515
|
+
return {
|
|
28516
|
+
kind: "literal",
|
|
28517
|
+
value: tok.value
|
|
28518
|
+
};
|
|
28519
|
+
case "keyword":
|
|
28520
|
+
this.countNode();
|
|
28521
|
+
return {
|
|
28522
|
+
kind: "literal",
|
|
28523
|
+
value: tok.keyword === "null" ? null : tok.keyword === "true"
|
|
28524
|
+
};
|
|
28525
|
+
case "identifier": {
|
|
28526
|
+
const nextTok = this.peek();
|
|
28527
|
+
if (nextTok.type === "punct" && nextTok.punct === "(") return this.parseCall(tok.name, tok.pos);
|
|
28528
|
+
this.identifiers.add(tok.name);
|
|
28529
|
+
this.countNode();
|
|
28530
|
+
return {
|
|
28531
|
+
kind: "identifier",
|
|
28532
|
+
name: tok.name
|
|
28533
|
+
};
|
|
28534
|
+
}
|
|
28535
|
+
case "punct":
|
|
28536
|
+
if (tok.punct === "(") {
|
|
28537
|
+
const inner = this.parseTernary();
|
|
28538
|
+
this.expectPunct(")");
|
|
28539
|
+
return inner;
|
|
28540
|
+
}
|
|
28541
|
+
throw new ExpressionParseError(`unexpected token '${tok.punct}'`, tok.pos);
|
|
28542
|
+
case "eof": throw new ExpressionParseError("unexpected end of expression", tok.pos);
|
|
28543
|
+
}
|
|
28429
28544
|
}
|
|
28430
|
-
|
|
28431
|
-
|
|
28432
|
-
|
|
28433
|
-
|
|
28434
|
-
|
|
28545
|
+
parseCall(callee, pos) {
|
|
28546
|
+
if (!EXPRESSION_BUILTIN_NAMES.has(callee)) throw new ExpressionParseError(`unknown function '${callee}'`, pos);
|
|
28547
|
+
this.expectPunct("(");
|
|
28548
|
+
const args = [];
|
|
28549
|
+
if (!this.matchPunct(")")) for (;;) {
|
|
28550
|
+
args.push(this.parseTernary());
|
|
28551
|
+
if (args.length > 16) throw new ExpressionParseError(`too many arguments to '${callee}'`, pos);
|
|
28552
|
+
if (this.matchPunct(",")) continue;
|
|
28553
|
+
this.expectPunct(")");
|
|
28554
|
+
break;
|
|
28435
28555
|
}
|
|
28436
|
-
|
|
28437
|
-
|
|
28438
|
-
|
|
28439
|
-
|
|
28440
|
-
|
|
28441
|
-
|
|
28442
|
-
}
|
|
28443
|
-
for (const cb of this.globalStateListeners) try {
|
|
28444
|
-
cb(deviceId, capName, slice);
|
|
28445
|
-
} catch {}
|
|
28446
|
-
const capSet = this.capListeners.get(capName);
|
|
28447
|
-
if (capSet) for (const cb of capSet) try {
|
|
28448
|
-
cb(deviceId, slice);
|
|
28449
|
-
} catch {}
|
|
28450
|
-
const devSet = this.deviceListeners.get(deviceId);
|
|
28451
|
-
if (devSet) for (const cb of devSet) try {
|
|
28452
|
-
cb(deviceId, capName, slice);
|
|
28453
|
-
} catch {}
|
|
28556
|
+
this.callees.add(callee);
|
|
28557
|
+
this.countNode();
|
|
28558
|
+
return {
|
|
28559
|
+
kind: "call",
|
|
28560
|
+
callee,
|
|
28561
|
+
args
|
|
28562
|
+
};
|
|
28454
28563
|
}
|
|
28455
|
-
|
|
28456
|
-
|
|
28457
|
-
|
|
28458
|
-
|
|
28459
|
-
|
|
28460
|
-
|
|
28461
|
-
|
|
28462
|
-
|
|
28564
|
+
};
|
|
28565
|
+
/** Tokenize + parse `source` into a validated `ParsedExpression`. Throws
|
|
28566
|
+
* `ExpressionParseError` on any lexical or grammatical failure. */
|
|
28567
|
+
function parseExpression(source) {
|
|
28568
|
+
return new Parser(tokenize(source)).parse();
|
|
28569
|
+
}
|
|
28570
|
+
//#endregion
|
|
28571
|
+
//#region src/expression/compile.ts
|
|
28572
|
+
/**
|
|
28573
|
+
* LRU compile cache for parsed expressions (spec §2.4 "parse once … LRU keyed
|
|
28574
|
+
* by expr"). The cache stores BOTH successes and failures (negative caching),
|
|
28575
|
+
* so a corrupt persisted string costs exactly one tokenize+parse total — not
|
|
28576
|
+
* one per read on a hot resolve path.
|
|
28577
|
+
*
|
|
28578
|
+
* The cache is a module-level singleton: entries are pure, content-addressed
|
|
28579
|
+
* ASTs keyed by the raw source string, so sharing one instance across all
|
|
28580
|
+
* callers is safe and maximises hit rate.
|
|
28581
|
+
*/
|
|
28582
|
+
var cache = /* @__PURE__ */ new Map();
|
|
28583
|
+
function getCached(source) {
|
|
28584
|
+
const hit = cache.get(source);
|
|
28585
|
+
if (hit !== void 0) {
|
|
28586
|
+
cache.delete(source);
|
|
28587
|
+
cache.set(source, hit);
|
|
28588
|
+
return hit;
|
|
28463
28589
|
}
|
|
28464
|
-
|
|
28465
|
-
|
|
28466
|
-
|
|
28467
|
-
|
|
28468
|
-
|
|
28469
|
-
}
|
|
28590
|
+
let result;
|
|
28591
|
+
try {
|
|
28592
|
+
result = {
|
|
28593
|
+
ok: true,
|
|
28594
|
+
parsed: parseExpression(source)
|
|
28595
|
+
};
|
|
28596
|
+
} catch (err) {
|
|
28597
|
+
result = {
|
|
28598
|
+
ok: false,
|
|
28599
|
+
error: err instanceof ExpressionParseError ? err.message : String(err)
|
|
28600
|
+
};
|
|
28470
28601
|
}
|
|
28471
|
-
|
|
28472
|
-
|
|
28473
|
-
|
|
28474
|
-
|
|
28475
|
-
const wasNew = !this.devices.has(deviceId);
|
|
28476
|
-
this.devices.set(deviceId, info);
|
|
28477
|
-
if (kind === "added" && wasNew) {
|
|
28478
|
-
await this.refreshBinding(deviceId);
|
|
28479
|
-
for (const cb of this.addedListeners) try {
|
|
28480
|
-
cb(deviceId, info);
|
|
28481
|
-
} catch {}
|
|
28482
|
-
}
|
|
28483
|
-
} catch {}
|
|
28602
|
+
cache.set(source, result);
|
|
28603
|
+
if (cache.size > 256) {
|
|
28604
|
+
const oldest = cache.keys().next().value;
|
|
28605
|
+
if (oldest !== void 0) cache.delete(oldest);
|
|
28484
28606
|
}
|
|
28485
|
-
|
|
28486
|
-
function inSet(value, set) {
|
|
28487
|
-
if (Array.isArray(set)) return set.includes(value);
|
|
28488
|
-
return value === set;
|
|
28607
|
+
return result;
|
|
28489
28608
|
}
|
|
28490
|
-
|
|
28491
|
-
|
|
28609
|
+
/** Compile `source` to a `ParsedExpression`, throwing `ExpressionParseError`
|
|
28610
|
+
* on failure. LRU/negative-cached. */
|
|
28611
|
+
function compileExpression(source) {
|
|
28612
|
+
const result = getCached(source);
|
|
28613
|
+
if (result.ok) return result.parsed;
|
|
28614
|
+
throw new ExpressionParseError(result.error);
|
|
28492
28615
|
}
|
|
28493
|
-
|
|
28494
|
-
|
|
28495
|
-
|
|
28496
|
-
|
|
28497
|
-
if ("contains" in match) return haystack.toLowerCase().includes(match.contains.toLowerCase());
|
|
28498
|
-
return false;
|
|
28616
|
+
/** Compile `source`, returning a discriminated result instead of throwing.
|
|
28617
|
+
* Used by read paths that must degrade rather than raise. LRU/negative-cached. */
|
|
28618
|
+
function compileExpressionSafe(source) {
|
|
28619
|
+
return getCached(source);
|
|
28499
28620
|
}
|
|
28500
28621
|
//#endregion
|
|
28501
|
-
//#region src/
|
|
28502
|
-
/** Access Zod v4 internal .def — not in public typings but stable at runtime */
|
|
28503
|
-
function zodDef(schema) {
|
|
28504
|
-
return schema.def;
|
|
28505
|
-
}
|
|
28506
|
-
/** Access internal properties on a Zod schema instance */
|
|
28507
|
-
function zodInternals(schema) {
|
|
28508
|
-
return schema;
|
|
28509
|
-
}
|
|
28622
|
+
//#region src/expression/evaluator.ts
|
|
28510
28623
|
/**
|
|
28511
|
-
*
|
|
28512
|
-
*
|
|
28513
|
-
* Each entry's Zod type is inspected to determine the correct ConfigField type:
|
|
28514
|
-
* - ZodString → 'text' (or 'password' when key contains "password"/"secret"/"token"/"apikey")
|
|
28515
|
-
* - ZodNumber → 'number' (extracts min/max/step from Zod v4 checks)
|
|
28516
|
-
* - ZodBoolean → 'boolean'
|
|
28517
|
-
* - ZodEnum → 'select' (options built from enum values)
|
|
28518
|
-
* - Anything else → 'text' fallback
|
|
28624
|
+
* Tree-walking evaluator for the safe expression mini-language.
|
|
28519
28625
|
*
|
|
28520
|
-
*
|
|
28521
|
-
*
|
|
28626
|
+
* SECURITY (spec §4 rule 2/5):
|
|
28627
|
+
* - The scope is an `Object.create(null)` copy of ONLY the caller's own
|
|
28628
|
+
* enumerable binding entries, so `name in scope` is a pure own-key check and
|
|
28629
|
+
* `constructor` / `__proto__` / `toString` are plain unknown identifiers.
|
|
28630
|
+
* - Performs ZERO I/O and never touches `globalThis` / `Date` / `Math`
|
|
28631
|
+
* directly — the only external calls are into the frozen builtin table.
|
|
28632
|
+
* - The grammar has no loops/recursion/lambdas, so a walk is O(nodeCount) by
|
|
28633
|
+
* construction; the step counter is defense-in-depth for a crafted max-size
|
|
28634
|
+
* AST. Nothing blocks: there are no timers, awaits or unbounded loops.
|
|
28522
28635
|
*/
|
|
28523
|
-
|
|
28524
|
-
|
|
28525
|
-
|
|
28526
|
-
|
|
28527
|
-
|
|
28528
|
-
|
|
28636
|
+
var EMPTY_HOOKS = Object.freeze({});
|
|
28637
|
+
/** Build a null-prototype scope from own-enumerable binding entries. Inherited
|
|
28638
|
+
* keys of the input (e.g. from a `{__proto__: {...}}` payload) are NOT copied,
|
|
28639
|
+
* so nothing smuggles in via the prototype chain. */
|
|
28640
|
+
function createExpressionScope(bindings) {
|
|
28641
|
+
const scope = Object.create(null);
|
|
28642
|
+
for (const key of Object.keys(bindings)) if (Object.prototype.hasOwnProperty.call(bindings, key)) scope[key] = bindings[key];
|
|
28643
|
+
return scope;
|
|
28529
28644
|
}
|
|
28530
|
-
function
|
|
28531
|
-
|
|
28532
|
-
const defaultValue = getZodDefault(schema);
|
|
28533
|
-
const base = {
|
|
28534
|
-
key,
|
|
28535
|
-
label: description ?? humanizeKey(key),
|
|
28536
|
-
description,
|
|
28537
|
-
default: defaultValue
|
|
28538
|
-
};
|
|
28539
|
-
if (inner instanceof z.ZodString) return buildStringField(key, base);
|
|
28540
|
-
if (inner instanceof z.ZodNumber) return buildNumberField(inner, base);
|
|
28541
|
-
if (inner instanceof z.ZodBoolean) return {
|
|
28542
|
-
...base,
|
|
28543
|
-
type: "boolean"
|
|
28544
|
-
};
|
|
28545
|
-
if (inner instanceof z.ZodEnum) return buildEnumField(inner, base);
|
|
28546
|
-
if (inner instanceof z.ZodArray || inner instanceof z.ZodObject) return {
|
|
28547
|
-
...base,
|
|
28548
|
-
type: "textarea",
|
|
28549
|
-
rows: 6,
|
|
28550
|
-
isJson: true
|
|
28551
|
-
};
|
|
28552
|
-
return {
|
|
28553
|
-
...base,
|
|
28554
|
-
type: "text"
|
|
28555
|
-
};
|
|
28645
|
+
function isFiniteNumber(value) {
|
|
28646
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
28556
28647
|
}
|
|
28557
|
-
|
|
28558
|
-
|
|
28559
|
-
|
|
28560
|
-
...base,
|
|
28561
|
-
type: "password",
|
|
28562
|
-
showToggle: true
|
|
28563
|
-
};
|
|
28564
|
-
return {
|
|
28565
|
-
...base,
|
|
28566
|
-
type: "text"
|
|
28567
|
-
};
|
|
28648
|
+
/** JS truthiness of a primitive value. */
|
|
28649
|
+
function truthy(value) {
|
|
28650
|
+
return Boolean(value);
|
|
28568
28651
|
}
|
|
28569
|
-
function
|
|
28570
|
-
|
|
28571
|
-
|
|
28572
|
-
const rawMax = anyInner.maxValue;
|
|
28573
|
-
const min = rawMin != null && isFinite(rawMin) ? rawMin : void 0;
|
|
28574
|
-
const max = rawMax != null && isFinite(rawMax) ? rawMax : void 0;
|
|
28575
|
-
const step = getMultipleOfStep(inner);
|
|
28576
|
-
return {
|
|
28577
|
-
...base,
|
|
28578
|
-
type: "number",
|
|
28579
|
-
...min !== void 0 ? { min } : {},
|
|
28580
|
-
...max !== void 0 ? { max } : {},
|
|
28581
|
-
...step !== void 0 ? { step } : {}
|
|
28582
|
-
};
|
|
28652
|
+
function requireFinite(value, context) {
|
|
28653
|
+
if (!Number.isFinite(value)) throw new ExpressionEvalError(`${context} produced a non-finite result`);
|
|
28654
|
+
return value;
|
|
28583
28655
|
}
|
|
28584
|
-
function
|
|
28585
|
-
|
|
28586
|
-
|
|
28656
|
+
function step(ctx) {
|
|
28657
|
+
ctx.steps += 1;
|
|
28658
|
+
if (ctx.steps > ctx.maxSteps) throw new ExpressionEvalError("expression evaluation step budget exceeded");
|
|
28587
28659
|
}
|
|
28588
|
-
function
|
|
28589
|
-
|
|
28590
|
-
|
|
28591
|
-
|
|
28592
|
-
|
|
28593
|
-
|
|
28594
|
-
|
|
28595
|
-
|
|
28596
|
-
|
|
28597
|
-
|
|
28660
|
+
function evalNode(node, ctx) {
|
|
28661
|
+
step(ctx);
|
|
28662
|
+
switch (node.kind) {
|
|
28663
|
+
case "literal": return node.value;
|
|
28664
|
+
case "identifier":
|
|
28665
|
+
if (!(node.name in ctx.scope)) throw new ExpressionEvalError(`unknown identifier: ${node.name}`);
|
|
28666
|
+
return ctx.scope[node.name];
|
|
28667
|
+
case "unary": return evalUnary(node.op, evalNode(node.operand, ctx));
|
|
28668
|
+
case "binary": return evalBinary(node.op, evalNode(node.left, ctx), evalNode(node.right, ctx));
|
|
28669
|
+
case "logical": {
|
|
28670
|
+
const left = evalNode(node.left, ctx);
|
|
28671
|
+
if (node.op === "&&") return truthy(left) ? evalNode(node.right, ctx) : left;
|
|
28672
|
+
return truthy(left) ? left : evalNode(node.right, ctx);
|
|
28673
|
+
}
|
|
28674
|
+
case "conditional": return truthy(evalNode(node.test, ctx)) ? evalNode(node.consequent, ctx) : evalNode(node.alternate, ctx);
|
|
28675
|
+
case "call": return evalCall(node.callee, node.args.map((a) => evalNode(a, ctx)), ctx.hooks);
|
|
28676
|
+
}
|
|
28598
28677
|
}
|
|
28599
|
-
function
|
|
28600
|
-
if (
|
|
28601
|
-
if (
|
|
28602
|
-
|
|
28603
|
-
return schema;
|
|
28678
|
+
function evalUnary(op, operand) {
|
|
28679
|
+
if (op === "!") return !truthy(operand);
|
|
28680
|
+
if (!isFiniteNumber(operand)) throw new ExpressionEvalError("unary \"-\" requires a finite number");
|
|
28681
|
+
return requireFinite(-operand, "unary \"-\"");
|
|
28604
28682
|
}
|
|
28605
|
-
function
|
|
28606
|
-
|
|
28683
|
+
function evalBinary(op, left, right) {
|
|
28684
|
+
switch (op) {
|
|
28685
|
+
case "==": return left === right;
|
|
28686
|
+
case "!=": return left !== right;
|
|
28687
|
+
case "+":
|
|
28688
|
+
if (typeof left === "string" && typeof right === "string") return left + right;
|
|
28689
|
+
if (isFiniteNumber(left) && isFiniteNumber(right)) return requireFinite(left + right, "\"+\"");
|
|
28690
|
+
throw new ExpressionEvalError("\"+\" requires two numbers or two strings");
|
|
28691
|
+
case "-":
|
|
28692
|
+
case "*":
|
|
28693
|
+
case "/":
|
|
28694
|
+
case "%":
|
|
28695
|
+
if (!isFiniteNumber(left) || !isFiniteNumber(right)) throw new ExpressionEvalError(`"${op}" requires two finite numbers`);
|
|
28696
|
+
return requireFinite(op === "-" ? left - right : op === "*" ? left * right : op === "/" ? left / right : left % right, `"${op}"`);
|
|
28697
|
+
case "<":
|
|
28698
|
+
case "<=":
|
|
28699
|
+
case ">":
|
|
28700
|
+
case ">=":
|
|
28701
|
+
if (isFiniteNumber(left) && isFiniteNumber(right)) return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
|
|
28702
|
+
if (typeof left === "string" && typeof right === "string") return op === "<" ? left < right : op === "<=" ? left <= right : op === ">" ? left > right : left >= right;
|
|
28703
|
+
throw new ExpressionEvalError(`"${op}" requires two numbers or two strings`);
|
|
28704
|
+
}
|
|
28607
28705
|
}
|
|
28608
|
-
function
|
|
28609
|
-
|
|
28706
|
+
function evalCall(callee, args, hooks) {
|
|
28707
|
+
if (!Object.prototype.hasOwnProperty.call(EXPRESSION_BUILTINS, callee)) throw new ExpressionEvalError(`unknown function: ${callee}`);
|
|
28708
|
+
const builtin = EXPRESSION_BUILTINS[callee];
|
|
28709
|
+
if (args.length < builtin.minArgs || args.length > builtin.maxArgs) throw new ExpressionEvalError(`${callee}: wrong number of arguments (${args.length})`);
|
|
28710
|
+
return builtin.apply(args, hooks);
|
|
28711
|
+
}
|
|
28712
|
+
/** Evaluate an AST node against a scope. Throws `ExpressionEvalError` on any
|
|
28713
|
+
* runtime failure (unknown identifier, type mismatch, non-finite result,
|
|
28714
|
+
* step-budget exhaustion). */
|
|
28715
|
+
function evaluateAst(node, scope, opts) {
|
|
28716
|
+
return evalNode(node, {
|
|
28717
|
+
scope,
|
|
28718
|
+
hooks: opts?.hooks ?? EMPTY_HOOKS,
|
|
28719
|
+
maxSteps: opts?.maxSteps ?? 4096,
|
|
28720
|
+
steps: 0
|
|
28721
|
+
});
|
|
28722
|
+
}
|
|
28723
|
+
//#endregion
|
|
28724
|
+
//#region src/expression/expression-source.ts
|
|
28725
|
+
/** The `now` epoch-ms binding is auto-injected into every evaluation and is a
|
|
28726
|
+
* reserved binding name (authors may not rebind it). */
|
|
28727
|
+
var EXPRESSION_INJECTED_NOW = "now";
|
|
28728
|
+
/**
|
|
28729
|
+
* Coerce an untrusted `getByPath` / mirror read to an `ExpressionValue`.
|
|
28730
|
+
* Non-primitive values (objects, arrays, `undefined`, functions, bigint,
|
|
28731
|
+
* symbol) and non-finite numbers become `undefined` so the caller can apply
|
|
28732
|
+
* its binding-miss policy (→ `null`). `null` itself is a valid value.
|
|
28733
|
+
*/
|
|
28734
|
+
function toExpressionValue(raw) {
|
|
28735
|
+
if (raw === null) return null;
|
|
28736
|
+
if (typeof raw === "string") return raw;
|
|
28737
|
+
if (typeof raw === "boolean") return raw;
|
|
28738
|
+
if (typeof raw === "number") return Number.isFinite(raw) ? raw : void 0;
|
|
28739
|
+
}
|
|
28740
|
+
/**
|
|
28741
|
+
* Author-time validation. Returns `null` when the source is valid, else a
|
|
28742
|
+
* human-readable error message. Checks: the expression compiles; binding count
|
|
28743
|
+
* is within `MAX_EXPRESSION_BINDINGS`; every binding name is a legal identifier,
|
|
28744
|
+
* is not reserved (`now`/keywords) and does not shadow a builtin; and every
|
|
28745
|
+
* FREE identifier of the AST is covered by a binding or the injected `now`.
|
|
28746
|
+
*/
|
|
28747
|
+
function validateExpressionSource(src) {
|
|
28748
|
+
const names = Object.keys(src.bindings);
|
|
28749
|
+
if (names.length > 32) return `too many bindings (${names.length} > 32)`;
|
|
28750
|
+
for (const name of names) {
|
|
28751
|
+
if (!EXPRESSION_IDENTIFIER_RE.test(name)) return `invalid binding name '${name}'`;
|
|
28752
|
+
if (RESERVED_BINDING_NAMES.has(name)) return `binding name '${name}' is reserved`;
|
|
28753
|
+
if (EXPRESSION_BUILTIN_NAMES.has(name)) return `binding name '${name}' shadows a builtin function`;
|
|
28754
|
+
}
|
|
28755
|
+
const compiled = compileExpressionSafe(src.expr);
|
|
28756
|
+
if (!compiled.ok) return compiled.error;
|
|
28757
|
+
const bound = new Set(names);
|
|
28758
|
+
for (const id of compiled.parsed.identifiers) {
|
|
28759
|
+
if (id === "now") continue;
|
|
28760
|
+
if (!bound.has(id)) return `expression references unbound identifier '${id}'`;
|
|
28761
|
+
}
|
|
28762
|
+
return null;
|
|
28763
|
+
}
|
|
28764
|
+
/**
|
|
28765
|
+
* Shared read-path evaluation. Builds a null-proto scope from `bindingValues`
|
|
28766
|
+
* plus the injected `now` (supplied by the caller for determinism and
|
|
28767
|
+
* testability), compiles via the LRU, and evaluates. Any failure (parse or
|
|
28768
|
+
* eval) returns `{ ok: false }` — the caller treats that as "skip this
|
|
28769
|
+
* derivation", never as a throw that takes the pass down.
|
|
28770
|
+
*/
|
|
28771
|
+
function evaluateExpressionSource(expr, bindingValues, now, opts) {
|
|
28772
|
+
const compiled = compileExpressionSafe(expr);
|
|
28773
|
+
if (!compiled.ok) return {
|
|
28774
|
+
ok: false,
|
|
28775
|
+
error: compiled.error
|
|
28776
|
+
};
|
|
28777
|
+
const scope = createExpressionScope({
|
|
28778
|
+
...bindingValues,
|
|
28779
|
+
["now"]: now
|
|
28780
|
+
});
|
|
28781
|
+
try {
|
|
28782
|
+
return {
|
|
28783
|
+
ok: true,
|
|
28784
|
+
value: evaluateAst(compiled.parsed.ast, scope, opts)
|
|
28785
|
+
};
|
|
28786
|
+
} catch (err) {
|
|
28787
|
+
return {
|
|
28788
|
+
ok: false,
|
|
28789
|
+
error: err instanceof ExpressionEvalError ? err.message : String(err)
|
|
28790
|
+
};
|
|
28791
|
+
}
|
|
28610
28792
|
}
|
|
28611
28793
|
//#endregion
|
|
28794
|
+
//#region src/expression/binding-source.ts
|
|
28795
|
+
/**
|
|
28796
|
+
* What an expression's named bindings READ from.
|
|
28797
|
+
*
|
|
28798
|
+
* Salvaged verbatim from the deleted device-link mechanism. Wiring's source
|
|
28799
|
+
* kinds were the one part of it worth keeping: addressing a device field by
|
|
28800
|
+
* re-sync-stable `stableId`, a per-device constant, and a sibling-accessory
|
|
28801
|
+
* read are the vocabulary any cross-device derivation needs, and they were
|
|
28802
|
+
* already correct. What wiring got wrong was the DESTINATION — a field on
|
|
28803
|
+
* somebody else's device, with no identity — not the source.
|
|
28804
|
+
*
|
|
28805
|
+
* These shapes are therefore kept, re-homed next to the engine that consumes
|
|
28806
|
+
* them, and are the binding type of a composition recipe (the source picker
|
|
28807
|
+
* stays `deviceManager.getWireableFields`). They deliberately do NOT nest: a
|
|
28808
|
+
* binding is a read, never another expression.
|
|
28809
|
+
*
|
|
28810
|
+
* Schemas are authoritative; every type is `z.infer` of one, so a wire shape and
|
|
28811
|
+
* a TypeScript shape cannot drift apart (`scripts/check-schema-type-twins.ts`).
|
|
28812
|
+
*/
|
|
28813
|
+
/** Read a sibling accessory's status field, addressed by the sibling's key.
|
|
28814
|
+
* `kind` is optional for wire compatibility — absent means `'field'`. */
|
|
28815
|
+
var ExpressionFieldBindingSchema = z.object({
|
|
28816
|
+
kind: z.literal("field").optional(),
|
|
28817
|
+
sourceKey: z.string(),
|
|
28818
|
+
cap: z.string(),
|
|
28819
|
+
fieldPath: z.string()
|
|
28820
|
+
});
|
|
28821
|
+
/** A constant. No device is read. */
|
|
28822
|
+
var ExpressionLiteralBindingSchema = z.object({
|
|
28823
|
+
kind: z.literal("literal"),
|
|
28824
|
+
value: z.union([
|
|
28825
|
+
z.string(),
|
|
28826
|
+
z.number(),
|
|
28827
|
+
z.boolean(),
|
|
28828
|
+
z.null()
|
|
28829
|
+
])
|
|
28830
|
+
});
|
|
28831
|
+
/** Read ANY device's status field, addressed by its re-sync-stable `stableId` —
|
|
28832
|
+
* never by numeric id, which a re-adoption reissues. */
|
|
28833
|
+
var ExpressionGlobalBindingSchema = z.object({
|
|
28834
|
+
kind: z.literal("global"),
|
|
28835
|
+
sourceStableId: z.string(),
|
|
28836
|
+
cap: z.string(),
|
|
28837
|
+
fieldPath: z.string()
|
|
28838
|
+
});
|
|
28839
|
+
var ExpressionBindingSourceSchema = z.union([
|
|
28840
|
+
ExpressionFieldBindingSchema,
|
|
28841
|
+
ExpressionLiteralBindingSchema,
|
|
28842
|
+
ExpressionGlobalBindingSchema
|
|
28843
|
+
]);
|
|
28844
|
+
/**
|
|
28845
|
+
* An expression plus the bindings its free identifiers resolve against.
|
|
28846
|
+
*
|
|
28847
|
+
* The `superRefine` runs the SAME author-time validation as
|
|
28848
|
+
* `validateExpressionSource` — compiles the expression, checks binding names,
|
|
28849
|
+
* checks identifier coverage — so every boundary that parses one
|
|
28850
|
+
* validates-at-write rather than discovering the problem at read time.
|
|
28851
|
+
* Compiles are LRU-cached, so repeated validation of the same string is a hit.
|
|
28852
|
+
*/
|
|
28853
|
+
var ExpressionSourceSchema = z.object({
|
|
28854
|
+
expr: z.string().min(1).max(MAX_EXPRESSION_SOURCE_LENGTH),
|
|
28855
|
+
bindings: z.record(z.string().regex(EXPRESSION_IDENTIFIER_RE), ExpressionBindingSourceSchema)
|
|
28856
|
+
}).superRefine((src, ctx) => {
|
|
28857
|
+
const err = validateExpressionSource(src);
|
|
28858
|
+
if (err !== null) ctx.addIssue({
|
|
28859
|
+
code: "custom",
|
|
28860
|
+
message: err,
|
|
28861
|
+
path: ["expr"]
|
|
28862
|
+
});
|
|
28863
|
+
});
|
|
28864
|
+
//#endregion
|
|
28612
28865
|
//#region src/generated/cap-status-types.ts
|
|
28613
28866
|
/**
|
|
28614
28867
|
* Runtime list of cap names with status. Used by the settings
|
|
@@ -30410,6 +30663,12 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
30410
30663
|
addonId: null,
|
|
30411
30664
|
access: "view"
|
|
30412
30665
|
},
|
|
30666
|
+
"coreBlocks.restart": {
|
|
30667
|
+
capName: "core-blocks",
|
|
30668
|
+
capScope: "system",
|
|
30669
|
+
addonId: null,
|
|
30670
|
+
access: "create"
|
|
30671
|
+
},
|
|
30413
30672
|
"coreBlocks.setEnabled": {
|
|
30414
30673
|
capName: "core-blocks",
|
|
30415
30674
|
capScope: "system",
|
|
@@ -31046,12 +31305,6 @@ var METHOD_ACCESS_MAP = Object.freeze({
|
|
|
31046
31305
|
addonId: null,
|
|
31047
31306
|
access: "create"
|
|
31048
31307
|
},
|
|
31049
|
-
"deviceManager.setDeviceLinks": {
|
|
31050
|
-
capName: "device-manager",
|
|
31051
|
-
capScope: "system",
|
|
31052
|
-
addonId: null,
|
|
31053
|
-
access: "create"
|
|
31054
|
-
},
|
|
31055
31308
|
"deviceManager.setDisabled": {
|
|
31056
31309
|
capName: "device-manager",
|
|
31057
31310
|
capScope: "system",
|
|
@@ -35464,6 +35717,7 @@ function createSystemProxy(api) {
|
|
|
35464
35717
|
update: (input) => dispatch("coreBlocks", "update", "mutation", input),
|
|
35465
35718
|
delete: (input) => dispatch("coreBlocks", "delete", "mutation", input),
|
|
35466
35719
|
setEnabled: (input) => dispatch("coreBlocks", "setEnabled", "mutation", input),
|
|
35720
|
+
restart: (input) => dispatch("coreBlocks", "restart", "mutation", input),
|
|
35467
35721
|
compile: (input) => dispatch("coreBlocks", "compile", "mutation", input),
|
|
35468
35722
|
getTypeDefs: (input) => dispatch("coreBlocks", "getTypeDefs", "query", input)
|
|
35469
35723
|
},
|
|
@@ -36387,6 +36641,441 @@ var TimelapseRuleSchema = TimelapseRuleInputSchema.extend({
|
|
|
36387
36641
|
updatedAt: z.number()
|
|
36388
36642
|
});
|
|
36389
36643
|
//#endregion
|
|
36644
|
+
//#region src/pipeline/detail-crop.ts
|
|
36645
|
+
/**
|
|
36646
|
+
* THE detail-crop convention — the single derivation of the rectangle a
|
|
36647
|
+
* detail/enrichment step (clip-embedding, face-detection, plate-detection…)
|
|
36648
|
+
* is fed.
|
|
36649
|
+
*
|
|
36650
|
+
* ## Why this is one module and not two constants
|
|
36651
|
+
*
|
|
36652
|
+
* `object-clip` is ONE vector index, and cosine similarity is only meaningful
|
|
36653
|
+
* between vectors produced from the same crop convention. Two encode paths
|
|
36654
|
+
* write into it — the live detail plane and the embedding rebuild — and they
|
|
36655
|
+
* used to derive their crops independently: `DETAIL_CROP_PADDING_RATIO = 0.15`
|
|
36656
|
+
* with no squaring on one side, `DEFAULT_CROP_MARGIN = 0.2` with squaring on
|
|
36657
|
+
* by default on the other. Every rebuild therefore poured a second, silently
|
|
36658
|
+
* incomparable feature space into the index it exists to keep consistent.
|
|
36659
|
+
*
|
|
36660
|
+
* So the rectangle is derived HERE, once, from ONE convention value. Both
|
|
36661
|
+
* paths now reach this function through `pipelineRunner.runDetailSubtree` —
|
|
36662
|
+
* the runner is the only process that cuts (see `detail-subtree.ts`), and the
|
|
36663
|
+
* convention is a cluster-global `pipeline-orchestrator` setting. There is
|
|
36664
|
+
* deliberately no per-node or per-device scope: a per-accelerator crop margin
|
|
36665
|
+
* would reintroduce the same split, merely relocated.
|
|
36666
|
+
*
|
|
36667
|
+
* ## The default IS the live convention
|
|
36668
|
+
*
|
|
36669
|
+
* {@link DEFAULT_DETAIL_CROP_CONVENTION} reproduces what the live path has
|
|
36670
|
+
* been storing (0.15, no squaring). Anything else would invalidate every
|
|
36671
|
+
* vector already in the index on the day it shipped. Changing the convention
|
|
36672
|
+
* is legitimate — that is what the operator knob is for — but it must be
|
|
36673
|
+
* followed by a rebuild, which is now guaranteed to produce crops from this
|
|
36674
|
+
* same function.
|
|
36675
|
+
*/
|
|
36676
|
+
/**
|
|
36677
|
+
* Store identity of the convention in `pipeline-orchestrator`'s GLOBAL
|
|
36678
|
+
* (cluster-wide) settings.
|
|
36679
|
+
*
|
|
36680
|
+
* These live here rather than in the orchestrator because the reader is a
|
|
36681
|
+
* different addon — the pipeline runner, over the hub-routed `addon-settings`
|
|
36682
|
+
* cap. Addons never import each other, so a key owned by the writer would have
|
|
36683
|
+
* to be hand-copied by the reader, and a hand-copied key is how a setting
|
|
36684
|
+
* silently stops arriving while both sides still look correct.
|
|
36685
|
+
*/
|
|
36686
|
+
var DETAIL_CROP_SECTION_ID = "detail-crop";
|
|
36687
|
+
var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
|
|
36688
|
+
var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
|
|
36689
|
+
/**
|
|
36690
|
+
* Operator-tunable crop convention. Single-valued and cluster-wide — see the
|
|
36691
|
+
* module docblock for why it cannot be scoped per node or per device.
|
|
36692
|
+
*/
|
|
36693
|
+
var DetailCropConventionSchema = z.object({
|
|
36694
|
+
/**
|
|
36695
|
+
* Fraction of the box's own size added on EACH side before cutting.
|
|
36696
|
+
*
|
|
36697
|
+
* CLIP is trained on natural images WITH surroundings; a pixel-tight crop
|
|
36698
|
+
* removes exactly the context it is strongest on (a dog cut to its outline
|
|
36699
|
+
* is a dark blob). The right value is an empirical question, which is why it
|
|
36700
|
+
* is a setting: 0 / 0.15 / 0.2 / 0.5 are the interesting points.
|
|
36701
|
+
*/
|
|
36702
|
+
paddingRatio: z.number().min(0).max(4),
|
|
36703
|
+
/**
|
|
36704
|
+
* Square the window (in PIXELS) before cutting.
|
|
36705
|
+
*
|
|
36706
|
+
* CLIP's input is square, so a tall bbox resized straight to NxN is squashed
|
|
36707
|
+
* — a standing person becomes a shape the model never saw. Squaring costs
|
|
36708
|
+
* extra background, which is context the model wants anyway. Off by default
|
|
36709
|
+
* because the live path has never squared and the stored index reflects that.
|
|
36710
|
+
*/
|
|
36711
|
+
square: z.boolean()
|
|
36712
|
+
});
|
|
36713
|
+
/**
|
|
36714
|
+
* The convention in force when nobody has configured one — byte-for-byte the
|
|
36715
|
+
* behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
|
|
36716
|
+
*/
|
|
36717
|
+
var DEFAULT_DETAIL_CROP_CONVENTION = {
|
|
36718
|
+
paddingRatio: .15,
|
|
36719
|
+
square: false
|
|
36720
|
+
};
|
|
36721
|
+
/**
|
|
36722
|
+
* Narrow a FLAT settings record to the convention.
|
|
36723
|
+
*
|
|
36724
|
+
* Per-FIELD fallback, deliberately: a junk padding must not also discard a
|
|
36725
|
+
* valid squaring choice. An absent or invalid value resolves to
|
|
36726
|
+
* {@link DEFAULT_DETAIL_CROP_CONVENTION} — the historical live behaviour —
|
|
36727
|
+
* rather than to a clamped number nobody chose, so a bad read can never
|
|
36728
|
+
* quietly change what the stored vectors mean.
|
|
36729
|
+
*/
|
|
36730
|
+
function readDetailCropConvention(config) {
|
|
36731
|
+
const paddingRatio = DetailCropConventionSchema.shape.paddingRatio.safeParse(config[DETAIL_CROP_PADDING_KEY]);
|
|
36732
|
+
const square = DetailCropConventionSchema.shape.square.safeParse(config[DETAIL_CROP_SQUARE_KEY]);
|
|
36733
|
+
return {
|
|
36734
|
+
paddingRatio: paddingRatio.success ? paddingRatio.data : DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio,
|
|
36735
|
+
square: square.success ? square.data : DEFAULT_DETAIL_CROP_CONVENTION.square
|
|
36736
|
+
};
|
|
36737
|
+
}
|
|
36738
|
+
function isHydratedField$1(entry) {
|
|
36739
|
+
return typeof entry === "object" && entry !== null && "key" in entry;
|
|
36740
|
+
}
|
|
36741
|
+
/**
|
|
36742
|
+
* Extract the convention from an `addon-settings.getGlobalSettings` payload.
|
|
36743
|
+
*
|
|
36744
|
+
* Walks EVERY section rather than looking inside {@link DETAIL_CROP_SECTION_ID}
|
|
36745
|
+
* alone: the keys are unique across the addon's schema, and a section rename
|
|
36746
|
+
* must not silently revert the whole cluster to the default. A `null` payload
|
|
36747
|
+
* (addon mid-boot) is the default convention.
|
|
36748
|
+
*/
|
|
36749
|
+
function pickDetailCropConvention(view) {
|
|
36750
|
+
if (view === null) return DEFAULT_DETAIL_CROP_CONVENTION;
|
|
36751
|
+
const flat = {};
|
|
36752
|
+
for (const section of view.sections) for (const entry of section.fields) {
|
|
36753
|
+
if (!isHydratedField$1(entry) || typeof entry.key !== "string") continue;
|
|
36754
|
+
if (entry.key === "detailCropPaddingRatio" || entry.key === "detailCropSquare") flat[entry.key] = entry.value;
|
|
36755
|
+
}
|
|
36756
|
+
return readDetailCropConvention(flat);
|
|
36757
|
+
}
|
|
36758
|
+
/** Slider bounds for the operator-facing padding knob (orchestrator settings UI). */
|
|
36759
|
+
var DETAIL_CROP_PADDING_FIELD = {
|
|
36760
|
+
min: 0,
|
|
36761
|
+
max: 1,
|
|
36762
|
+
step: .05,
|
|
36763
|
+
default: DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio
|
|
36764
|
+
};
|
|
36765
|
+
/**
|
|
36766
|
+
* Derive the crop rectangle for one parent detection.
|
|
36767
|
+
*
|
|
36768
|
+
* Order: pad by `paddingRatio` of the box's own size → optionally square in
|
|
36769
|
+
* pixel space around the padded centre → keep it inside the frame. Pure:
|
|
36770
|
+
* always returns a new rect and never mutates `bbox`.
|
|
36771
|
+
*
|
|
36772
|
+
* Edge handling differs by mode, on purpose:
|
|
36773
|
+
*
|
|
36774
|
+
* - **unsquared** — TRUNCATED at the frame border, byte-for-byte what the live
|
|
36775
|
+
* path has always done (`padAndClampFrameBbox`). A subject against the edge
|
|
36776
|
+
* gets a slightly smaller window. Changing this would silently reinterpret
|
|
36777
|
+
* every edge-touching vector already in the index.
|
|
36778
|
+
* - **squared** — SLID inward instead, because a truncated square is not
|
|
36779
|
+
* square and squaring exists precisely to preserve the aspect the model
|
|
36780
|
+
* sees. It only shrinks when the square is larger than the frame itself.
|
|
36781
|
+
*/
|
|
36782
|
+
function deriveDetailCropRect(bbox, frameWidth, frameHeight, convention) {
|
|
36783
|
+
const padX = convention.paddingRatio * bbox.w;
|
|
36784
|
+
const padY = convention.paddingRatio * bbox.h;
|
|
36785
|
+
const padded = {
|
|
36786
|
+
x: bbox.x - padX,
|
|
36787
|
+
y: bbox.y - padY,
|
|
36788
|
+
w: bbox.w + 2 * padX,
|
|
36789
|
+
h: bbox.h + 2 * padY
|
|
36790
|
+
};
|
|
36791
|
+
return convention.square ? slideInsideFrame(squareInPixels(padded, frameWidth, frameHeight), frameWidth, frameHeight) : truncateToFrame(padded, frameWidth, frameHeight);
|
|
36792
|
+
}
|
|
36793
|
+
/**
|
|
36794
|
+
* Grow the shorter side to the longer one around the window's centre, bounded
|
|
36795
|
+
* by the frame's shorter side — a square larger than the frame cannot exist,
|
|
36796
|
+
* and collapsing to the frame's short side is the most that does.
|
|
36797
|
+
*/
|
|
36798
|
+
function squareInPixels(rect, frameWidth, frameHeight) {
|
|
36799
|
+
const side = Math.min(Math.max(rect.w, rect.h), Math.min(frameWidth, frameHeight));
|
|
36800
|
+
const cx = rect.x + rect.w / 2;
|
|
36801
|
+
const cy = rect.y + rect.h / 2;
|
|
36802
|
+
return {
|
|
36803
|
+
x: cx - side / 2,
|
|
36804
|
+
y: cy - side / 2,
|
|
36805
|
+
w: side,
|
|
36806
|
+
h: side
|
|
36807
|
+
};
|
|
36808
|
+
}
|
|
36809
|
+
/**
|
|
36810
|
+
* Cut the window at the frame border — the pre-unification live behaviour,
|
|
36811
|
+
* preserved exactly so unsquared crops keep matching the stored index.
|
|
36812
|
+
*/
|
|
36813
|
+
function truncateToFrame(rect, frameWidth, frameHeight) {
|
|
36814
|
+
const x1 = Math.max(0, rect.x);
|
|
36815
|
+
const y1 = Math.max(0, rect.y);
|
|
36816
|
+
const x2 = Math.min(frameWidth, rect.x + rect.w);
|
|
36817
|
+
const y2 = Math.min(frameHeight, rect.y + rect.h);
|
|
36818
|
+
return {
|
|
36819
|
+
x: x1,
|
|
36820
|
+
y: y1,
|
|
36821
|
+
w: Math.max(0, x2 - x1),
|
|
36822
|
+
h: Math.max(0, y2 - y1)
|
|
36823
|
+
};
|
|
36824
|
+
}
|
|
36825
|
+
/**
|
|
36826
|
+
* Move the window inside the frame keeping its extent — used only for squared
|
|
36827
|
+
* windows, where truncating would destroy the squareness that is the point.
|
|
36828
|
+
*/
|
|
36829
|
+
function slideInsideFrame(rect, frameWidth, frameHeight) {
|
|
36830
|
+
const w = Math.max(0, Math.min(rect.w, frameWidth));
|
|
36831
|
+
const h = Math.max(0, Math.min(rect.h, frameHeight));
|
|
36832
|
+
return {
|
|
36833
|
+
x: Math.min(Math.max(0, rect.x), Math.max(0, frameWidth - w)),
|
|
36834
|
+
y: Math.min(Math.max(0, rect.y), Math.max(0, frameHeight - h)),
|
|
36835
|
+
w,
|
|
36836
|
+
h
|
|
36837
|
+
};
|
|
36838
|
+
}
|
|
36839
|
+
//#endregion
|
|
36840
|
+
//#region src/pipeline/native-lease.ts
|
|
36841
|
+
/**
|
|
36842
|
+
* THE native-frame **lease** knobs — TTL, RAM budget and demand window for the
|
|
36843
|
+
* decode worker's native-resolution frame retention.
|
|
36844
|
+
*
|
|
36845
|
+
* ## Why they live here and not in the addon that reads them
|
|
36846
|
+
*
|
|
36847
|
+
* The WRITER is `pipeline-orchestrator` (the cluster-wide settings authority);
|
|
36848
|
+
* the READER is a private child process of `addon-pipeline`'s pipeline-runner.
|
|
36849
|
+
* Addons never import each other, so a key owned by either side would have to
|
|
36850
|
+
* be hand-copied by the other — and a hand-copied key is how a setting silently
|
|
36851
|
+
* stops arriving while both sides still look correct. Same reasoning, same
|
|
36852
|
+
* placement as `detail-crop.ts` (D52's "one cluster-wide orchestrator setting").
|
|
36853
|
+
*
|
|
36854
|
+
* ## Why cluster-wide and not per-node
|
|
36855
|
+
*
|
|
36856
|
+
* The lease is a per-decode-worker RAM window. Its purpose — the late
|
|
36857
|
+
* cross-process native crop landing on a full-resolution frame rather than the
|
|
36858
|
+
* ≤640 detection fallback — is a property of the PIPELINE, not of a node's
|
|
36859
|
+
* hardware: a per-node TTL would mean the same camera produces different crop
|
|
36860
|
+
* quality depending on which node the balancer placed it on, and nobody could
|
|
36861
|
+
* tell that from the stored media. Node-level RAM pressure is already handled
|
|
36862
|
+
* by the per-session budget ceiling, which is itself one of these knobs.
|
|
36863
|
+
*
|
|
36864
|
+
* ## What each knob costs
|
|
36865
|
+
*
|
|
36866
|
+
* A retained frame is a full NATIVE-resolution copy in system RAM. With the
|
|
36867
|
+
* default pinned-RGB24 lease path (`CAMSTACK_SESSION_PINNED_RGB_CROP`, on):
|
|
36868
|
+
* 4K ≈ 24.9 MB/frame, 1080p ≈ 6.2 MB/frame. On the YUV420P path (flag off, and
|
|
36869
|
+
* for software-decoded sessions): 4K ≈ 12.4 MB, 1080p ≈ 3.1 MB. Worst-case
|
|
36870
|
+
* resident RAM for ONE busy camera ≈ frameBytes × deliveredFps × ttlSeconds,
|
|
36871
|
+
* clamped by the budget ceiling. See `docs/design/decode-path.md` → "Lease
|
|
36872
|
+
* admission" for what actually gets admitted.
|
|
36873
|
+
*/
|
|
36874
|
+
/**
|
|
36875
|
+
* Store identity of the lease knobs in `pipeline-orchestrator`'s GLOBAL
|
|
36876
|
+
* (cluster-wide) settings. Keys are unique across that addon's whole schema, so
|
|
36877
|
+
* the reader can walk every section instead of trusting the section id.
|
|
36878
|
+
*/
|
|
36879
|
+
var NATIVE_LEASE_SECTION_ID = "native-lease";
|
|
36880
|
+
var NATIVE_LEASE_TTL_KEY = "nativeLeaseTtlMs";
|
|
36881
|
+
var NATIVE_LEASE_BUDGET_KEY = "nativeLeaseBudgetMb";
|
|
36882
|
+
var NATIVE_LEASE_ACTIVITY_KEY = "nativeLeaseActivityMs";
|
|
36883
|
+
var NATIVE_LEASE_ADMISSION_KEY = "nativeLeaseAdmission";
|
|
36884
|
+
/**
|
|
36885
|
+
* WHICH delivered frames the decode worker retains a native copy of.
|
|
36886
|
+
*
|
|
36887
|
+
* - `all` — every frame the worker delivered to the runner. The shipped
|
|
36888
|
+
* behaviour, and the only correct one if something can ask for a crop of a
|
|
36889
|
+
* frame the runner never sent to inference.
|
|
36890
|
+
* - `inferred` — only the frames the runner ADMITTED to its detection queue.
|
|
36891
|
+
* A native-crop request always names a `frameId` that rode an inference
|
|
36892
|
+
* result, so that is the only set a request can name. How much it drops is
|
|
36893
|
+
* the two-plane governor's admit ratio and nothing else: measured at ~50% on
|
|
36894
|
+
* this cluster, not the ~80% the design sketch assumed, because the governor
|
|
36895
|
+
* was not throttling as hard as the sketch supposed. Read
|
|
36896
|
+
* `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
|
|
36897
|
+
* of you rather than quoting a number from here. The newest delivered frame is
|
|
36898
|
+
* croppable regardless — it is still the worker's reserved slot, not a lease —
|
|
36899
|
+
* which covers the one-frame race between a mark and the supersede that
|
|
36900
|
+
* consumes it.
|
|
36901
|
+
*/
|
|
36902
|
+
var NativeLeaseAdmissionSchema = z.enum(["all", "inferred"]);
|
|
36903
|
+
/**
|
|
36904
|
+
* Operator-tunable native-lease settings. Bounds are enforced HERE (not only in
|
|
36905
|
+
* the slider) because the value also travels to a forked child process, where a
|
|
36906
|
+
* junk number would silently become a 0-length or unbounded retention window.
|
|
36907
|
+
*/
|
|
36908
|
+
var NativeLeaseSettingsSchema = z.object({
|
|
36909
|
+
/**
|
|
36910
|
+
* How long a retained native frame is served before it counts as a miss.
|
|
36911
|
+
*
|
|
36912
|
+
* Must cover the FULL late-crop horizon: detection inference + the
|
|
36913
|
+
* cross-process inference-result hop to hub post-analysis + tracking + the
|
|
36914
|
+
* tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
|
|
36915
|
+
* outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
|
|
36916
|
+
* RAM per busy camera grows linearly with no measured hit-rate gain.
|
|
36917
|
+
*/
|
|
36918
|
+
ttlMs: z.number().int().min(250).max(1e4),
|
|
36919
|
+
/**
|
|
36920
|
+
* Hard per-decode-worker RAM ceiling for retained native frames, in MB.
|
|
36921
|
+
*
|
|
36922
|
+
* Intended as a SAFETY ceiling with the TTL as the effective cap — but check
|
|
36923
|
+
* which one is actually binding before reasoning from that. At the shipped
|
|
36924
|
+
* 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
|
|
36925
|
+
* at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
|
|
36926
|
+
* `leaseFrames` on the metrics line say which. When the ceiling binds, a
|
|
36927
|
+
* change that admits fewer frames buys retention WINDOW at constant RAM
|
|
36928
|
+
* rather than giving RAM back — lower this knob if RAM is what you wanted.
|
|
36929
|
+
* `0` DISABLES the lease entirely and falls the worker back to the tiny
|
|
36930
|
+
* leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
|
|
36931
|
+
* to replace).
|
|
36932
|
+
*/
|
|
36933
|
+
budgetMb: z.number().int().min(0).max(4096),
|
|
36934
|
+
/**
|
|
36935
|
+
* Demand window: eager per-frame native retention runs only within this many
|
|
36936
|
+
* ms of the last native-crop request (or of the dial starting).
|
|
36937
|
+
*
|
|
36938
|
+
* `0` means ALWAYS ON — it disables the gate, it does not disable retention.
|
|
36939
|
+
* That is the legacy behaviour that saturated an N100 (24 native-4K downloads
|
|
36940
|
+
* per second on a camera with zero crop demand), so leave it non-zero unless
|
|
36941
|
+
* you are reproducing that.
|
|
36942
|
+
*/
|
|
36943
|
+
activityMs: z.number().int().min(0).max(12e4),
|
|
36944
|
+
/**
|
|
36945
|
+
* Which delivered frames are retained at all — see
|
|
36946
|
+
* {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
|
|
36947
|
+
* changes WHAT is kept rather than for how long, so it is also the only one
|
|
36948
|
+
* that can turn a crop that used to hit into a miss. The worker counts every
|
|
36949
|
+
* crop request naming a frame it did NOT see marked
|
|
36950
|
+
* (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
|
|
36951
|
+
* there is the signal that some caller names frames outside the inference set
|
|
36952
|
+
* and that this must go back to `all`.
|
|
36953
|
+
*/
|
|
36954
|
+
admission: NativeLeaseAdmissionSchema
|
|
36955
|
+
});
|
|
36956
|
+
/**
|
|
36957
|
+
* The values in force when the operator has set nothing — byte-for-byte the
|
|
36958
|
+
* constants the decode worker shipped with as env-var defaults, so making these
|
|
36959
|
+
* settings changed no behaviour on the day it landed.
|
|
36960
|
+
*/
|
|
36961
|
+
var DEFAULT_NATIVE_LEASE_SETTINGS = {
|
|
36962
|
+
ttlMs: 1200,
|
|
36963
|
+
budgetMb: 1024,
|
|
36964
|
+
activityMs: 15e3,
|
|
36965
|
+
admission: "inferred"
|
|
36966
|
+
};
|
|
36967
|
+
/** Slider bounds for the operator-facing knobs (orchestrator settings UI). */
|
|
36968
|
+
var NATIVE_LEASE_TTL_FIELD = {
|
|
36969
|
+
min: 250,
|
|
36970
|
+
max: 1e4,
|
|
36971
|
+
step: 50,
|
|
36972
|
+
default: DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs
|
|
36973
|
+
};
|
|
36974
|
+
var NATIVE_LEASE_BUDGET_FIELD = {
|
|
36975
|
+
min: 0,
|
|
36976
|
+
max: 4096,
|
|
36977
|
+
step: 64,
|
|
36978
|
+
default: DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb
|
|
36979
|
+
};
|
|
36980
|
+
var NATIVE_LEASE_ACTIVITY_FIELD = {
|
|
36981
|
+
min: 0,
|
|
36982
|
+
max: 12e4,
|
|
36983
|
+
step: 1e3,
|
|
36984
|
+
default: DEFAULT_NATIVE_LEASE_SETTINGS.activityMs
|
|
36985
|
+
};
|
|
36986
|
+
/** Select options for the admission knob (orchestrator settings UI). */
|
|
36987
|
+
var NATIVE_LEASE_ADMISSION_FIELD = {
|
|
36988
|
+
options: [{
|
|
36989
|
+
value: "all",
|
|
36990
|
+
label: "Every delivered frame"
|
|
36991
|
+
}, {
|
|
36992
|
+
value: "inferred",
|
|
36993
|
+
label: "Only frames sent to inference"
|
|
36994
|
+
}],
|
|
36995
|
+
default: DEFAULT_NATIVE_LEASE_SETTINGS.admission
|
|
36996
|
+
};
|
|
36997
|
+
/**
|
|
36998
|
+
* Parse one knob, reporting `null` for absent, junk, out-of-bounds — AND for a
|
|
36999
|
+
* value equal to the shipped default.
|
|
37000
|
+
*
|
|
37001
|
+
* That last rule is not tidiness, it is the difference between the documented
|
|
37002
|
+
* precedence being true and being a lie. `addon-settings.getGlobalSettings`
|
|
37003
|
+
* returns a HYDRATED payload, and `hydrateField` fills an unstored field with
|
|
37004
|
+
* the schema's own `default` (verified live on the hub: a cluster that has never
|
|
37005
|
+
* opened the form still reports `nativeLeaseTtlMs = 1200`). A reader that took
|
|
37006
|
+
* that at face value would report all three knobs as "set" on every cluster on
|
|
37007
|
+
* the day this shipped, permanently retiring the `CAMSTACK_SESSION_NATIVE_LEASE_*`
|
|
37008
|
+
* emergency override that the precedence promises. There is no raw-store read on
|
|
37009
|
+
* this cap to distinguish the two, so the default value itself is treated as
|
|
37010
|
+
* "the operator has expressed no preference" — which is also what leaving a
|
|
37011
|
+
* slider untouched means.
|
|
37012
|
+
*
|
|
37013
|
+
* The cost is one honest edge: an operator who deliberately selects the default
|
|
37014
|
+
* value in order to overrule an env var does not get it. Clear the env var
|
|
37015
|
+
* instead; the worker's spawn line names the source, so this is visible rather
|
|
37016
|
+
* than mysterious.
|
|
37017
|
+
*/
|
|
37018
|
+
function readKnob(knob, raw) {
|
|
37019
|
+
const parsed = NativeLeaseSettingsSchema.shape[knob].safeParse(raw);
|
|
37020
|
+
if (!parsed.success) return null;
|
|
37021
|
+
return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS[knob] ? null : parsed.data;
|
|
37022
|
+
}
|
|
37023
|
+
/** {@link readKnob} for the one non-numeric knob. Same default-means-unset rule. */
|
|
37024
|
+
function readAdmissionKnob(raw) {
|
|
37025
|
+
const parsed = NativeLeaseAdmissionSchema.safeParse(raw);
|
|
37026
|
+
if (!parsed.success) return null;
|
|
37027
|
+
return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS.admission ? null : parsed.data;
|
|
37028
|
+
}
|
|
37029
|
+
/**
|
|
37030
|
+
* Narrow a FLAT settings record to the knobs the operator set.
|
|
37031
|
+
*
|
|
37032
|
+
* Per-FIELD parse, deliberately: a junk TTL must not also discard a valid
|
|
37033
|
+
* budget. An absent, out-of-bounds or default-valued knob is OMITTED (not
|
|
37034
|
+
* clamped, not defaulted) so the caller can still fall through to the env
|
|
37035
|
+
* override — clamping here would turn a typo into a value nobody chose. See
|
|
37036
|
+
* {@link readKnob} for why the default counts as unset.
|
|
37037
|
+
*/
|
|
37038
|
+
function readNativeLeaseOverride(config) {
|
|
37039
|
+
const ttlMs = readKnob("ttlMs", config[NATIVE_LEASE_TTL_KEY]);
|
|
37040
|
+
const budgetMb = readKnob("budgetMb", config[NATIVE_LEASE_BUDGET_KEY]);
|
|
37041
|
+
const activityMs = readKnob("activityMs", config[NATIVE_LEASE_ACTIVITY_KEY]);
|
|
37042
|
+
const admission = readAdmissionKnob(config[NATIVE_LEASE_ADMISSION_KEY]);
|
|
37043
|
+
return {
|
|
37044
|
+
...ttlMs === null ? {} : { ttlMs },
|
|
37045
|
+
...budgetMb === null ? {} : { budgetMb },
|
|
37046
|
+
...activityMs === null ? {} : { activityMs },
|
|
37047
|
+
...admission === null ? {} : { admission }
|
|
37048
|
+
};
|
|
37049
|
+
}
|
|
37050
|
+
function isHydratedField(entry) {
|
|
37051
|
+
return typeof entry === "object" && entry !== null && "key" in entry;
|
|
37052
|
+
}
|
|
37053
|
+
var LEASE_KEYS = [
|
|
37054
|
+
NATIVE_LEASE_TTL_KEY,
|
|
37055
|
+
NATIVE_LEASE_BUDGET_KEY,
|
|
37056
|
+
NATIVE_LEASE_ACTIVITY_KEY,
|
|
37057
|
+
NATIVE_LEASE_ADMISSION_KEY
|
|
37058
|
+
];
|
|
37059
|
+
/**
|
|
37060
|
+
* Extract the operator's lease overrides from an
|
|
37061
|
+
* `addon-settings.getGlobalSettings` payload.
|
|
37062
|
+
*
|
|
37063
|
+
* Walks EVERY section rather than looking inside {@link NATIVE_LEASE_SECTION_ID}
|
|
37064
|
+
* alone: the keys are unique across the addon's schema, and a section rename
|
|
37065
|
+
* must not silently revert the whole cluster to the defaults. A `null` payload
|
|
37066
|
+
* (addon mid-boot) means "operator set nothing" — the env/default fallback then
|
|
37067
|
+
* applies, which is the correct read of "I could not ask".
|
|
37068
|
+
*/
|
|
37069
|
+
function pickNativeLeaseOverride(view) {
|
|
37070
|
+
if (view === null) return {};
|
|
37071
|
+
const flat = {};
|
|
37072
|
+
for (const section of view.sections) for (const entry of section.fields) {
|
|
37073
|
+
if (!isHydratedField(entry) || typeof entry.key !== "string") continue;
|
|
37074
|
+
if (LEASE_KEYS.includes(entry.key)) flat[entry.key] = entry.value;
|
|
37075
|
+
}
|
|
37076
|
+
return readNativeLeaseOverride(flat);
|
|
37077
|
+
}
|
|
37078
|
+
//#endregion
|
|
36390
37079
|
//#region src/types/device-type.ts
|
|
36391
37080
|
var DEVICE_TYPE_INFO = { ["camera"]: {
|
|
36392
37081
|
type: "camera",
|
|
@@ -37446,441 +38135,6 @@ function pointInPolygon(point, polygon) {
|
|
|
37446
38135
|
return inside;
|
|
37447
38136
|
}
|
|
37448
38137
|
//#endregion
|
|
37449
|
-
//#region src/pipeline/detail-crop.ts
|
|
37450
|
-
/**
|
|
37451
|
-
* THE detail-crop convention — the single derivation of the rectangle a
|
|
37452
|
-
* detail/enrichment step (clip-embedding, face-detection, plate-detection…)
|
|
37453
|
-
* is fed.
|
|
37454
|
-
*
|
|
37455
|
-
* ## Why this is one module and not two constants
|
|
37456
|
-
*
|
|
37457
|
-
* `object-clip` is ONE vector index, and cosine similarity is only meaningful
|
|
37458
|
-
* between vectors produced from the same crop convention. Two encode paths
|
|
37459
|
-
* write into it — the live detail plane and the embedding rebuild — and they
|
|
37460
|
-
* used to derive their crops independently: `DETAIL_CROP_PADDING_RATIO = 0.15`
|
|
37461
|
-
* with no squaring on one side, `DEFAULT_CROP_MARGIN = 0.2` with squaring on
|
|
37462
|
-
* by default on the other. Every rebuild therefore poured a second, silently
|
|
37463
|
-
* incomparable feature space into the index it exists to keep consistent.
|
|
37464
|
-
*
|
|
37465
|
-
* So the rectangle is derived HERE, once, from ONE convention value. Both
|
|
37466
|
-
* paths now reach this function through `pipelineRunner.runDetailSubtree` —
|
|
37467
|
-
* the runner is the only process that cuts (see `detail-subtree.ts`), and the
|
|
37468
|
-
* convention is a cluster-global `pipeline-orchestrator` setting. There is
|
|
37469
|
-
* deliberately no per-node or per-device scope: a per-accelerator crop margin
|
|
37470
|
-
* would reintroduce the same split, merely relocated.
|
|
37471
|
-
*
|
|
37472
|
-
* ## The default IS the live convention
|
|
37473
|
-
*
|
|
37474
|
-
* {@link DEFAULT_DETAIL_CROP_CONVENTION} reproduces what the live path has
|
|
37475
|
-
* been storing (0.15, no squaring). Anything else would invalidate every
|
|
37476
|
-
* vector already in the index on the day it shipped. Changing the convention
|
|
37477
|
-
* is legitimate — that is what the operator knob is for — but it must be
|
|
37478
|
-
* followed by a rebuild, which is now guaranteed to produce crops from this
|
|
37479
|
-
* same function.
|
|
37480
|
-
*/
|
|
37481
|
-
/**
|
|
37482
|
-
* Store identity of the convention in `pipeline-orchestrator`'s GLOBAL
|
|
37483
|
-
* (cluster-wide) settings.
|
|
37484
|
-
*
|
|
37485
|
-
* These live here rather than in the orchestrator because the reader is a
|
|
37486
|
-
* different addon — the pipeline runner, over the hub-routed `addon-settings`
|
|
37487
|
-
* cap. Addons never import each other, so a key owned by the writer would have
|
|
37488
|
-
* to be hand-copied by the reader, and a hand-copied key is how a setting
|
|
37489
|
-
* silently stops arriving while both sides still look correct.
|
|
37490
|
-
*/
|
|
37491
|
-
var DETAIL_CROP_SECTION_ID = "detail-crop";
|
|
37492
|
-
var DETAIL_CROP_PADDING_KEY = "detailCropPaddingRatio";
|
|
37493
|
-
var DETAIL_CROP_SQUARE_KEY = "detailCropSquare";
|
|
37494
|
-
/**
|
|
37495
|
-
* Operator-tunable crop convention. Single-valued and cluster-wide — see the
|
|
37496
|
-
* module docblock for why it cannot be scoped per node or per device.
|
|
37497
|
-
*/
|
|
37498
|
-
var DetailCropConventionSchema = z.object({
|
|
37499
|
-
/**
|
|
37500
|
-
* Fraction of the box's own size added on EACH side before cutting.
|
|
37501
|
-
*
|
|
37502
|
-
* CLIP is trained on natural images WITH surroundings; a pixel-tight crop
|
|
37503
|
-
* removes exactly the context it is strongest on (a dog cut to its outline
|
|
37504
|
-
* is a dark blob). The right value is an empirical question, which is why it
|
|
37505
|
-
* is a setting: 0 / 0.15 / 0.2 / 0.5 are the interesting points.
|
|
37506
|
-
*/
|
|
37507
|
-
paddingRatio: z.number().min(0).max(4),
|
|
37508
|
-
/**
|
|
37509
|
-
* Square the window (in PIXELS) before cutting.
|
|
37510
|
-
*
|
|
37511
|
-
* CLIP's input is square, so a tall bbox resized straight to NxN is squashed
|
|
37512
|
-
* — a standing person becomes a shape the model never saw. Squaring costs
|
|
37513
|
-
* extra background, which is context the model wants anyway. Off by default
|
|
37514
|
-
* because the live path has never squared and the stored index reflects that.
|
|
37515
|
-
*/
|
|
37516
|
-
square: z.boolean()
|
|
37517
|
-
});
|
|
37518
|
-
/**
|
|
37519
|
-
* The convention in force when nobody has configured one — byte-for-byte the
|
|
37520
|
-
* behaviour of the pre-unification LIVE path (`DETAIL_CROP_PADDING_RATIO`).
|
|
37521
|
-
*/
|
|
37522
|
-
var DEFAULT_DETAIL_CROP_CONVENTION = {
|
|
37523
|
-
paddingRatio: .15,
|
|
37524
|
-
square: false
|
|
37525
|
-
};
|
|
37526
|
-
/**
|
|
37527
|
-
* Narrow a FLAT settings record to the convention.
|
|
37528
|
-
*
|
|
37529
|
-
* Per-FIELD fallback, deliberately: a junk padding must not also discard a
|
|
37530
|
-
* valid squaring choice. An absent or invalid value resolves to
|
|
37531
|
-
* {@link DEFAULT_DETAIL_CROP_CONVENTION} — the historical live behaviour —
|
|
37532
|
-
* rather than to a clamped number nobody chose, so a bad read can never
|
|
37533
|
-
* quietly change what the stored vectors mean.
|
|
37534
|
-
*/
|
|
37535
|
-
function readDetailCropConvention(config) {
|
|
37536
|
-
const paddingRatio = DetailCropConventionSchema.shape.paddingRatio.safeParse(config[DETAIL_CROP_PADDING_KEY]);
|
|
37537
|
-
const square = DetailCropConventionSchema.shape.square.safeParse(config[DETAIL_CROP_SQUARE_KEY]);
|
|
37538
|
-
return {
|
|
37539
|
-
paddingRatio: paddingRatio.success ? paddingRatio.data : DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio,
|
|
37540
|
-
square: square.success ? square.data : DEFAULT_DETAIL_CROP_CONVENTION.square
|
|
37541
|
-
};
|
|
37542
|
-
}
|
|
37543
|
-
function isHydratedField$1(entry) {
|
|
37544
|
-
return typeof entry === "object" && entry !== null && "key" in entry;
|
|
37545
|
-
}
|
|
37546
|
-
/**
|
|
37547
|
-
* Extract the convention from an `addon-settings.getGlobalSettings` payload.
|
|
37548
|
-
*
|
|
37549
|
-
* Walks EVERY section rather than looking inside {@link DETAIL_CROP_SECTION_ID}
|
|
37550
|
-
* alone: the keys are unique across the addon's schema, and a section rename
|
|
37551
|
-
* must not silently revert the whole cluster to the default. A `null` payload
|
|
37552
|
-
* (addon mid-boot) is the default convention.
|
|
37553
|
-
*/
|
|
37554
|
-
function pickDetailCropConvention(view) {
|
|
37555
|
-
if (view === null) return DEFAULT_DETAIL_CROP_CONVENTION;
|
|
37556
|
-
const flat = {};
|
|
37557
|
-
for (const section of view.sections) for (const entry of section.fields) {
|
|
37558
|
-
if (!isHydratedField$1(entry) || typeof entry.key !== "string") continue;
|
|
37559
|
-
if (entry.key === "detailCropPaddingRatio" || entry.key === "detailCropSquare") flat[entry.key] = entry.value;
|
|
37560
|
-
}
|
|
37561
|
-
return readDetailCropConvention(flat);
|
|
37562
|
-
}
|
|
37563
|
-
/** Slider bounds for the operator-facing padding knob (orchestrator settings UI). */
|
|
37564
|
-
var DETAIL_CROP_PADDING_FIELD = {
|
|
37565
|
-
min: 0,
|
|
37566
|
-
max: 1,
|
|
37567
|
-
step: .05,
|
|
37568
|
-
default: DEFAULT_DETAIL_CROP_CONVENTION.paddingRatio
|
|
37569
|
-
};
|
|
37570
|
-
/**
|
|
37571
|
-
* Derive the crop rectangle for one parent detection.
|
|
37572
|
-
*
|
|
37573
|
-
* Order: pad by `paddingRatio` of the box's own size → optionally square in
|
|
37574
|
-
* pixel space around the padded centre → keep it inside the frame. Pure:
|
|
37575
|
-
* always returns a new rect and never mutates `bbox`.
|
|
37576
|
-
*
|
|
37577
|
-
* Edge handling differs by mode, on purpose:
|
|
37578
|
-
*
|
|
37579
|
-
* - **unsquared** — TRUNCATED at the frame border, byte-for-byte what the live
|
|
37580
|
-
* path has always done (`padAndClampFrameBbox`). A subject against the edge
|
|
37581
|
-
* gets a slightly smaller window. Changing this would silently reinterpret
|
|
37582
|
-
* every edge-touching vector already in the index.
|
|
37583
|
-
* - **squared** — SLID inward instead, because a truncated square is not
|
|
37584
|
-
* square and squaring exists precisely to preserve the aspect the model
|
|
37585
|
-
* sees. It only shrinks when the square is larger than the frame itself.
|
|
37586
|
-
*/
|
|
37587
|
-
function deriveDetailCropRect(bbox, frameWidth, frameHeight, convention) {
|
|
37588
|
-
const padX = convention.paddingRatio * bbox.w;
|
|
37589
|
-
const padY = convention.paddingRatio * bbox.h;
|
|
37590
|
-
const padded = {
|
|
37591
|
-
x: bbox.x - padX,
|
|
37592
|
-
y: bbox.y - padY,
|
|
37593
|
-
w: bbox.w + 2 * padX,
|
|
37594
|
-
h: bbox.h + 2 * padY
|
|
37595
|
-
};
|
|
37596
|
-
return convention.square ? slideInsideFrame(squareInPixels(padded, frameWidth, frameHeight), frameWidth, frameHeight) : truncateToFrame(padded, frameWidth, frameHeight);
|
|
37597
|
-
}
|
|
37598
|
-
/**
|
|
37599
|
-
* Grow the shorter side to the longer one around the window's centre, bounded
|
|
37600
|
-
* by the frame's shorter side — a square larger than the frame cannot exist,
|
|
37601
|
-
* and collapsing to the frame's short side is the most that does.
|
|
37602
|
-
*/
|
|
37603
|
-
function squareInPixels(rect, frameWidth, frameHeight) {
|
|
37604
|
-
const side = Math.min(Math.max(rect.w, rect.h), Math.min(frameWidth, frameHeight));
|
|
37605
|
-
const cx = rect.x + rect.w / 2;
|
|
37606
|
-
const cy = rect.y + rect.h / 2;
|
|
37607
|
-
return {
|
|
37608
|
-
x: cx - side / 2,
|
|
37609
|
-
y: cy - side / 2,
|
|
37610
|
-
w: side,
|
|
37611
|
-
h: side
|
|
37612
|
-
};
|
|
37613
|
-
}
|
|
37614
|
-
/**
|
|
37615
|
-
* Cut the window at the frame border — the pre-unification live behaviour,
|
|
37616
|
-
* preserved exactly so unsquared crops keep matching the stored index.
|
|
37617
|
-
*/
|
|
37618
|
-
function truncateToFrame(rect, frameWidth, frameHeight) {
|
|
37619
|
-
const x1 = Math.max(0, rect.x);
|
|
37620
|
-
const y1 = Math.max(0, rect.y);
|
|
37621
|
-
const x2 = Math.min(frameWidth, rect.x + rect.w);
|
|
37622
|
-
const y2 = Math.min(frameHeight, rect.y + rect.h);
|
|
37623
|
-
return {
|
|
37624
|
-
x: x1,
|
|
37625
|
-
y: y1,
|
|
37626
|
-
w: Math.max(0, x2 - x1),
|
|
37627
|
-
h: Math.max(0, y2 - y1)
|
|
37628
|
-
};
|
|
37629
|
-
}
|
|
37630
|
-
/**
|
|
37631
|
-
* Move the window inside the frame keeping its extent — used only for squared
|
|
37632
|
-
* windows, where truncating would destroy the squareness that is the point.
|
|
37633
|
-
*/
|
|
37634
|
-
function slideInsideFrame(rect, frameWidth, frameHeight) {
|
|
37635
|
-
const w = Math.max(0, Math.min(rect.w, frameWidth));
|
|
37636
|
-
const h = Math.max(0, Math.min(rect.h, frameHeight));
|
|
37637
|
-
return {
|
|
37638
|
-
x: Math.min(Math.max(0, rect.x), Math.max(0, frameWidth - w)),
|
|
37639
|
-
y: Math.min(Math.max(0, rect.y), Math.max(0, frameHeight - h)),
|
|
37640
|
-
w,
|
|
37641
|
-
h
|
|
37642
|
-
};
|
|
37643
|
-
}
|
|
37644
|
-
//#endregion
|
|
37645
|
-
//#region src/pipeline/native-lease.ts
|
|
37646
|
-
/**
|
|
37647
|
-
* THE native-frame **lease** knobs — TTL, RAM budget and demand window for the
|
|
37648
|
-
* decode worker's native-resolution frame retention.
|
|
37649
|
-
*
|
|
37650
|
-
* ## Why they live here and not in the addon that reads them
|
|
37651
|
-
*
|
|
37652
|
-
* The WRITER is `pipeline-orchestrator` (the cluster-wide settings authority);
|
|
37653
|
-
* the READER is a private child process of `addon-pipeline`'s pipeline-runner.
|
|
37654
|
-
* Addons never import each other, so a key owned by either side would have to
|
|
37655
|
-
* be hand-copied by the other — and a hand-copied key is how a setting silently
|
|
37656
|
-
* stops arriving while both sides still look correct. Same reasoning, same
|
|
37657
|
-
* placement as `detail-crop.ts` (D52's "one cluster-wide orchestrator setting").
|
|
37658
|
-
*
|
|
37659
|
-
* ## Why cluster-wide and not per-node
|
|
37660
|
-
*
|
|
37661
|
-
* The lease is a per-decode-worker RAM window. Its purpose — the late
|
|
37662
|
-
* cross-process native crop landing on a full-resolution frame rather than the
|
|
37663
|
-
* ≤640 detection fallback — is a property of the PIPELINE, not of a node's
|
|
37664
|
-
* hardware: a per-node TTL would mean the same camera produces different crop
|
|
37665
|
-
* quality depending on which node the balancer placed it on, and nobody could
|
|
37666
|
-
* tell that from the stored media. Node-level RAM pressure is already handled
|
|
37667
|
-
* by the per-session budget ceiling, which is itself one of these knobs.
|
|
37668
|
-
*
|
|
37669
|
-
* ## What each knob costs
|
|
37670
|
-
*
|
|
37671
|
-
* A retained frame is a full NATIVE-resolution copy in system RAM. With the
|
|
37672
|
-
* default pinned-RGB24 lease path (`CAMSTACK_SESSION_PINNED_RGB_CROP`, on):
|
|
37673
|
-
* 4K ≈ 24.9 MB/frame, 1080p ≈ 6.2 MB/frame. On the YUV420P path (flag off, and
|
|
37674
|
-
* for software-decoded sessions): 4K ≈ 12.4 MB, 1080p ≈ 3.1 MB. Worst-case
|
|
37675
|
-
* resident RAM for ONE busy camera ≈ frameBytes × deliveredFps × ttlSeconds,
|
|
37676
|
-
* clamped by the budget ceiling. See `docs/design/decode-path.md` → "Lease
|
|
37677
|
-
* admission" for what actually gets admitted.
|
|
37678
|
-
*/
|
|
37679
|
-
/**
|
|
37680
|
-
* Store identity of the lease knobs in `pipeline-orchestrator`'s GLOBAL
|
|
37681
|
-
* (cluster-wide) settings. Keys are unique across that addon's whole schema, so
|
|
37682
|
-
* the reader can walk every section instead of trusting the section id.
|
|
37683
|
-
*/
|
|
37684
|
-
var NATIVE_LEASE_SECTION_ID = "native-lease";
|
|
37685
|
-
var NATIVE_LEASE_TTL_KEY = "nativeLeaseTtlMs";
|
|
37686
|
-
var NATIVE_LEASE_BUDGET_KEY = "nativeLeaseBudgetMb";
|
|
37687
|
-
var NATIVE_LEASE_ACTIVITY_KEY = "nativeLeaseActivityMs";
|
|
37688
|
-
var NATIVE_LEASE_ADMISSION_KEY = "nativeLeaseAdmission";
|
|
37689
|
-
/**
|
|
37690
|
-
* WHICH delivered frames the decode worker retains a native copy of.
|
|
37691
|
-
*
|
|
37692
|
-
* - `all` — every frame the worker delivered to the runner. The shipped
|
|
37693
|
-
* behaviour, and the only correct one if something can ask for a crop of a
|
|
37694
|
-
* frame the runner never sent to inference.
|
|
37695
|
-
* - `inferred` — only the frames the runner ADMITTED to its detection queue.
|
|
37696
|
-
* A native-crop request always names a `frameId` that rode an inference
|
|
37697
|
-
* result, so that is the only set a request can name. How much it drops is
|
|
37698
|
-
* the two-plane governor's admit ratio and nothing else: measured at ~50% on
|
|
37699
|
-
* this cluster, not the ~80% the design sketch assumed, because the governor
|
|
37700
|
-
* was not throttling as hard as the sketch supposed. Read
|
|
37701
|
-
* `leaseAdmitted`/`leaseOffered` off the metrics line for the camera in front
|
|
37702
|
-
* of you rather than quoting a number from here. The newest delivered frame is
|
|
37703
|
-
* croppable regardless — it is still the worker's reserved slot, not a lease —
|
|
37704
|
-
* which covers the one-frame race between a mark and the supersede that
|
|
37705
|
-
* consumes it.
|
|
37706
|
-
*/
|
|
37707
|
-
var NativeLeaseAdmissionSchema = z.enum(["all", "inferred"]);
|
|
37708
|
-
/**
|
|
37709
|
-
* Operator-tunable native-lease settings. Bounds are enforced HERE (not only in
|
|
37710
|
-
* the slider) because the value also travels to a forked child process, where a
|
|
37711
|
-
* junk number would silently become a 0-length or unbounded retention window.
|
|
37712
|
-
*/
|
|
37713
|
-
var NativeLeaseSettingsSchema = z.object({
|
|
37714
|
-
/**
|
|
37715
|
-
* How long a retained native frame is served before it counts as a miss.
|
|
37716
|
-
*
|
|
37717
|
-
* Must cover the FULL late-crop horizon: detection inference + the
|
|
37718
|
-
* cross-process inference-result hop to hub post-analysis + tracking + the
|
|
37719
|
-
* tRPC crop round-trip back. Below ~500 ms the busiest cameras' subject crops
|
|
37720
|
-
* outrun it and fall back to the ≤640 detection frame; above ~3 s the resident
|
|
37721
|
-
* RAM per busy camera grows linearly with no measured hit-rate gain.
|
|
37722
|
-
*/
|
|
37723
|
-
ttlMs: z.number().int().min(250).max(1e4),
|
|
37724
|
-
/**
|
|
37725
|
-
* Hard per-decode-worker RAM ceiling for retained native frames, in MB.
|
|
37726
|
-
*
|
|
37727
|
-
* Intended as a SAFETY ceiling with the TTL as the effective cap — but check
|
|
37728
|
-
* which one is actually binding before reasoning from that. At the shipped
|
|
37729
|
-
* 1024 MB and a 2 800 ms TTL, a 4K camera hits the CEILING first (~43 frames
|
|
37730
|
-
* at ~24 MB each) and the TTL never gets to expire anything; `leaseMb` /
|
|
37731
|
-
* `leaseFrames` on the metrics line say which. When the ceiling binds, a
|
|
37732
|
-
* change that admits fewer frames buys retention WINDOW at constant RAM
|
|
37733
|
-
* rather than giving RAM back — lower this knob if RAM is what you wanted.
|
|
37734
|
-
* `0` DISABLES the lease entirely and falls the worker back to the tiny
|
|
37735
|
-
* leak-prone GPU surface ring (~85% crop miss; that is what the lease exists
|
|
37736
|
-
* to replace).
|
|
37737
|
-
*/
|
|
37738
|
-
budgetMb: z.number().int().min(0).max(4096),
|
|
37739
|
-
/**
|
|
37740
|
-
* Demand window: eager per-frame native retention runs only within this many
|
|
37741
|
-
* ms of the last native-crop request (or of the dial starting).
|
|
37742
|
-
*
|
|
37743
|
-
* `0` means ALWAYS ON — it disables the gate, it does not disable retention.
|
|
37744
|
-
* That is the legacy behaviour that saturated an N100 (24 native-4K downloads
|
|
37745
|
-
* per second on a camera with zero crop demand), so leave it non-zero unless
|
|
37746
|
-
* you are reproducing that.
|
|
37747
|
-
*/
|
|
37748
|
-
activityMs: z.number().int().min(0).max(12e4),
|
|
37749
|
-
/**
|
|
37750
|
-
* Which delivered frames are retained at all — see
|
|
37751
|
-
* {@link NativeLeaseAdmissionSchema}. This is the only knob of the four that
|
|
37752
|
-
* changes WHAT is kept rather than for how long, so it is also the only one
|
|
37753
|
-
* that can turn a crop that used to hit into a miss. The worker counts every
|
|
37754
|
-
* crop request naming a frame it did NOT see marked
|
|
37755
|
-
* (`leaseUnmarkedCrops` on the session-decode metrics line): a non-zero value
|
|
37756
|
-
* there is the signal that some caller names frames outside the inference set
|
|
37757
|
-
* and that this must go back to `all`.
|
|
37758
|
-
*/
|
|
37759
|
-
admission: NativeLeaseAdmissionSchema
|
|
37760
|
-
});
|
|
37761
|
-
/**
|
|
37762
|
-
* The values in force when the operator has set nothing — byte-for-byte the
|
|
37763
|
-
* constants the decode worker shipped with as env-var defaults, so making these
|
|
37764
|
-
* settings changed no behaviour on the day it landed.
|
|
37765
|
-
*/
|
|
37766
|
-
var DEFAULT_NATIVE_LEASE_SETTINGS = {
|
|
37767
|
-
ttlMs: 1200,
|
|
37768
|
-
budgetMb: 1024,
|
|
37769
|
-
activityMs: 15e3,
|
|
37770
|
-
admission: "inferred"
|
|
37771
|
-
};
|
|
37772
|
-
/** Slider bounds for the operator-facing knobs (orchestrator settings UI). */
|
|
37773
|
-
var NATIVE_LEASE_TTL_FIELD = {
|
|
37774
|
-
min: 250,
|
|
37775
|
-
max: 1e4,
|
|
37776
|
-
step: 50,
|
|
37777
|
-
default: DEFAULT_NATIVE_LEASE_SETTINGS.ttlMs
|
|
37778
|
-
};
|
|
37779
|
-
var NATIVE_LEASE_BUDGET_FIELD = {
|
|
37780
|
-
min: 0,
|
|
37781
|
-
max: 4096,
|
|
37782
|
-
step: 64,
|
|
37783
|
-
default: DEFAULT_NATIVE_LEASE_SETTINGS.budgetMb
|
|
37784
|
-
};
|
|
37785
|
-
var NATIVE_LEASE_ACTIVITY_FIELD = {
|
|
37786
|
-
min: 0,
|
|
37787
|
-
max: 12e4,
|
|
37788
|
-
step: 1e3,
|
|
37789
|
-
default: DEFAULT_NATIVE_LEASE_SETTINGS.activityMs
|
|
37790
|
-
};
|
|
37791
|
-
/** Select options for the admission knob (orchestrator settings UI). */
|
|
37792
|
-
var NATIVE_LEASE_ADMISSION_FIELD = {
|
|
37793
|
-
options: [{
|
|
37794
|
-
value: "all",
|
|
37795
|
-
label: "Every delivered frame"
|
|
37796
|
-
}, {
|
|
37797
|
-
value: "inferred",
|
|
37798
|
-
label: "Only frames sent to inference"
|
|
37799
|
-
}],
|
|
37800
|
-
default: DEFAULT_NATIVE_LEASE_SETTINGS.admission
|
|
37801
|
-
};
|
|
37802
|
-
/**
|
|
37803
|
-
* Parse one knob, reporting `null` for absent, junk, out-of-bounds — AND for a
|
|
37804
|
-
* value equal to the shipped default.
|
|
37805
|
-
*
|
|
37806
|
-
* That last rule is not tidiness, it is the difference between the documented
|
|
37807
|
-
* precedence being true and being a lie. `addon-settings.getGlobalSettings`
|
|
37808
|
-
* returns a HYDRATED payload, and `hydrateField` fills an unstored field with
|
|
37809
|
-
* the schema's own `default` (verified live on the hub: a cluster that has never
|
|
37810
|
-
* opened the form still reports `nativeLeaseTtlMs = 1200`). A reader that took
|
|
37811
|
-
* that at face value would report all three knobs as "set" on every cluster on
|
|
37812
|
-
* the day this shipped, permanently retiring the `CAMSTACK_SESSION_NATIVE_LEASE_*`
|
|
37813
|
-
* emergency override that the precedence promises. There is no raw-store read on
|
|
37814
|
-
* this cap to distinguish the two, so the default value itself is treated as
|
|
37815
|
-
* "the operator has expressed no preference" — which is also what leaving a
|
|
37816
|
-
* slider untouched means.
|
|
37817
|
-
*
|
|
37818
|
-
* The cost is one honest edge: an operator who deliberately selects the default
|
|
37819
|
-
* value in order to overrule an env var does not get it. Clear the env var
|
|
37820
|
-
* instead; the worker's spawn line names the source, so this is visible rather
|
|
37821
|
-
* than mysterious.
|
|
37822
|
-
*/
|
|
37823
|
-
function readKnob(knob, raw) {
|
|
37824
|
-
const parsed = NativeLeaseSettingsSchema.shape[knob].safeParse(raw);
|
|
37825
|
-
if (!parsed.success) return null;
|
|
37826
|
-
return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS[knob] ? null : parsed.data;
|
|
37827
|
-
}
|
|
37828
|
-
/** {@link readKnob} for the one non-numeric knob. Same default-means-unset rule. */
|
|
37829
|
-
function readAdmissionKnob(raw) {
|
|
37830
|
-
const parsed = NativeLeaseAdmissionSchema.safeParse(raw);
|
|
37831
|
-
if (!parsed.success) return null;
|
|
37832
|
-
return parsed.data === DEFAULT_NATIVE_LEASE_SETTINGS.admission ? null : parsed.data;
|
|
37833
|
-
}
|
|
37834
|
-
/**
|
|
37835
|
-
* Narrow a FLAT settings record to the knobs the operator set.
|
|
37836
|
-
*
|
|
37837
|
-
* Per-FIELD parse, deliberately: a junk TTL must not also discard a valid
|
|
37838
|
-
* budget. An absent, out-of-bounds or default-valued knob is OMITTED (not
|
|
37839
|
-
* clamped, not defaulted) so the caller can still fall through to the env
|
|
37840
|
-
* override — clamping here would turn a typo into a value nobody chose. See
|
|
37841
|
-
* {@link readKnob} for why the default counts as unset.
|
|
37842
|
-
*/
|
|
37843
|
-
function readNativeLeaseOverride(config) {
|
|
37844
|
-
const ttlMs = readKnob("ttlMs", config[NATIVE_LEASE_TTL_KEY]);
|
|
37845
|
-
const budgetMb = readKnob("budgetMb", config[NATIVE_LEASE_BUDGET_KEY]);
|
|
37846
|
-
const activityMs = readKnob("activityMs", config[NATIVE_LEASE_ACTIVITY_KEY]);
|
|
37847
|
-
const admission = readAdmissionKnob(config[NATIVE_LEASE_ADMISSION_KEY]);
|
|
37848
|
-
return {
|
|
37849
|
-
...ttlMs === null ? {} : { ttlMs },
|
|
37850
|
-
...budgetMb === null ? {} : { budgetMb },
|
|
37851
|
-
...activityMs === null ? {} : { activityMs },
|
|
37852
|
-
...admission === null ? {} : { admission }
|
|
37853
|
-
};
|
|
37854
|
-
}
|
|
37855
|
-
function isHydratedField(entry) {
|
|
37856
|
-
return typeof entry === "object" && entry !== null && "key" in entry;
|
|
37857
|
-
}
|
|
37858
|
-
var LEASE_KEYS = [
|
|
37859
|
-
NATIVE_LEASE_TTL_KEY,
|
|
37860
|
-
NATIVE_LEASE_BUDGET_KEY,
|
|
37861
|
-
NATIVE_LEASE_ACTIVITY_KEY,
|
|
37862
|
-
NATIVE_LEASE_ADMISSION_KEY
|
|
37863
|
-
];
|
|
37864
|
-
/**
|
|
37865
|
-
* Extract the operator's lease overrides from an
|
|
37866
|
-
* `addon-settings.getGlobalSettings` payload.
|
|
37867
|
-
*
|
|
37868
|
-
* Walks EVERY section rather than looking inside {@link NATIVE_LEASE_SECTION_ID}
|
|
37869
|
-
* alone: the keys are unique across the addon's schema, and a section rename
|
|
37870
|
-
* must not silently revert the whole cluster to the defaults. A `null` payload
|
|
37871
|
-
* (addon mid-boot) means "operator set nothing" — the env/default fallback then
|
|
37872
|
-
* applies, which is the correct read of "I could not ask".
|
|
37873
|
-
*/
|
|
37874
|
-
function pickNativeLeaseOverride(view) {
|
|
37875
|
-
if (view === null) return {};
|
|
37876
|
-
const flat = {};
|
|
37877
|
-
for (const section of view.sections) for (const entry of section.fields) {
|
|
37878
|
-
if (!isHydratedField(entry) || typeof entry.key !== "string") continue;
|
|
37879
|
-
if (LEASE_KEYS.includes(entry.key)) flat[entry.key] = entry.value;
|
|
37880
|
-
}
|
|
37881
|
-
return readNativeLeaseOverride(flat);
|
|
37882
|
-
}
|
|
37883
|
-
//#endregion
|
|
37884
38138
|
//#region src/helpers/bind-addon-actions.ts
|
|
37885
38139
|
/**
|
|
37886
38140
|
* Bind an addon's custom-action catalog to its tRPC surface, returning a
|
|
@@ -38132,4 +38386,4 @@ function enumerateInferenceDevices(hw) {
|
|
|
38132
38386
|
return out;
|
|
38133
38387
|
}
|
|
38134
38388
|
//#endregion
|
|
38135
|
-
export { ACCESSORY_LABEL, ALEXA_EGRESS_PROFILE, ALL_CAPABILITY_DEFINITIONS, APPLE_SA_TO_MACRO, AUDIO_ANALYSIS_CAP_NAME, AUDIO_BACKEND_CHOICES, AUDIO_MACRO_LABELS, AUDIO_PRESETS, AccessoriesStatusSchema, AccessoryKind, AddBrokerInputSchema, AddonAutoUpdateSchema, AddonListItemSchema, AddonPageDeclarationSchema, AddonPageInfoSchema, AdoptInputSchema as AdoptionAdoptInputSchema, AdoptResultSchema as AdoptionAdoptResultSchema, AdoptionFilterSchema, GetCandidateInputSchema as AdoptionGetCandidateInputSchema, ListCandidatesInputSchema as AdoptionListCandidatesInputSchema, ListCandidatesOutputSchema as AdoptionListCandidatesOutputSchema, ReleaseInputSchema as AdoptionReleaseInputSchema, AdoptionStatusSchema, AgentLoadSummarySchema, AirQualitySensorStatusSchema, AlarmArmModeSchema, AlarmPanelStatusSchema, AlarmStateSchema, AlertSchema, AlertSeveritySchema, AlertSourceSchema, AlertStatusSchema, AmbientLightSensorStatusSchema, ApiKeyRecordSchema, ApiKeySummarySchema, ArchiveEntrySchema, ArchiveManifestSchema, AttachmentMediaTypeSchema, AttachmentSchema, AudioAnalysisResultSchema, AudioAnalysisSettingsSchema, AudioChunkInputSchema, AudioClassSummarySchema, AudioClassificationLabelSchema, AudioClassificationResultSchema, AudioCodecInfoSchema, AudioDecodeSessionConfigSchema, AudioEncodeSchema, AudioEncodeSessionConfigSchema, AudioEncodedChunkSchema, AudioEventSchema, AudioLevelSchema, AudioMetricsHistoryPointSchema, AudioMetricsHistorySchema, AudioMetricsSnapshotSchema, AudioPcmChunkSchema, AuthResultSchema, AutoUpdateSettingsSchema, AutomationControlStatusSchema, AvailableIntegrationTypeSchema, BACKEND_TO_FORMAT, BASE_LIVE_EGRESS_PROFILE, BATTERY_DEVICE_PROFILE, BacklightModeSchema, BackupDestinationInfoSchema, BackupEntrySchema, BaseAddon, BaseDevice, BaseDeviceProvider, BatteryStatusSchema, BinaryStatusSchema, BoundingBoxSchema, BrightnessStatusSchema, AddInputSchema as BrokerAddInputSchema, BrokerAudioClientSchema, BrokerClientsSchema, BrokerConnectionDetailsSchema, BrokerConsumerAttributionSchema, BrokerConsumerKindSchema, BrokerDecodedClientSchema, BrokerEncodedClientSchema, GetStateInputSchema as BrokerGetStateInputSchema, BrokerInfoSchema, BrokerProviderInfoSchema, PublishInputSchema as BrokerPublishInputSchema, RegistryStatusSchema as BrokerRegistryStatusSchema, BrokerRtspClientSchema, BrokerStatsSchema, BrokerStatusEnum, BrokerStatusSchema, SubscribeInputSchema as BrokerSubscribeInputSchema, SubscribeResultSchema as BrokerSubscribeResultSchema, TestConnectionResultSchema as BrokerTestConnectionResultSchema, UnsubscribeInputSchema as BrokerUnsubscribeInputSchema, CAMERA_SWITCH_CATALOG, CAMERA_SWITCH_ORDER, CAM_PROFILE_ORDER, CAPABILITY_NAMES, CAPABILITY_ROUTER_KEYS, CAP_NAMES_WITH_STATUS, CAP_NODE_PIN_CONTEXT_KEY, CAP_PROVIDER_KIND_MAP, COCO_80_LABELS, COCO_TO_MACRO, CamProfileSchema, CamStreamDescriptorSchema, CamStreamKindSchema, CamStreamResolutionSchema, CameraAssignmentStatusSchema, CameraAudioStatusSchema, CameraBrokerProfileSchema, CameraBrokerStatusSchema, CameraCredentialsSchema, CameraCredentialsStatusSchema, CameraDecoderShmSchema, CameraDecoderStatusSchema, CameraDetectionPhaseSchema, CameraDetectionProvisioningSchema, CameraDetectionProvisioningStateSchema, CameraDetectionStatusSchema, CameraMetricsSchema, CameraMetricsWithDeviceIdSchema, CameraMotionStatusSchema, CameraRecordingModeSchema, CameraRecordingStatusSchema, CameraSourceStatusSchema, CameraSourceStreamSchema, CameraStatusSchema, CameraStreamSchema, CameraSwitchAuthoritySchema, CameraSwitchGroupSchema, CameraSwitchIdSchema, CameraSwitchSchema, CameraSwitchUnavailableReasonSchema, CandidateQueryFilterSchema, CapScopeSchema, CapabilityBindingsSchema, CarbonMonoxideStatusSchema, ChargingStatus, ClientNetworkStatsSchema, ClimateControlStatusSchema, ClipPlaybackSchema, ClipSchema, ClusterAddonNodeDeploymentSchema, ClusterAddonStatusEntrySchema, CollectionColumnSchema, CollectionIndexSchema, ColorStatusSchema, ConfigEntrySchema, ConfigSectionWithValuesSchema, ConfigTabDeclarationSchema, ConnectivityStatusSchema, ConsumableItemSchema, ConsumablesStatusSchema, ContactStatusSchema, ControlKindSchema, ControlStatusSchema, ConvertArtifactSchema, ConvertResultSchema, ConvertTargetSchema, CoreBlockCompileResultSchema, CoreBlockInputSchema, CoreBlockPlacementSchema, CoreBlockSchema, CoreBlockStatusSchema, CoverStateSchema, CoverStatusSchema, CreateApiKeyInputSchema, CreateApiKeyResultSchema, CreateIntegrationInputSchema, CreateScopedTokenInputSchema, CreateScopedTokenResultSchema, CreateUserInputSchema, CustomActionInputSchema, CustomModelDescriptorSchema, DATAPLANE_SECRET_HEADER, DEFAULT_ADDON_PLACEMENT, DEFAULT_AUDIO_ANALYZER_CONFIG, DEFAULT_DECODER_HWACCEL_CONFIG, DEFAULT_DETAIL_CROP_CONVENTION, DEFAULT_EVENT_COLOR, DEFAULT_FEATURES, DEFAULT_NATIVE_LEASE_SETTINGS, DEFAULT_RETENTION, DEFAULT_SCRUB_THUMBNAIL_PRESET, DETAIL_CROP_PADDING_FIELD, DETAIL_CROP_PADDING_KEY, DETAIL_CROP_SECTION_ID, DETAIL_CROP_SQUARE_KEY, DETECTION_PIPELINE_CAP_NAME, DEVICE_BACKEND_TO_FORMAT, DEVICE_CAP_NAMES, DEVICE_PROFILES, DEVICE_SCOPED_CAPS, DEVICE_SETTINGS_CONTRIBUTION_METHODS, DEVICE_STATE_READERS, DEVICE_STATUS_METHOD, DEVICE_TYPE_CONTROL_KIND, DEVICE_TYPE_INFO, EngineInfoSchema as DataStoreEngineInfoSchema, DayNightModeSchema, DayNightOptionsSchema, DayNightSettingsPatchSchema, DayNightStatusSchema, DecodedAudioChunkSchema, DecodedFrameSchema, DecoderSessionConfigSchema, DecoderStatsSchema, DeleteIntegrationResultSchema, DetailCropConventionSchema, DetectionSourceSchema, DeviceCodeSeveritySchema, DeviceConfig, DeviceDiscoveryStatusSchema, ExposeInputSchema as DeviceExportExposeInputSchema, DeviceExportStatusSchema, UnexposeInputSchema as DeviceExportUnexposeInputSchema, DeviceFeature, DeviceInfoSchema, DeviceLinkModeSchema, DeviceNetworkStatsSchema, DeviceRole, DeviceRuntimeState, DeviceStatusSchema, DeviceType, DiscoveredChildDeviceSchema, DiscoveredChildStatusSchema, DiscoveredDeviceSchema, DiscoveredTargetSchema, DisposerChain, DoorbellPressEventSchema, DoorbellStatusSchema, EVENTFUL_CAP_NAMES, EVENT_KIND_BY_CAP, EVENT_PAD_MS, EVENT_TAXONOMY, EXPRESSION_BUILTINS, EXPRESSION_BUILTIN_NAMES, EXPRESSION_COMPILE_CACHE_CAPACITY, EXPRESSION_IDENTIFIER_RE, EXPRESSION_INJECTED_NOW, EgressEncodeSchema, EgressRateControlSchema, EgressTranscodeRequestSchema, EgressTranscodeSchema, ElementConfigStore, EmbeddingInfoSchema, EmbeddingResultSchema, EncodeProfileSchema, EncodedPacketSchema, EnrichedWidgetMetadataSchema, EnumSensorDateTimeFormatSchema, EnumSensorStatusSchema, EventCategory, EventEmitterStatusSchema, EventFireSchema, EventItemSchema, EventKindCategorySchema, EventKindDescriptorSchema, EventKindIconSchema, EventKindSchema, EventKindsForDeviceSchema, EventMediaArtifactSchema, EventMediaCoverageSchema, EventMediaKindSchema, EventMediaProductionSchema, EventSourceType, ExportDownloadSchema, ExportOptionsSchema, ExportRecordSchema, ExportSetupFieldSchema, ExportSetupSchema, ExportSpeedSchema, ExportStateSchema, ExportTimelapseSchema, ExposedDeviceSchema, ExposureModeSchema, ExpressionEvalError, ExpressionParseError, FanControlStatusSchema, FanDirectionSchema, FeatureManifestSchema, FeatureProbeStatusSchema, FloodStatusSchema, Fmp4BoxSplitter, FrameHandleFormatSchema, FrameHandleSchema, FrameInputSchema, GasStatusSchema, GetStreamWithCodecInputSchema, GlobalMetricsSchema, HAP_AUDIO_BASE, HAP_AUDIO_BITRATE_KBPS, HAP_AUDIO_VBV_KBITS, HAP_KEYFRAME_INTERVAL_SEC, HF_BASE_URL, HF_REPO, HWACCEL_OPTIONS, HealthStatusSchema, HistoryPointSchema, HistoryResolutionEnum, HumidifierStatusSchema, HumiditySensorStatusSchema, HvacModeSchema, ImageContractSchema, ImageContractStateSchema, ImageRotateSchema, ImageSettingsOptionsSchema, ImageSettingsPatchSchema, ImageSettingsStatusSchema, ImageStatusSchema, IngestOwnerSchema, InstalledPackageSchema, IntegrationLiteSchema, IntegrationWithStateSchema, IntercomAbilitySchema, IntercomStatusSchema, KNOWN_CAP_NAMES, KeyEventSchema, LOG_LEVEL_RANK, LabelAttributionSchema, LabelDefinitionSchema, LabelTierSchema, LawnMowerActivitySchema, LawnMowerControlStatusSchema, LinkedDeviceSchema, LlmDefaultSchema, LlmDefaultSelectorSchema, LlmErrorCodeSchema, LlmGenerateBaseInputSchema, LlmGenerateErrSchema, LlmGenerateOkSchema, LlmGenerateResultSchema, LlmImageSchema, LlmNodeModelSchema, LlmProfileKindDescriptorSchema, LlmProfileKindSchema, LlmProfileSchema, LlmRuntimeCompleteInputSchema, LlmRuntimeDiskUsageSchema, LlmRuntimeNodeSchema, LlmRuntimeStatusSchema, LlmUsageRollupSchema, LlmUsageSchema, LocateSegmentResultSchema, LocationStatSchema, LockControlStatusSchema, LockStateSchema, LogEntrySchema, LogLevelSchema, LogStreamEntrySchema, LoginMethodContributionSchema, LoginStageEnum, MACRO_LABELS, MAX_EXPRESSION_AST_NODES, MAX_EXPRESSION_BINDINGS, MAX_EXPRESSION_CALL_ARGS, MAX_EXPRESSION_EVAL_STEPS, MAX_EXPRESSION_SOURCE_LENGTH, METHOD_ACCESS_MAP, MODEL_FORMATS, MOTION_TRIGGER_FEATURE, ManagedModelCatalogEntrySchema, ManagedModelRefSchema, ManagedRuntimeConfigSchema, MaskGridDimsSchema, MaskGridShapeSchema, MaskLineShapeSchema, MaskPointSchema, MaskPolygonShapeSchema, MaskPolygonVerticesSchema, MaskRectShapeSchema, MaskShapeKindSchema, MaskShapeSchema, MediaFileInfoSchema, MediaFileSchema, MediaPlayerRepeatSchema, MediaPlayerStateSchema, MediaPlayerStatusSchema, MeshPeerSchema, MeshStatusSchema, MethodAccessSchema, ModelCatalogEntrySchema, ModelConvertInputSchema, ModelConvertMetadataSchema, ModelDistributeInputSchema, ModelDistributeResultSchema, ModelExtraFileSchema, ModelFormatEntrySchema, ModelFormatsSchema, ModelSubstitutionSchema, ModelVariantGroupSchema, MotionAnalysisResultSchema, MotionEventSchema, MotionOnMotionChangedDataSchema, MotionRegionSchema, MotionSourceEnum, MotionSourcesSchema, MotionStatusSchema, MotionTriggerRuntimeStateSchema, MotionTriggerStatusSchema, MotionZoneOptionsSchema, MotionZonePatchSchema, MotionZoneRegionSchema, MotionZoneStatusSchema, StatusSchema as MqttBrokerStatusSchema, MutationFilterSchema, NATIVE_LEASE_ACTIVITY_FIELD, NATIVE_LEASE_ACTIVITY_KEY, NATIVE_LEASE_ADMISSION_FIELD, NATIVE_LEASE_ADMISSION_KEY, NATIVE_LEASE_BUDGET_FIELD, NATIVE_LEASE_BUDGET_KEY, NATIVE_LEASE_SECTION_ID, NATIVE_LEASE_TTL_FIELD, NATIVE_LEASE_TTL_KEY, NC_BASE_CONDITION_KEYS, NC_CONDITION_CATALOG, NC_HISTORY_LIMIT_DEFAULT, NC_HISTORY_LIMIT_MAX, NC_MAX_PER_TRACK_IMMEDIATE, NC_SNOOZE_MAX_MINUTES, NC_TAXONOMY, NativeCropBboxSchema, NativeCropRefSchema, NativeCropResultSchema, NativeDetectionSchema, NativeLeaseAdmissionSchema, NativeLeaseSettingsSchema, NativeObjectClassEnum, NativeObjectDetectionRuntimeStateSchema, NativeObjectDetectionStatusSchema, NcAlarmConfigSchema, NcAlarmModeCoverageSchema, NcAlarmSettingsPatchSchema, NcAlarmSettingsSchema, NcConditionDescriptorSchema, NcConditionsSchema, NcCrossingSchema, NcDeliverySchema, NcDeviceStateConditionSchema, NcHistoryEntrySchema, NcHistoryFilterSchema, NcHistoryRecordKindSchema, NcHistoryStatusSchema, NcHistorySubjectSchema, NcMediaFrameSchema, NcMediaPolicySchema, NcOccupancyConditionSchema, NcPlateMatcherSchema, NcRuleActionSchema, NcRuleActionSequenceSchema, NcRuleActionsSchema, NcRuleInputSchema, NcRuleNotificationButtonSchema, NcRulePatchSchema, NcRuleSchema, NcRuleTargetSchema, NcScheduleSchema, NcScheduleWindowSchema, NcSnoozeInputSchema, NcSnoozeSchema, NcSnoozeScopeSchema, NcSnoozeSuppressedSchema, NcTaxonomyEntrySchema, NcTaxonomySchema, NcTestResultSchema, NcThrottleGranularitySchema, NcThrottleSchema, NcZoneConditionSchema, NetworkAccessStatusSchema, NetworkAddressSchema, NetworkEndpointSchema, NotificationActionIconSchema, NotificationActionSchema, NotificationFormatSchema, NotificationSchema, NotifierStatusSchema, NumericSensorStatusSchema, OPS_LOG_DEFAULT_LIMIT, OPS_LOG_RING_DEFAULT_MAX, OauthIntegrationDescriptorSchema, ObjectEventSchema, OpsLogDomainSchema, OpsLogEntrySchema, OpsLogOpSchema, OpsLogQueryInputSchema, OpsLogReasonSchema, OrchestratorMetricsSchema, OsdOverlayKindEnum, OsdOverlayPatchSchema, OsdOverlaySchema, OsdPositionEnum, OsdRenderOutcomeEnum, OsdRenderResultSchema, OsdSlotBindingSchema, OsdSlotViewSchema, OsdSourceOptionSchema, OsdSourceSchema, OsdSourceValueTypeEnum, OsdStatusSchema, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, PIPELINE_FLOW_CAPABILITY_NAMES, PIPELINE_OWNER_CAPABILITY_NAMES, PRIVACY_MASK_CAP_NAME, PROVIDER_KIND_CAP_NAMES, PYTHON_SCRIPT, PackageUpdateSchema, PackageVersionInfoSchema, PasskeyLoginMethodSchema, PasskeySummarySchema, PcmSampleFormatSchema, PerScopeBreakdownSchema, PetFeederStatusSchema, PickStreamPreferencesSchema, PickStreamRequirementsSchema, PickedCamStreamSchema, PipelineAssignmentSchema, PipelineDefaultStepSchema, PipelineEngineChoiceSchema, PipelineRunResultBridge, PipelineStepInputSchema, PipelineValidationIssueSchema, PipelineValidationResultSchema, PlaceholderReasonSchema, PolygonPointSchema, PowerMeterStatusSchema, PresenceStatusSchema, PressureSensorStatusSchema, PrivacyMaskOptionsSchema, PrivacyMaskPatchSchema, PrivacyMaskRegionSchema, PrivacyMaskShapeSchema, PrivacyMaskStatusSchema, ProfileRtspEntrySchema, ProfileSlotSchema, ProfileSlotStatusSchema, ProviderStatusSchema, PtzAutotrackRuntimeStateSchema, PtzAutotrackSettingsSchema, PtzAutotrackStatusSchema, PtzAutotrackTargetOptionSchema, PtzMoveCommandSchema, PtzOptionsSchema, PtzPositionSchema, PtzPresetSchema, PtzStatusSchema, QueryFilterSchema, RATE_CONTROL_RELAXED, RATE_CONTROL_TIGHT, REACHABILITY_FAILURES_TO_OFFLINE, REACHABILITY_POLL_INTERVAL_MS, REACHABILITY_PROBE_TIMEOUT_MS, RECOGNITION_TYPES, RESERVED_BINDING_NAMES, RUNTIME_DEFAULTS, RUNTIME_TO_FORMAT, RawStateResultSchema, ReadGopBytesResultSchema, ReadSegmentBytesResultSchema, ReadinessRegistry, ReadinessTimeoutError, RecentTracksPageSchema, RecentTracksQueryInput, RecordingAvailabilitySchema, RecordingBandModeSchema, RecordingBandSchema, RecordingBandTriggersSchema, RecordingConfigSchema, RecordingDaysSchema, RecordingDeviceUsageSchema, RecordingLocationUsageSchema, RecordingManifestSchema, RecordingRangeSchema, RecordingRetentionSchema, RecordingStatusSchema, RecordingStorageModeSchema, RecordingStorageUsageSchema, RecordingTriggersSchema, RecordingWeekdaySchema, RedirectLoginMethodSchema, RelocateFootageInputSchema, RelocateJobSchema, RelocateJobStateSchema, RelocateMediaInputSchema, RenderedAsSchema, ReportMotionInputSchema, RetrainAnnotationDraftSchema, RetrainAnnotationKindSchema, RetrainAnnotationSchema, RetrainAnnotationSourceSchema, RetrainAssistResultSchema, RetrainAssistSubjectSchema, RetrainCopyRefusalSchema, RetrainFrameCandidateSchema, RetrainFrameListSchema, RetrainFrameSchema, RetrainFrameSelectionSchema, RetrainMacroClassSchema, RetrainStatusSchema, RetrainTrackSchema, RetrainTransitionResultSchema, RingBuffer, RtpSourceSchema, RtspRestreamEntrySchema, RunnerCameraConfigSchema, RunnerCameraDeviceUIFields, RunnerFrameSourceSchema, RunnerInferenceDeviceSchema, RunnerLocalLoadSchema, RunnerLocalMetricsSchema, SCOPE_PRESETS, SCRUB_THUMBNAIL_PRESETS, SCRUB_THUMBNAIL_PRESET_LABELS, SCRUB_THUMBNAIL_PRESET_ORDER, SENSOR_FEATURES, SENSOR_MAP, SOURCE_INFO_METADATA_KEY, STREAM_PROFILE_META, STREAM_QUALITY_LABELS, SUB_DETECTION_TYPES, SYSTEM_CAP_NAMES, SceneCheckSchema, SceneConditionSchema, SceneMonitorSchema, SceneMonitorStateSchema, SceneMonitorStatusSchema, SceneReferenceSchema, ScopedTokenSchema, ScopedTokenSummarySchema, ScoredObjectEventSchema, ScriptRunnerStatusSchema, ScrubThumbnailPresetSchema, SearchResultSchema, SendEmailInputSchema, SendEmailResultSchema, SendResultSchema, SensorEventSchema, ServerBootModeSchema, ServerPackageStatusSchema, ServerRollbackInfoSchema, ServerUpdateActionResultSchema, ServerUpdateCheckResultSchema, ServerUpdateStateSchema, SettingsPatchSchema, SettingsRecordSchema, SettingsSchemaWithValuesSchema, SettingsUpdateResultSchema, ShmRingStatsSchema, SmokeStatusSchema, SmtpStatusSchema, SnapshotImageSchema, SourceInfoSchema, SpatialDetectionSchema, SsoBridgeClaimsSchema, StartEmbeddedInputSchema, StationaryObjectSchema, AbortUploadInputSchema as StorageAbortUploadInputSchema, BeginDownloadInputSchema as StorageBeginDownloadInputSchema, BeginDownloadResultSchema as StorageBeginDownloadResultSchema, BeginUploadInputSchema as StorageBeginUploadInputSchema, BeginUploadResultSchema as StorageBeginUploadResultSchema, EndDownloadInputSchema as StorageEndDownloadInputSchema, FinalizeUploadInputSchema as StorageFinalizeUploadInputSchema, StorageLocationDeclarationSchema, StorageLocationRefSchema, StorageLocationSchema, StorageLocationTypeSchema, ProviderInfoSchema as StorageProviderInfoSchema, ReadChunkInputSchema as StorageReadChunkInputSchema, TestLocationResultSchema as StorageTestLocationResultSchema, WriteChunkInputSchema as StorageWriteChunkInputSchema, StreamCodecSchema, StreamFormatSchema, StreamNetworkStatsSchema, StreamParamsOptionsSchema, StreamParamsStatusSchema, StreamProfileConfigSchema, StreamProfileOptionsSchema, StreamProfilePatchSchema, StreamProfileSchema, StreamSourceEntrySchema, StreamSourceSchema, SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, SubscribeFramesInputSchema, SubscribeFramesResultSchema, SwitchStatusSchema, SystemMetricsSchema, SystemMirror, TAXONOMY_COLORS, TIMEZONES, TRANSCODE_DOWN_MAX_BITRATE_KBPS, TRANSCODE_DOWN_MAX_HEIGHT, TamperStatusSchema, TankStatusSchema, TargetKindCapsSchema, TargetKindLevelSchema, TargetKindSchema, TargetSchema, TemperatureSensorStatusSchema, TerminalProfileInfoSchema, TerminalSessionInfoSchema, TestConnectionResultSchema$1 as TestConnectionResultSchema, TestResultSchema, TimelapseRuleInputSchema, TimelapseRulePatchSchema, TimelapseRuleSchema, TimelapseTemplateSchema, ToastSchema, TokenScopeSchema, TopologyNodeSchema, TopologyProcessSchema, TopologyServiceSchema, TrackCascadeCountsSchema, TrackEnvelopeSchema, TrackFlagsPatchSchema, TrackFlagsSchema, TrackProjectionSchema, TrackSchema, TrackSourceSchema, TrackStateSchema, TrackZoneFilterSchema, TrackedDetectionSchema, TrainingExportDeviceTotalsSchema, TrainingExportSummarySchema, TurnServerSchema, UNIT_TABLE, BrokerInfoSchema$1 as UnifiedBrokerInfoSchema, UnitConversionError, UpdateIntegrationInputSchema, UpdateStatusSchema, UpdateUserInputSchema, UserRecordSchema, UserSummarySchema, VacuumControlStatusSchema, VacuumStateSchema, ValveStateSchema, ValveStatusSchema, VectorDeclareIndexInputSchema, VectorDeleteByFilterInputSchema, VectorDeleteInputSchema, VectorDeleteResultSchema, VectorFilterSchema, VectorGetInputSchema, VectorGetResultSchema, VectorItemSchema, VectorMatchSchema, VectorMetadataSchema, VectorMetricSchema, VectorQueryInputSchema, VectorQueryResultSchema, VectorStatsInputSchema, VectorStatsResultSchema, VectorUpsertInputSchema, VectorUpsertResultSchema, VibrationStatusSchema, VideoEncodeSchema, WEBRTC_EGRESS_PROFILE, WELL_KNOWN_TABS, WELL_KNOWN_TAB_MAP, WaterHeaterStatusSchema, WeatherStatusSchema, WebrtcStreamChoiceSchema, WebrtcStreamTargetSchema, WhiteBalanceModeSchema, WidgetHostEnum, WidgetLoginMethodSchema, WidgetMetadataSchema, WidgetRemoteSchema, WidgetSizeEnum, YAMNET_TO_MACRO, ZoneCrossingDirectionSchema, ZoneCrossingSchema, ZoneKindEnum, ZoneRuleModeEnum, ZoneRuleSchema, ZoneRuleStageEnum, ZoneRulesArraySchema, ZoneSchema, ZoneScopeBreakdownSchema, accessoriesCapability, accessoryStableId, addonPagesCapability, addonPagesSourceCapability, addonRoutesCapability, addonSettingsCapability, addonWidgetsCapability, addonWidgetsSourceCapability, addonsCapability, adminUiCapability, airQualitySensorCapability, alarmPanelCapability, alertsCapability, ambientLightSensorCapability, applyTransform, asBoolean, asJsonArray, asJsonObject, asNumber, asString, audioAnalysisCapability, audioAnalyzerCapability, audioCodecCapability, audioMetricsCapability, audioPlanFromEncodeProfile, authProviderCapability, autoAssignProfiles, automationControlCapability, backupCapability, bareAddonId, batteryCapability, bestLocationMatch, binaryCapability, bindAddonActions, brightnessCapability, brokerCapability, buildAddonRouteProvider, buildAudioArgs, buildEventKindDescriptor, buildFfmpegArgs, buildInputArgs, buildModelVariantGroups, buildNcTaxonomy, buildStreamParamsConfigSchema, buildVideoArgs, buttonCapability, cameraCredentialsCapability, cameraPipelineConfigCapability, cameraStreamsCapability, canConvertUnit, canonicalEgressPlan, carbonMonoxideCapability, cellsToRects, classifyStream, classifyStreams, climateControlCapability, collectHydratedFieldEntries, collectHydratedFieldValues, colorCapability, colorForKind, compileExpression, compileExpressionSafe, connectivityCapability, consumablesCapability, contactCapability, controlCapability, convertUnit, coreBlocksCapability, cosineSimilarity, coverCapability, createDeviceProxy, createDurableState, createEvent, createExpressionScope, createHwAccelCache, createLazyTrpcSource, createMirrorSource, createRuntimeStateBridge, createSliceHandle, createSystemProxy, customAction, customModelRegistryCapability, dataStoreProviderCapability, dayNightCapability, decodeVectorBase64, decoderCapability, defaultDeviceFor, defineCustomActions, deriveCameraSwitches, deriveDetailCropRect, deriveRecordingMode, describeModelVariant, detectionPipelineCapability, deviceAdoptionCapability, deviceBackendToFormat, deviceCustomAction, deviceDiscoveryCapability, deviceExportCapability, deviceManagerCapability, deviceMatchesProfile, deviceOpsCapability, deviceProviderCapability, deviceStateCapability, deviceStatusCapability, doorbellCapability, egressTranscodeSharingKey, egressTransportFromRequest, embeddingEncoderCapability, emitDownForOwnedCaps, emitReadiness, encodeProfileFromStreamShape, encodeVectorBase64, enumSensorCapability, enumerateInferenceDevices, enumerateItemArrayFields, enumerateSchemaFields, errMsg, evaluateAst, evaluateLinkExpression, evaluateZoneRules, event, eventEmitterCapability, eventsCapability, expandCapMethods, extractNestedAddonId, extractSourceInfoFromMetadata, faceGalleryCapability, fanControlCapability, featureProbeCapability, filesystemBrowseCapability, findTimezone, floodCapability, formatForBackend, formatForRuntime, gasCapability, getAudioMacroClassIds, getByPath, getCapsByProviderKind, getTaxonomyEntry, hasMotionTrigger, hfModelUrl, htmlToText, humidifierCapability, humiditySensorCapability, hydrateSchema, imageCapability, imageSettingsCapability, integrationsCapability, intercomCapability, invocationFromEncodeProfile, isAgentOnlyPlacement, isArrayOutputSchema, isBaseConditionKey, isCollectionArrayMethod, isDeployableToAgent, isDeviceConfigCap, isDeviceScopedCap, isEvent, isNode, isObjectInput, isSameAddonId, isScheduleActive, isSoftwareDecode, isVoidInput, jobKindSchema, kebabToCamel, knownValues, lawnMowerControlCapability, lifecycleJobSchema, lifecycleJobScopeSchema, lifecycleJobStateSchema, lifecycleTaskSchema, llmCapability, llmRuntimeCapability, localNetworkCapability, locationSimilarity, lockControlCapability, logBannerArgs, logDestinationCapability, logLevelAtMost, loginMethodCapability, looseSchema, makeProfileBrokerId, makeSourceBrokerId, mapAudioLabelToMacro, markdownToHtmlLite, markdownToText, maskUrlCredentials, mediaPlayerCapability, mergeSourceInfo, meshNetworkCapability, method, metricsProviderCapability, modelConvertCapability, modelDistributorCapability, modelFormatForRuntime, motionCapability, motionDetectionCapability, motionTriggerCapability, motionZonesCapability, mqttBrokerCapability, nativeObjectDetectionCapability, networkAccessCapability, networkQualityCapability, nodePin, nodesCapability, normalizeAddonInitResult, normalizeUnit, notificationOutputCapability, notificationRulesCapability, notifierCapability, numericSensorCapability, oauthIntegrationCapability, objectInputDeclaresAddonId, osdCapability, osdManagerCapability, parseCameraStreamConfig, parseExpression, parseJsonArray, parseJsonObject, parseJsonUnknown, parseProfileBrokerId, parseStreamParamsFormPatch, petFeederCapability, pickAccessoryControl, pickDetailCropConvention, pickNativeLeaseOverride, pickPreferredRtspEntry, pickVideoEncoder, pickerForCondition, pipelineAnalyticsCapability, pipelineExecutorCapability, pipelineOrchestratorCapability, pipelineRunnerCapability, plateGalleryCapability, platformProbeCapability, powerMeterCapability, prepareNotification, presenceCapability, pressureSensorCapability, privacyMaskCapability, procedureAuthKey, ptzAutotrackCapability, ptzCapability, pythonScriptForBackend, readDetailCropConvention, readDeviceStateFrom, readNativeLeaseOverride, readNodePin, readinessKey, rebootCapability, recordingCapability, recordingExportCapability, rectsToCells, requiresPython, resolveAddonExecution, resolveAddonGroup, resolveAddonPlacement, resolveAddonRuntime, resolveCapMount, resolveDetectionRuntime, resolveDeviceControlKind, resolveDeviceProfile, resolveEgressDecodeHwAccel, resolveFormat, resolveHydratedFieldValue, resolveModelFormat, resolveMutate, resolveRunnerId, resolveScrubThumbnailGeometry, resolveVariantModelId, runInferenceStep, runtimeDevices, sceneMonitorCapability, scopeKey, scoreRuntimes, scriptRunnerCapability, selectAssignedProfileSlots, serverManagementCapability, setByPath, settingsStoreCapability, sleep, sleepCancellable, smokeCapability, smtpProviderCapability, snapshotCapability, ssoBridgeCapability, startReachabilityPoll, stateVocabularyFor, storageCapability, storageEvictableCapability, storageProviderCapability, streamBrokerCapability, streamCatalogCapability, streamParamsCapability, streamPixels, streamQualityLabel, subKindsOf, summarisePrivacyAudio, supportedRuntimes, switchCapability, switchedOffIds, synthesizeSourceInfo, systemCapability, tamperCapability, taskLogEntrySchema, taskPhaseSchema, taskTargetSchema, temperatureSensorCapability, terminalSessionCapability, textToHtml, toDeviceSummary, toExpressionValue, toNodeId, toStreamSourceEntry, toastCapability, tokenize, transcodeBody, tryConvertUnit, turnProviderCapability, unitDimension, unitsForDimension, updateCapability, userManagementCapability, userPasskeysCapability, vacuumControlCapability, validateExpressionSource, valveCapability, vectorDimFromBase64, vectorStoreCapability, vibrationCapability, videoclipsCapability, viewerUiCapability, waterHeaterCapability, weatherCapability, webrtcClientHintsSchema, webrtcSessionCapability, wiringAddonHealthSchema, wiringHealthSnapshotSchema, wiringNodeHealthSchema, wiringProbeKindSchema, wiringProbeResultSchema, zodEntriesToConfigUI, zoneAnalyticsCapability, zoneRulesCapability, zonesCapability };
|
|
38389
|
+
export { ACCESSORY_LABEL, ALEXA_EGRESS_PROFILE, ALL_CAPABILITY_DEFINITIONS, APPLE_SA_TO_MACRO, AUDIO_ANALYSIS_CAP_NAME, AUDIO_BACKEND_CHOICES, AUDIO_MACRO_LABELS, AUDIO_PRESETS, AccessoriesStatusSchema, AccessoryKind, AddBrokerInputSchema, AddonAutoUpdateSchema, AddonListItemSchema, AddonPageDeclarationSchema, AddonPageInfoSchema, AdoptInputSchema as AdoptionAdoptInputSchema, AdoptResultSchema as AdoptionAdoptResultSchema, AdoptionFilterSchema, GetCandidateInputSchema as AdoptionGetCandidateInputSchema, ListCandidatesInputSchema as AdoptionListCandidatesInputSchema, ListCandidatesOutputSchema as AdoptionListCandidatesOutputSchema, ReleaseInputSchema as AdoptionReleaseInputSchema, AdoptionStatusSchema, AgentLoadSummarySchema, AirQualitySensorStatusSchema, AlarmArmModeSchema, AlarmPanelStatusSchema, AlarmStateSchema, AlertSchema, AlertSeveritySchema, AlertSourceSchema, AlertStatusSchema, AmbientLightSensorStatusSchema, ApiKeyRecordSchema, ApiKeySummarySchema, ArchiveEntrySchema, ArchiveManifestSchema, AttachmentMediaTypeSchema, AttachmentSchema, AudioAnalysisResultSchema, AudioAnalysisSettingsSchema, AudioChunkInputSchema, AudioClassSummarySchema, AudioClassificationLabelSchema, AudioClassificationResultSchema, AudioCodecInfoSchema, AudioDecodeSessionConfigSchema, AudioEncodeSchema, AudioEncodeSessionConfigSchema, AudioEncodedChunkSchema, AudioEventSchema, AudioLevelSchema, AudioMetricsHistoryPointSchema, AudioMetricsHistorySchema, AudioMetricsSnapshotSchema, AudioPcmChunkSchema, AuthResultSchema, AutoUpdateSettingsSchema, AutomationControlStatusSchema, AvailableIntegrationTypeSchema, BACKEND_TO_FORMAT, BASE_LIVE_EGRESS_PROFILE, BATTERY_DEVICE_PROFILE, BacklightModeSchema, BackupDestinationInfoSchema, BackupEntrySchema, BaseAddon, BaseDevice, BaseDeviceProvider, BatteryStatusSchema, BinaryStatusSchema, BoundingBoxSchema, BrightnessStatusSchema, AddInputSchema as BrokerAddInputSchema, BrokerAudioClientSchema, BrokerClientsSchema, BrokerConnectionDetailsSchema, BrokerConsumerAttributionSchema, BrokerConsumerKindSchema, BrokerDecodedClientSchema, BrokerEncodedClientSchema, GetStateInputSchema as BrokerGetStateInputSchema, BrokerInfoSchema, BrokerProviderInfoSchema, PublishInputSchema as BrokerPublishInputSchema, RegistryStatusSchema as BrokerRegistryStatusSchema, BrokerRtspClientSchema, BrokerStatsSchema, BrokerStatusEnum, BrokerStatusSchema, SubscribeInputSchema as BrokerSubscribeInputSchema, SubscribeResultSchema as BrokerSubscribeResultSchema, TestConnectionResultSchema as BrokerTestConnectionResultSchema, UnsubscribeInputSchema as BrokerUnsubscribeInputSchema, CAMERA_SWITCH_CATALOG, CAMERA_SWITCH_ORDER, CAM_PROFILE_ORDER, CAPABILITY_NAMES, CAPABILITY_ROUTER_KEYS, CAP_NAMES_WITH_STATUS, CAP_NODE_PIN_CONTEXT_KEY, CAP_PROVIDER_KIND_MAP, COCO_80_LABELS, COCO_TO_MACRO, CORE_BLOCK_ADDON_PREFIX, CamProfileSchema, CamStreamDescriptorSchema, CamStreamKindSchema, CamStreamResolutionSchema, CameraAssignmentStatusSchema, CameraAudioStatusSchema, CameraBrokerProfileSchema, CameraBrokerStatusSchema, CameraCredentialsSchema, CameraCredentialsStatusSchema, CameraDecoderShmSchema, CameraDecoderStatusSchema, CameraDetectionPhaseSchema, CameraDetectionProvisioningSchema, CameraDetectionProvisioningStateSchema, CameraDetectionStatusSchema, CameraMetricsSchema, CameraMetricsWithDeviceIdSchema, CameraMotionStatusSchema, CameraRecordingModeSchema, CameraRecordingStatusSchema, CameraSourceStatusSchema, CameraSourceStreamSchema, CameraStatusSchema, CameraStreamSchema, CameraSwitchAuthoritySchema, CameraSwitchGroupSchema, CameraSwitchIdSchema, CameraSwitchSchema, CameraSwitchUnavailableReasonSchema, CandidateQueryFilterSchema, CapScopeSchema, CapabilityBindingsSchema, CarbonMonoxideStatusSchema, ChargingStatus, ClientNetworkStatsSchema, ClimateControlStatusSchema, ClipPlaybackSchema, ClipSchema, ClusterAddonNodeDeploymentSchema, ClusterAddonStatusEntrySchema, CollectionColumnSchema, CollectionIndexSchema, ColorStatusSchema, ConfigEntrySchema, ConfigSectionWithValuesSchema, ConfigTabDeclarationSchema, ConnectivityStatusSchema, ConsumableItemSchema, ConsumablesStatusSchema, ContactStatusSchema, ControlKindSchema, ControlStatusSchema, ConvertArtifactSchema, ConvertResultSchema, ConvertTargetSchema, CoreBlockCompileResultSchema, CoreBlockInputSchema, CoreBlockPlacementSchema, CoreBlockSchema, CoreBlockStatusSchema, CoverStateSchema, CoverStatusSchema, CreateApiKeyInputSchema, CreateApiKeyResultSchema, CreateIntegrationInputSchema, CreateScopedTokenInputSchema, CreateScopedTokenResultSchema, CreateUserInputSchema, CustomActionInputSchema, CustomModelDescriptorSchema, DATAPLANE_SECRET_HEADER, DECLARED_DEVICE_SWEEP_LIMIT, DECLARED_INTEGRATION_FIXED_KEY, DEFAULT_ADDON_PLACEMENT, DEFAULT_AUDIO_ANALYZER_CONFIG, DEFAULT_DECODER_HWACCEL_CONFIG, DEFAULT_DETAIL_CROP_CONVENTION, DEFAULT_EVENT_COLOR, DEFAULT_FEATURES, DEFAULT_NATIVE_LEASE_SETTINGS, DEFAULT_RETENTION, DEFAULT_SCRUB_THUMBNAIL_PRESET, DETAIL_CROP_PADDING_FIELD, DETAIL_CROP_PADDING_KEY, DETAIL_CROP_SECTION_ID, DETAIL_CROP_SQUARE_KEY, DETECTION_PIPELINE_CAP_NAME, DEVICE_BACKEND_TO_FORMAT, DEVICE_CAP_NAMES, DEVICE_PROFILES, DEVICE_SCOPED_CAPS, DEVICE_SETTINGS_CONTRIBUTION_METHODS, DEVICE_STATE_READERS, DEVICE_STATUS_METHOD, DEVICE_TYPE_CONTROL_KIND, DEVICE_TYPE_INFO, EngineInfoSchema as DataStoreEngineInfoSchema, DayNightModeSchema, DayNightOptionsSchema, DayNightSettingsPatchSchema, DayNightStatusSchema, DeclaredDevices, DecodedAudioChunkSchema, DecodedFrameSchema, DecoderSessionConfigSchema, DecoderStatsSchema, DeleteIntegrationResultSchema, DetailCropConventionSchema, DetectionSourceSchema, DeviceCodeSeveritySchema, DeviceConfig, DeviceDiscoveryStatusSchema, ExposeInputSchema as DeviceExportExposeInputSchema, DeviceExportStatusSchema, UnexposeInputSchema as DeviceExportUnexposeInputSchema, DeviceFeature, DeviceInfoSchema, DeviceNetworkStatsSchema, DeviceRole, DeviceRuntimeState, DeviceStatusSchema, DeviceType, DiscoveredChildDeviceSchema, DiscoveredChildStatusSchema, DiscoveredDeviceSchema, DiscoveredTargetSchema, DisposerChain, DoorbellPressEventSchema, DoorbellStatusSchema, EVENTFUL_CAP_NAMES, EVENT_KIND_BY_CAP, EVENT_PAD_MS, EVENT_TAXONOMY, EXPRESSION_BUILTINS, EXPRESSION_BUILTIN_NAMES, EXPRESSION_COMPILE_CACHE_CAPACITY, EXPRESSION_IDENTIFIER_RE, EXPRESSION_INJECTED_NOW, EgressEncodeSchema, EgressRateControlSchema, EgressTranscodeRequestSchema, EgressTranscodeSchema, ElementConfigStore, EmbeddingInfoSchema, EmbeddingResultSchema, EncodeProfileSchema, EncodedPacketSchema, EnrichedWidgetMetadataSchema, EnumSensorDateTimeFormatSchema, EnumSensorStatusSchema, EventCategory, EventEmitterStatusSchema, EventFireSchema, EventItemSchema, EventKindCategorySchema, EventKindDescriptorSchema, EventKindIconSchema, EventKindSchema, EventKindsForDeviceSchema, EventMediaArtifactSchema, EventMediaCoverageSchema, EventMediaKindSchema, EventMediaProductionSchema, EventSourceType, ExportDownloadSchema, ExportOptionsSchema, ExportRecordSchema, ExportSetupFieldSchema, ExportSetupSchema, ExportSpeedSchema, ExportStateSchema, ExportTimelapseSchema, ExposedDeviceSchema, ExposureModeSchema, ExpressionBindingSourceSchema, ExpressionEvalError, ExpressionFieldBindingSchema, ExpressionGlobalBindingSchema, ExpressionLiteralBindingSchema, ExpressionParseError, ExpressionSourceSchema, FanControlStatusSchema, FanDirectionSchema, FeatureManifestSchema, FeatureProbeStatusSchema, FloodStatusSchema, Fmp4BoxSplitter, FrameHandleFormatSchema, FrameHandleSchema, FrameInputSchema, GasStatusSchema, GetStreamWithCodecInputSchema, GlobalMetricsSchema, HAP_AUDIO_BASE, HAP_AUDIO_BITRATE_KBPS, HAP_AUDIO_VBV_KBITS, HAP_KEYFRAME_INTERVAL_SEC, HF_BASE_URL, HF_REPO, HWACCEL_OPTIONS, HealthStatusSchema, HistoryPointSchema, HistoryResolutionEnum, HumidifierStatusSchema, HumiditySensorStatusSchema, HvacModeSchema, ImageContractSchema, ImageContractStateSchema, ImageRotateSchema, ImageSettingsOptionsSchema, ImageSettingsPatchSchema, ImageSettingsStatusSchema, ImageStatusSchema, IngestOwnerSchema, InstalledPackageSchema, IntegrationLiteSchema, IntegrationWithStateSchema, IntercomAbilitySchema, IntercomStatusSchema, KNOWN_CAP_NAMES, KeyEventSchema, LOG_LEVEL_RANK, LabelAttributionSchema, LabelDefinitionSchema, LabelTierSchema, LawnMowerActivitySchema, LawnMowerControlStatusSchema, LinkedDeviceSchema, LinkedDevicesModeSchema, LlmDefaultSchema, LlmDefaultSelectorSchema, LlmErrorCodeSchema, LlmGenerateBaseInputSchema, LlmGenerateErrSchema, LlmGenerateOkSchema, LlmGenerateResultSchema, LlmImageSchema, LlmNodeModelSchema, LlmProfileKindDescriptorSchema, LlmProfileKindSchema, LlmProfileSchema, LlmRuntimeCompleteInputSchema, LlmRuntimeDiskUsageSchema, LlmRuntimeNodeSchema, LlmRuntimeStatusSchema, LlmUsageRollupSchema, LlmUsageSchema, LocateSegmentResultSchema, LocationStatSchema, LockControlStatusSchema, LockStateSchema, LogEntrySchema, LogLevelSchema, LogStreamEntrySchema, LoginMethodContributionSchema, LoginStageEnum, MACRO_LABELS, MAX_EXPRESSION_AST_NODES, MAX_EXPRESSION_BINDINGS, MAX_EXPRESSION_CALL_ARGS, MAX_EXPRESSION_EVAL_STEPS, MAX_EXPRESSION_SOURCE_LENGTH, METHOD_ACCESS_MAP, MODEL_FORMATS, MOTION_TRIGGER_FEATURE, ManagedModelCatalogEntrySchema, ManagedModelRefSchema, ManagedRuntimeConfigSchema, MaskGridDimsSchema, MaskGridShapeSchema, MaskLineShapeSchema, MaskPointSchema, MaskPolygonShapeSchema, MaskPolygonVerticesSchema, MaskRectShapeSchema, MaskShapeKindSchema, MaskShapeSchema, MediaFileInfoSchema, MediaFileSchema, MediaPlayerRepeatSchema, MediaPlayerStateSchema, MediaPlayerStatusSchema, MeshPeerSchema, MeshStatusSchema, MethodAccessSchema, ModelCatalogEntrySchema, ModelConvertInputSchema, ModelConvertMetadataSchema, ModelDistributeInputSchema, ModelDistributeResultSchema, ModelExtraFileSchema, ModelFormatEntrySchema, ModelFormatsSchema, ModelSubstitutionSchema, ModelVariantGroupSchema, MotionAnalysisResultSchema, MotionEventSchema, MotionOnMotionChangedDataSchema, MotionRegionSchema, MotionSourceEnum, MotionSourcesSchema, MotionStatusSchema, MotionTriggerRuntimeStateSchema, MotionTriggerStatusSchema, MotionZoneOptionsSchema, MotionZonePatchSchema, MotionZoneRegionSchema, MotionZoneStatusSchema, StatusSchema as MqttBrokerStatusSchema, MutationFilterSchema, NATIVE_LEASE_ACTIVITY_FIELD, NATIVE_LEASE_ACTIVITY_KEY, NATIVE_LEASE_ADMISSION_FIELD, NATIVE_LEASE_ADMISSION_KEY, NATIVE_LEASE_BUDGET_FIELD, NATIVE_LEASE_BUDGET_KEY, NATIVE_LEASE_SECTION_ID, NATIVE_LEASE_TTL_FIELD, NATIVE_LEASE_TTL_KEY, NC_BASE_CONDITION_KEYS, NC_CONDITION_CATALOG, NC_HISTORY_LIMIT_DEFAULT, NC_HISTORY_LIMIT_MAX, NC_MAX_PER_TRACK_IMMEDIATE, NC_SNOOZE_MAX_MINUTES, NC_TAXONOMY, NativeCropBboxSchema, NativeCropRefSchema, NativeCropResultSchema, NativeDetectionSchema, NativeLeaseAdmissionSchema, NativeLeaseSettingsSchema, NativeObjectClassEnum, NativeObjectDetectionRuntimeStateSchema, NativeObjectDetectionStatusSchema, NcAlarmConfigSchema, NcAlarmModeCoverageSchema, NcAlarmSettingsPatchSchema, NcAlarmSettingsSchema, NcConditionDescriptorSchema, NcConditionsSchema, NcCrossingSchema, NcDeliverySchema, NcDeviceStateConditionSchema, NcHistoryEntrySchema, NcHistoryFilterSchema, NcHistoryRecordKindSchema, NcHistoryStatusSchema, NcHistorySubjectSchema, NcMediaFrameSchema, NcMediaPolicySchema, NcOccupancyConditionSchema, NcPlateMatcherSchema, NcRuleActionSchema, NcRuleActionSequenceSchema, NcRuleActionsSchema, NcRuleInputSchema, NcRuleNotificationButtonSchema, NcRulePatchSchema, NcRuleSchema, NcRuleTargetSchema, NcScheduleSchema, NcScheduleWindowSchema, NcSnoozeInputSchema, NcSnoozeSchema, NcSnoozeScopeSchema, NcSnoozeSuppressedSchema, NcTaxonomyEntrySchema, NcTaxonomySchema, NcTestResultSchema, NcThrottleGranularitySchema, NcThrottleSchema, NcZoneConditionSchema, NetworkAccessStatusSchema, NetworkAddressSchema, NetworkEndpointSchema, NotificationActionIconSchema, NotificationActionSchema, NotificationFormatSchema, NotificationSchema, NotifierStatusSchema, NumericSensorStatusSchema, OPS_LOG_DEFAULT_LIMIT, OPS_LOG_RING_DEFAULT_MAX, OauthIntegrationDescriptorSchema, ObjectEventSchema, OpsLogDomainSchema, OpsLogEntrySchema, OpsLogOpSchema, OpsLogQueryInputSchema, OpsLogReasonSchema, OrchestratorMetricsSchema, OsdOverlayKindEnum, OsdOverlayPatchSchema, OsdOverlaySchema, OsdPositionEnum, OsdRenderOutcomeEnum, OsdRenderResultSchema, OsdSlotBindingSchema, OsdSlotViewSchema, OsdSourceOptionSchema, OsdSourceSchema, OsdSourceValueTypeEnum, OsdStatusSchema, PET_FEEDER_MANUAL_FEED_MAX, PET_FEEDER_MANUAL_FEED_MIN, PIPELINE_FLOW_CAPABILITY_NAMES, PIPELINE_OWNER_CAPABILITY_NAMES, PRIVACY_MASK_CAP_NAME, PROVIDER_KIND_CAP_NAMES, PYTHON_SCRIPT, PackageUpdateSchema, PackageVersionInfoSchema, PasskeyLoginMethodSchema, PasskeySummarySchema, PcmSampleFormatSchema, PerScopeBreakdownSchema, PetFeederStatusSchema, PickStreamPreferencesSchema, PickStreamRequirementsSchema, PickedCamStreamSchema, PipelineAssignmentSchema, PipelineDefaultStepSchema, PipelineEngineChoiceSchema, PipelineRunResultBridge, PipelineStepInputSchema, PipelineValidationIssueSchema, PipelineValidationResultSchema, PlaceholderReasonSchema, PolygonPointSchema, PowerMeterStatusSchema, PresenceStatusSchema, PressureSensorStatusSchema, PrivacyMaskOptionsSchema, PrivacyMaskPatchSchema, PrivacyMaskRegionSchema, PrivacyMaskShapeSchema, PrivacyMaskStatusSchema, ProfileRtspEntrySchema, ProfileSlotSchema, ProfileSlotStatusSchema, ProviderStatusSchema, PtzAutotrackRuntimeStateSchema, PtzAutotrackSettingsSchema, PtzAutotrackStatusSchema, PtzAutotrackTargetOptionSchema, PtzMoveCommandSchema, PtzOptionsSchema, PtzPositionSchema, PtzPresetSchema, PtzStatusSchema, QueryFilterSchema, RATE_CONTROL_RELAXED, RATE_CONTROL_TIGHT, REACHABILITY_FAILURES_TO_OFFLINE, REACHABILITY_POLL_INTERVAL_MS, REACHABILITY_PROBE_TIMEOUT_MS, RECOGNITION_TYPES, RESERVED_BINDING_NAMES, RUNTIME_DEFAULTS, RUNTIME_TO_FORMAT, RawStateResultSchema, ReadGopBytesResultSchema, ReadSegmentBytesResultSchema, ReadinessRegistry, ReadinessTimeoutError, RecentTracksPageSchema, RecentTracksQueryInput, RecordingAvailabilitySchema, RecordingBandModeSchema, RecordingBandSchema, RecordingBandTriggersSchema, RecordingConfigSchema, RecordingDaysSchema, RecordingDeviceUsageSchema, RecordingLocationUsageSchema, RecordingManifestSchema, RecordingRangeSchema, RecordingRetentionSchema, RecordingStatusSchema, RecordingStorageModeSchema, RecordingStorageUsageSchema, RecordingTriggersSchema, RecordingWeekdaySchema, RedirectLoginMethodSchema, RelocateFootageInputSchema, RelocateJobSchema, RelocateJobStateSchema, RelocateMediaInputSchema, RenderedAsSchema, ReportMotionInputSchema, RetrainAnnotationDraftSchema, RetrainAnnotationKindSchema, RetrainAnnotationSchema, RetrainAnnotationSourceSchema, RetrainAssistResultSchema, RetrainAssistSubjectSchema, RetrainCopyRefusalSchema, RetrainFrameCandidateSchema, RetrainFrameListSchema, RetrainFrameSchema, RetrainFrameSelectionSchema, RetrainMacroClassSchema, RetrainStatusSchema, RetrainTrackSchema, RetrainTransitionResultSchema, RingBuffer, RtpSourceSchema, RtspRestreamEntrySchema, RunnerCameraConfigSchema, RunnerCameraDeviceUIFields, RunnerFrameSourceSchema, RunnerInferenceDeviceSchema, RunnerLocalLoadSchema, RunnerLocalMetricsSchema, SCOPE_PRESETS, SCRUB_THUMBNAIL_PRESETS, SCRUB_THUMBNAIL_PRESET_LABELS, SCRUB_THUMBNAIL_PRESET_ORDER, SENSOR_FEATURES, SENSOR_MAP, SOURCE_INFO_METADATA_KEY, STREAM_PROFILE_META, STREAM_QUALITY_LABELS, SUB_DETECTION_TYPES, SYSTEM_CAP_NAMES, SceneCheckSchema, SceneConditionSchema, SceneMonitorSchema, SceneMonitorStateSchema, SceneMonitorStatusSchema, SceneReferenceSchema, ScopedTokenSchema, ScopedTokenSummarySchema, ScoredObjectEventSchema, ScriptRunnerStatusSchema, ScrubThumbnailPresetSchema, SearchResultSchema, SendEmailInputSchema, SendEmailResultSchema, SendResultSchema, SensorEventSchema, ServerBootModeSchema, ServerPackageStatusSchema, ServerRollbackInfoSchema, ServerUpdateActionResultSchema, ServerUpdateCheckResultSchema, ServerUpdateStateSchema, SettingsPatchSchema, SettingsRecordSchema, SettingsSchemaWithValuesSchema, SettingsUpdateResultSchema, ShmRingStatsSchema, SmokeStatusSchema, SmtpStatusSchema, SnapshotImageSchema, SourceInfoSchema, SpatialDetectionSchema, SsoBridgeClaimsSchema, StartEmbeddedInputSchema, StationaryObjectSchema, AbortUploadInputSchema as StorageAbortUploadInputSchema, BeginDownloadInputSchema as StorageBeginDownloadInputSchema, BeginDownloadResultSchema as StorageBeginDownloadResultSchema, BeginUploadInputSchema as StorageBeginUploadInputSchema, BeginUploadResultSchema as StorageBeginUploadResultSchema, EndDownloadInputSchema as StorageEndDownloadInputSchema, FinalizeUploadInputSchema as StorageFinalizeUploadInputSchema, StorageLocationDeclarationSchema, StorageLocationRefSchema, StorageLocationSchema, StorageLocationTypeSchema, ProviderInfoSchema as StorageProviderInfoSchema, ReadChunkInputSchema as StorageReadChunkInputSchema, TestLocationResultSchema as StorageTestLocationResultSchema, WriteChunkInputSchema as StorageWriteChunkInputSchema, StreamCodecSchema, StreamFormatSchema, StreamNetworkStatsSchema, StreamParamsOptionsSchema, StreamParamsStatusSchema, StreamProfileConfigSchema, StreamProfileOptionsSchema, StreamProfilePatchSchema, StreamProfileSchema, StreamSourceEntrySchema, StreamSourceSchema, SubscribeAudioChunksInputSchema, SubscribeAudioChunksResultSchema, SubscribeFramesInputSchema, SubscribeFramesResultSchema, SwitchStatusSchema, SystemMetricsSchema, SystemMirror, TAXONOMY_COLORS, TIMEZONES, TRANSCODE_DOWN_MAX_BITRATE_KBPS, TRANSCODE_DOWN_MAX_HEIGHT, TamperStatusSchema, TankStatusSchema, TargetKindCapsSchema, TargetKindLevelSchema, TargetKindSchema, TargetSchema, TemperatureSensorStatusSchema, TerminalProfileInfoSchema, TerminalSessionInfoSchema, TestConnectionResultSchema$1 as TestConnectionResultSchema, TestResultSchema, TimelapseRuleInputSchema, TimelapseRulePatchSchema, TimelapseRuleSchema, TimelapseTemplateSchema, ToastSchema, TokenScopeSchema, TopologyNodeSchema, TopologyProcessSchema, TopologyServiceSchema, TrackCascadeCountsSchema, TrackEnvelopeSchema, TrackFlagsPatchSchema, TrackFlagsSchema, TrackProjectionSchema, TrackSchema, TrackSourceSchema, TrackStateSchema, TrackZoneFilterSchema, TrackedDetectionSchema, TrainingExportDeviceTotalsSchema, TrainingExportSummarySchema, TurnServerSchema, UNIT_TABLE, BrokerInfoSchema$1 as UnifiedBrokerInfoSchema, UnitConversionError, UpdateIntegrationInputSchema, UpdateStatusSchema, UpdateUserInputSchema, UserRecordSchema, UserSummarySchema, VacuumControlStatusSchema, VacuumStateSchema, ValveStateSchema, ValveStatusSchema, VectorDeclareIndexInputSchema, VectorDeleteByFilterInputSchema, VectorDeleteInputSchema, VectorDeleteResultSchema, VectorFilterSchema, VectorGetInputSchema, VectorGetResultSchema, VectorItemSchema, VectorMatchSchema, VectorMetadataSchema, VectorMetricSchema, VectorQueryInputSchema, VectorQueryResultSchema, VectorStatsInputSchema, VectorStatsResultSchema, VectorUpsertInputSchema, VectorUpsertResultSchema, VibrationStatusSchema, VideoEncodeSchema, WEBRTC_EGRESS_PROFILE, WELL_KNOWN_TABS, WELL_KNOWN_TAB_MAP, WaterHeaterStatusSchema, WeatherStatusSchema, WebrtcStreamChoiceSchema, WebrtcStreamTargetSchema, WhiteBalanceModeSchema, WidgetHostEnum, WidgetLoginMethodSchema, WidgetMetadataSchema, WidgetRemoteSchema, WidgetSizeEnum, YAMNET_TO_MACRO, ZoneCrossingDirectionSchema, ZoneCrossingSchema, ZoneKindEnum, ZoneRuleModeEnum, ZoneRuleSchema, ZoneRuleStageEnum, ZoneRulesArraySchema, ZoneSchema, ZoneScopeBreakdownSchema, accessoriesCapability, accessoryStableId, addonPagesCapability, addonPagesSourceCapability, addonRoutesCapability, addonSettingsCapability, addonWidgetsCapability, addonWidgetsSourceCapability, addonsCapability, adminUiCapability, airQualitySensorCapability, alarmPanelCapability, alertsCapability, ambientLightSensorCapability, asBoolean, asJsonArray, asJsonObject, asNumber, asString, audioAnalysisCapability, audioAnalyzerCapability, audioCodecCapability, audioMetricsCapability, audioPlanFromEncodeProfile, authProviderCapability, autoAssignProfiles, automationControlCapability, backupCapability, bareAddonId, batteryCapability, bestLocationMatch, binaryCapability, bindAddonActions, brightnessCapability, brokerCapability, buildAddonRouteProvider, buildAudioArgs, buildEventKindDescriptor, buildFfmpegArgs, buildInputArgs, buildModelVariantGroups, buildNcTaxonomy, buildStreamParamsConfigSchema, buildVideoArgs, buttonCapability, cameraCredentialsCapability, cameraPipelineConfigCapability, cameraStreamsCapability, canConvertUnit, canonicalEgressPlan, carbonMonoxideCapability, cellsToRects, classifyStream, classifyStreams, climateControlCapability, collectHydratedFieldEntries, collectHydratedFieldValues, colorCapability, colorForKind, compileExpression, compileExpressionSafe, connectivityCapability, consumablesCapability, contactCapability, controlCapability, convertUnit, coreBlockAddonId, coreBlockIdFromAddonId, coreBlocksCapability, cosineSimilarity, coverCapability, createDeviceProxy, createDurableState, createEvent, createExpressionScope, createHwAccelCache, createLazyTrpcSource, createMirrorSource, createRuntimeStateBridge, createSliceHandle, createSystemProxy, customAction, customModelRegistryCapability, dataStoreProviderCapability, dayNightCapability, declarationOwnerNodeId, decodeVectorBase64, decoderCapability, defaultDeviceFor, defineCustomActions, deriveCameraSwitches, deriveDetailCropRect, deriveRecordingMode, describeModelVariant, detectionPipelineCapability, deviceAdoptionCapability, deviceBackendToFormat, deviceCustomAction, deviceDiscoveryCapability, deviceExportCapability, deviceManagerCapability, deviceMatchesProfile, deviceOpsCapability, deviceProviderCapability, deviceStateCapability, deviceStatusCapability, doorbellCapability, egressTranscodeSharingKey, egressTransportFromRequest, embeddingEncoderCapability, emitDownForOwnedCaps, emitReadiness, encodeProfileFromStreamShape, encodeVectorBase64, enumSensorCapability, enumerateInferenceDevices, enumerateItemArrayFields, enumerateSchemaFields, errMsg, evaluateAst, evaluateExpressionSource, evaluateZoneRules, event, eventEmitterCapability, eventsCapability, expandCapMethods, extractNestedAddonId, extractSourceInfoFromMetadata, faceGalleryCapability, fanControlCapability, featureProbeCapability, filesystemBrowseCapability, findTimezone, floodCapability, formatForBackend, formatForRuntime, gasCapability, getAudioMacroClassIds, getByPath, getCapsByProviderKind, getTaxonomyEntry, hasMotionTrigger, hfModelUrl, htmlToText, humidifierCapability, humiditySensorCapability, hydrateSchema, imageCapability, imageSettingsCapability, integrationsCapability, intercomCapability, invocationFromEncodeProfile, isAgentOnlyPlacement, isArrayOutputSchema, isBaseConditionKey, isCollectionArrayMethod, isDeployableToAgent, isDeviceConfigCap, isDeviceScopedCap, isEvent, isNode, isObjectInput, isSameAddonId, isScheduleActive, isSoftwareDecode, isVoidInput, jobKindSchema, kebabToCamel, knownValues, lawnMowerControlCapability, lifecycleJobSchema, lifecycleJobScopeSchema, lifecycleJobStateSchema, lifecycleTaskSchema, llmCapability, llmRuntimeCapability, localNetworkCapability, locationSimilarity, lockControlCapability, logBannerArgs, logDestinationCapability, logLevelAtMost, loginMethodCapability, looseSchema, makeProfileBrokerId, makeSourceBrokerId, mapAudioLabelToMacro, markdownToHtmlLite, markdownToText, maskUrlCredentials, mediaPlayerCapability, mergeSourceInfo, meshNetworkCapability, method, metricsProviderCapability, modelConvertCapability, modelDistributorCapability, modelFormatForRuntime, motionCapability, motionDetectionCapability, motionTriggerCapability, motionZonesCapability, mqttBrokerCapability, nativeObjectDetectionCapability, networkAccessCapability, networkQualityCapability, nodePin, nodesCapability, normalizeAddonInitResult, normalizeUnit, notificationOutputCapability, notificationRulesCapability, notifierCapability, numericSensorCapability, oauthIntegrationCapability, objectInputDeclaresAddonId, osdCapability, osdManagerCapability, parseCameraStreamConfig, parseExpression, parseJsonArray, parseJsonObject, parseJsonUnknown, parseProfileBrokerId, parseStreamParamsFormPatch, petFeederCapability, pickAccessoryControl, pickDetailCropConvention, pickNativeLeaseOverride, pickPreferredRtspEntry, pickVideoEncoder, pickerForCondition, pipelineAnalyticsCapability, pipelineExecutorCapability, pipelineOrchestratorCapability, pipelineRunnerCapability, plateGalleryCapability, platformProbeCapability, powerMeterCapability, prepareNotification, presenceCapability, pressureSensorCapability, privacyMaskCapability, procedureAuthKey, ptzAutotrackCapability, ptzCapability, pythonScriptForBackend, readDetailCropConvention, readDeviceStateFrom, readNativeLeaseOverride, readNodePin, readinessKey, rebootCapability, recordingCapability, recordingExportCapability, rectsToCells, requiresPython, resolveAddonExecution, resolveAddonGroup, resolveAddonPlacement, resolveAddonRuntime, resolveCapMount, resolveDetectionRuntime, resolveDeviceControlKind, resolveDeviceProfile, resolveEgressDecodeHwAccel, resolveFormat, resolveHydratedFieldValue, resolveModelFormat, resolveMutate, resolveRunnerId, resolveScrubThumbnailGeometry, resolveVariantModelId, runInferenceStep, runtimeDevices, sceneMonitorCapability, scopeKey, scoreRuntimes, scriptRunnerCapability, selectAssignedProfileSlots, serverManagementCapability, setByPath, settingsStoreCapability, sleep, sleepCancellable, smokeCapability, smtpProviderCapability, snapshotCapability, ssoBridgeCapability, startReachabilityPoll, stateVocabularyFor, storageCapability, storageEvictableCapability, storageProviderCapability, streamBrokerCapability, streamCatalogCapability, streamParamsCapability, streamPixels, streamQualityLabel, subKindsOf, summarisePrivacyAudio, supportedRuntimes, switchCapability, switchedOffIds, synthesizeSourceInfo, systemCapability, tamperCapability, taskLogEntrySchema, taskPhaseSchema, taskTargetSchema, temperatureSensorCapability, terminalSessionCapability, textToHtml, toDeviceSummary, toExpressionValue, toNodeId, toStreamSourceEntry, toastCapability, tokenize, transcodeBody, tryConvertUnit, turnProviderCapability, unitDimension, unitsForDimension, updateCapability, userManagementCapability, userPasskeysCapability, vacuumControlCapability, validateExpressionSource, valveCapability, vectorDimFromBase64, vectorStoreCapability, vibrationCapability, videoclipsCapability, viewerUiCapability, waterHeaterCapability, weatherCapability, webrtcClientHintsSchema, webrtcSessionCapability, wiringAddonHealthSchema, wiringHealthSnapshotSchema, wiringNodeHealthSchema, wiringProbeKindSchema, wiringProbeResultSchema, zodEntriesToConfigUI, zoneAnalyticsCapability, zoneRulesCapability, zonesCapability };
|