@camstack/addon-osd-manager 0.1.155 → 0.1.157

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.
Files changed (25) hide show
  1. package/dist/{MaskShapeCanvas-BEVxAjNq.mjs → MaskShapeCanvas-BT714ljC.mjs} +113 -113
  2. package/dist/MotionZonesSettings-vYt4dibj.mjs +310 -0
  3. package/dist/{PrivacyMaskSettings-D7EH_wl3.mjs → PrivacyMaskSettings-BiQ78hst.mjs} +35 -60
  4. package/dist/{SceneMonitorEditor-CO6VwdkE.mjs → SceneMonitorEditor-CJtJIyMf.mjs} +307 -307
  5. package/dist/_stub.js +11763 -4481
  6. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-B3v0K7b1.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-DdAtpC7z.mjs} +3 -3
  7. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BtgV-Ym5.mjs +26 -0
  8. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-CHxDN0gE.mjs +26 -0
  9. package/dist/{_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare__react__loadShare__.js-Bs1t18EM.mjs → _virtual_mf___mfe_internal__addon_osd_manager_page__loadShare__react__loadShare__.js-C5jUMd7X.mjs} +1 -1
  10. package/dist/{_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare__react_mf_2_dom__loadShare__.js-C0K4h_0e.mjs → _virtual_mf___mfe_internal__addon_osd_manager_page__loadShare__react_mf_2_dom__loadShare__.js-qB5uHZAd.mjs} +8 -7
  11. package/dist/addon-osd-manager.css +1 -1
  12. package/dist/{hostInit-B3sHUn3j.mjs → hostInit-0Mc7Fm1R.mjs} +2 -2
  13. package/dist/index.js +1077 -112
  14. package/dist/index.mjs +1077 -112
  15. package/dist/{player-overlays-CTsQBdl8.mjs → player-overlays-BPyna-eI.mjs} +14 -14
  16. package/dist/remoteEntry.js +1 -1
  17. package/dist/{responsive-BPw6Y2I0.mjs → responsive-B0yDP9vf.mjs} +31 -27
  18. package/dist/{scene-monitor-copy-DNkKk6vF.mjs → scene-monitor-copy-BzX20O6q.mjs} +13 -13
  19. package/dist/{square-BPzhX8Pc.mjs → square-BOMOn_6j.mjs} +29 -4
  20. package/dist/{trash-2-DemFUbcB.mjs → trash-2-BolGRl2P.mjs} +1 -1
  21. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-BUQgSy9b.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-UY9P_4V2.mjs} +1 -1
  22. package/package.json +1 -3
  23. package/dist/MotionZonesSettings-DHyaxzBi.mjs +0 -310
  24. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-1qxETV_o.mjs +0 -26
  25. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_tanstack_mf_1_react_mf_2_query__loadShare__.js-DmzTg7Gl.mjs +0 -25
package/dist/index.js CHANGED
@@ -7523,7 +7523,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7523
7523
  return DeviceRole;
7524
7524
  }({});
7525
7525
  //#endregion
7526
- //#region ../types/dist/composition-D7ULvLdD.mjs
7526
+ //#region ../types/dist/composition-Bzr7GwgP.mjs
7527
7527
  /**
7528
7528
  * The ffmpeg build every CamStack node downloads, per platform and arch.
7529
7529
  *
@@ -7611,6 +7611,8 @@ var SOURCE_CAP_ACTIVE_FIELD = {
7611
7611
  vibration: "detected",
7612
7612
  tamper: "tampered"
7613
7613
  };
7614
+ /** Cap names whose presence in a device's bindings qualify it as a source. */
7615
+ var SOURCE_CAPS = Object.keys(SOURCE_CAP_ACTIVE_FIELD);
7614
7616
  /**
7615
7617
  * Error types for the safe expression engine. Two distinct classes so callers
7616
7618
  * can tell a compile-time (grammar) failure from a runtime (evaluation)
@@ -12480,7 +12482,7 @@ var BrokerStatusEnum = _enum([
12480
12482
  "unreachable",
12481
12483
  "error"
12482
12484
  ]);
12483
- var BrokerInfoSchema$1 = object({
12485
+ var BrokerInfoSchema = object({
12484
12486
  /** Stable broker id. Persisted; survives addon restarts. */
12485
12487
  id: string(),
12486
12488
  /** Addon id of the provider that OWNS this broker.
@@ -12623,8 +12625,8 @@ var brokerCapability = {
12623
12625
  kind: "poll"
12624
12626
  },
12625
12627
  methods: {
12626
- list: method(ListInputSchema, array(BrokerInfoSchema$1)),
12627
- get: method(GetInputSchema, BrokerInfoSchema$1.nullable()),
12628
+ list: method(ListInputSchema, array(BrokerInfoSchema)),
12629
+ get: method(GetInputSchema, BrokerInfoSchema.nullable()),
12628
12630
  /** Enumerate which addon provides which broker kind(s) for the
12629
12631
  * unified create picker. The auto-mount fans this array across
12630
12632
  * every registered `broker` provider (array-output method), so the
@@ -15443,6 +15445,51 @@ var SourceInfoSchema = object({
15443
15445
  raw: record(string(), unknown()).optional()
15444
15446
  });
15445
15447
  /**
15448
+ * Coerce common unit-of-measurement string variants to a canonical form
15449
+ * so the UI's display templates can rely on stable strings. Pure
15450
+ * passthrough for unknown units — kept conservative on purpose, the goal
15451
+ * is to normalise the long tail of HA `unit_of_measurement` values that
15452
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
15453
+ *
15454
+ * Extend the map as we encounter real-world data. Returning the raw
15455
+ * input for unknown values is intentional: providers should never
15456
+ * lose information silently here.
15457
+ */
15458
+ function normalizeUnit(raw) {
15459
+ if (raw == null) return void 0;
15460
+ const trimmed = raw.trim();
15461
+ if (trimmed === "") return void 0;
15462
+ const lower = trimmed.toLowerCase();
15463
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
15464
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
15465
+ if (lower === "k" || lower === "kelvin") return "K";
15466
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
15467
+ if (lower === "lx" || lower === "lux") return "lx";
15468
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
15469
+ if (lower === "kpa") return "kPa";
15470
+ if (lower === "pa") return "Pa";
15471
+ if (lower === "mbar" || lower === "millibar") return "mbar";
15472
+ if (lower === "w" || lower === "watt") return "W";
15473
+ if (lower === "kw" || lower === "kilowatt") return "kW";
15474
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
15475
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
15476
+ if (lower === "v" || lower === "volt") return "V";
15477
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
15478
+ if (lower === "ma" || lower === "milliampere") return "mA";
15479
+ if (lower === "va") return "VA";
15480
+ if (lower === "ppm") return "ppm";
15481
+ if (lower === "ppb") return "ppb";
15482
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
15483
+ if (lower === "km/h" || lower === "kph") return "km/h";
15484
+ if (lower === "mph") return "mph";
15485
+ if (lower === "m/s") return "m/s";
15486
+ if (lower === "m") return "m";
15487
+ if (lower === "km") return "km";
15488
+ if (lower === "kg") return "kg";
15489
+ if (lower === "g") return "g";
15490
+ return trimmed;
15491
+ }
15492
+ /**
15446
15493
  * device-discovery — device-scoped capability for parents that host /
15447
15494
  * enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
15448
15495
  * integrations). The parent advertises a list of discoverable children,
@@ -19679,7 +19726,7 @@ var BrokerStatusSchema$1 = _enum([
19679
19726
  "unreachable",
19680
19727
  "tls-error"
19681
19728
  ]);
19682
- var BrokerInfoSchema = object({
19729
+ var BrokerInfoSchema$1 = object({
19683
19730
  id: string(),
19684
19731
  name: string(),
19685
19732
  url: string(),
@@ -19752,7 +19799,7 @@ var mqttBrokerCapability = {
19752
19799
  kind: "poll"
19753
19800
  },
19754
19801
  methods: {
19755
- listBrokers: method(_void(), array(BrokerInfoSchema)),
19802
+ listBrokers: method(_void(), array(BrokerInfoSchema$1)),
19756
19803
  getBrokerConfig: method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }),
19757
19804
  addBroker: method(AddBrokerInputSchema, AddBrokerResultSchema, {
19758
19805
  kind: "mutation",
@@ -22006,78 +22053,16 @@ var NcRuleInputSchema = object({
22006
22053
  */
22007
22054
  confirm: NcConfirmSchema.optional(),
22008
22055
  /**
22009
- * WAIT for face/plate recognition before saying anything.
22010
- *
22011
- * A notification's TEXT is frozen at enqueue and its media is re-resolved at
22012
- * send; the identity is neither. A face is confirmed after `confirmFrames`
22013
- * agreeing observations — p50 **11.4 s** after the track was first seen,
22014
- * measured on this hub — and an `immediate` rule enqueues on the first object
22015
- * event, seconds before that. So "Gianluca è arrivato" is unsayable on the
22016
- * immediate path, and no amount of media re-resolution fixes a sentence.
22017
- *
22018
- * Only two honest answers exist, and this flag picks between them. It has
22019
- * effect ONLY on a rule that declares a recognition scope
22020
- * ({@link NcConditions.identities} or {@link NcConditions.plates}) — on any
22021
- * other rule there is nothing to wait for and the flag is inert.
22022
- *
22023
- * | value | what happens |
22024
- * | --- | --- |
22025
- * | `true` | the rule stops firing on the object event and fires at TRACK CLOSE instead, once, with the name — later, and complete |
22026
- * | absent / `false` | it fires at once WITHOUT the name, and if recognition lands before the track closes a SECOND, "…is Gianluca" notification follows (one per track, per rule, per target) |
22027
- *
22028
- * `.optional()` and deliberately NOT `.default()`: a Zod default does not run
22029
- * on the addon cap path, and absent has to keep meaning exactly what every
22030
- * rule authored before this field meant.
22031
- *
22032
- * The cost of `true` is stated here because the editor states it too: a rule
22033
- * that waits also inherits track-close SEMANTICS — its `zones` condition
22034
- * tests every zone the track visited and a `crossing` condition can no longer
22035
- * be satisfied, because a closed track carries no crossing.
22056
+ * IMMEDIATE GROUPED (D692): the first matching subject opens a window of this many
22057
+ * seconds; the rule sends ONE message when the window closes — early, once every
22058
+ * subject in it is final, never before {@link NC_GROUPING_MIN_SEC} — naming everybody
22059
+ * the rule matched, with one mosaic tile per subject.
22060
+ * Absent = plain immediate (one message per track, at the first match).
22061
+ * Only an `immediate` DETECTION rule may carry it (`ruleGroupingApplies`, enforced on
22062
+ * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
22063
+ * path does not run Zod defaults.
22036
22064
  */
22037
- waitForEnhancement: boolean().optional(),
22038
- /**
22039
- * GROUP a burst of subjects into ONE notification that grows.
22040
- *
22041
- * Seconds of quiet after the last matching subject before the burst is
22042
- * considered over. While it is open, the first subject enqueues immediately —
22043
- * **exactly as today, with no added latency** — and every real growth (a new
22044
- * subject, or a name confirmed on one already in it) REPLACES that
22045
- * notification with an updated one naming everybody. The push carries the
22046
- * group's own coalescing tag, so the phone replaces rather than stacks.
22047
- *
22048
- * `0` / absent = off, and off is today's behaviour byte for byte.
22049
- *
22050
- * ### Why an idle cutoff and not a window
22051
- *
22052
- * The measured seven-person arrival on device 590 spans 110 s with every
22053
- * internal gap under 30 s. A 12 s fixed window cuts it into three groups; an
22054
- * idle cutoff holds it as one and ends it when the arrival actually ends.
22055
- * 30 s is a widely shipped value for the same decision.
22056
- *
22057
- * ### What it replaces
22058
- *
22059
- * The blind cooldown, which collapses a burst by DISCARDING it. Measured on
22060
- * device 615 / *Persona su Uscio* over six days: 116 qualifying tracks → 74
22061
- * notifications, **44 (37.9%) suppressed outright**, 23 of them overlapping a
22062
- * track that did fire and 7 carrying a confirmed identity nobody heard about.
22063
- * A group collapses the same volume by MERGING, so the cooldown becomes a
22064
- * budget over GROUPS — which is what it always meant — and a growth is never
22065
- * throttled by the window its own first member spent.
22066
- *
22067
- * ### Interaction with {@link waitForEnhancement}
22068
- *
22069
- * They compose, and the order matters. `waitForEnhancement` defers the rule to
22070
- * TRACK CLOSE, so with both set the group is opened by the first member to
22071
- * CLOSE — already carrying its name — and grows as later members close. That
22072
- * is later, and complete. With grouping alone the group opens on the first
22073
- * object event and picks up names as they are confirmed, through the growth
22074
- * path. Neither combination fires twice for one subject.
22075
- *
22076
- * `.optional()` and deliberately NOT `.default()`: a Zod default does not run
22077
- * on the addon cap path, so absent must keep meaning what it meant before this
22078
- * field existed.
22079
- */
22080
- groupIdleSec: number().int().min(0).max(600).optional()
22065
+ groupingSec: number().int().min(5).max(60).optional()
22081
22066
  });
22082
22067
  /**
22083
22068
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -22125,9 +22110,10 @@ var NcRuleClearableKeySchema = _enum([
22125
22110
  "targetUsers",
22126
22111
  "snoozeOptions",
22127
22112
  "snoozeAllowGlobal",
22128
- "waitForEnhancement",
22129
22113
  "actions",
22130
22114
  "confirm",
22115
+ "groupingSec",
22116
+ "waitForEnhancement",
22131
22117
  "groupIdleSec"
22132
22118
  ]);
22133
22119
  var NcRuleUpdateInputSchema = object({
@@ -22149,9 +22135,25 @@ var NcRuleSchema = NcRuleInputSchema.extend({
22149
22135
  */
22150
22136
  disabledTargetIds: array(string()).default([])
22151
22137
  });
22138
+ /**
22139
+ * The state-summary SECTIONS (D647 / D698): the D637 kind groups that report a
22140
+ * still-true state. `backups-exports` and `alarm` are not sections — a completed
22141
+ * job is not an open state, and the alarm stays instant.
22142
+ */
22143
+ var NcStateSummarySectionIdSchema = _enum([
22144
+ "device-state",
22145
+ "streams",
22146
+ "battery",
22147
+ "consumables",
22148
+ "tanks",
22149
+ "detection-blind",
22150
+ "nodes",
22151
+ "addons",
22152
+ "updates"
22153
+ ]);
22152
22154
  /** One section of a previewed state summary. `unavailable` = its state could not be read. */
22153
22155
  var NcStateSummaryPreviewSectionSchema = object({
22154
- id: string(),
22156
+ id: NcStateSummarySectionIdSchema,
22155
22157
  status: _enum(["read", "unavailable"]),
22156
22158
  items: number().int().nonnegative(),
22157
22159
  reason: string().optional()
@@ -22161,7 +22163,7 @@ var NcStateSummaryPreviewSectionSchema = object({
22161
22163
  * a `rule` split (no `section`), one section's on a `section` split.
22162
22164
  */
22163
22165
  var NcStateSummaryPreviewRowSchema = object({
22164
- section: string().optional(),
22166
+ section: NcStateSummarySectionIdSchema.optional(),
22165
22167
  /** The section's name on a per-section row; every reported section's, joined, on a combined one. */
22166
22168
  title: string(),
22167
22169
  text: string(),
@@ -22679,7 +22681,7 @@ var NC_CONDITION_CATALOG = [
22679
22681
  operator: "in",
22680
22682
  appliesTo: ["immediate", "track-end"],
22681
22683
  phase: "P1",
22682
- description: "Only recognised people. Selecting NOBODY means every identity in the face gallery; selecting some narrows to those. Matched on the gallery id, so renaming a person does not break the rule. On immediate delivery the name is not known yet when the notification is enqueued — see the rule’s \"wait for recognition\" switch."
22684
+ description: "Only recognised people. Selecting NOBODY means every identity in the face gallery; selecting some narrows to those. Matched on the gallery id, so renaming a person does not break the rule. On Immediate delivery the name must already be known when an event is written — choose Immediate, grouped to wait for it (D692)."
22683
22685
  },
22684
22686
  {
22685
22687
  id: "plates",
@@ -25402,6 +25404,7 @@ var KeyEventSchema = object({
25402
25404
  bestEventId: string(),
25403
25405
  /** Track lifetime in ms (lastSeen - firstSeen). */
25404
25406
  windowMs: number().optional(),
25407
+ audioLabels: array(TrackAudioLabelSchema).readonly().optional(),
25405
25408
  ...TrackFlagFields,
25406
25409
  ...TrackRetrainFields
25407
25410
  });
@@ -39338,28 +39341,29 @@ setOverlay: method(object({
39338
39341
  *
39339
39342
  * This cap owns the other half: a per-(camera, slot) BINDING that says
39340
39343
  * which value feeds the slot, how it is formatted, and under which
39341
- * conditions it is shown at all. One addon renders every binding on every
39342
- * camera, so a new source costs zero driver code.
39343
- *
39344
- * Three deliberate choices, each with a rejected alternative:
39345
- *
39346
- * 1. A source is `(capName, valuePath)` over the kernel's device
39347
- * runtime-state mirror — NOT a closed enum of source kinds. Every
39348
- * cap-keyed slice a device publishes is bindable the day the cap
39349
- * ships. The rejected alternative (one enum member per source, with
39350
- * a resolver branch each) is what makes "add the humidity too" a
39351
- * code change.
39352
- * 2. The display gate reuses `NcConditionsSchema` verbatim — the
39353
- * notification centre's condition vocabulary — rather than a parallel
39354
- * model. An operator who has learned one condition editor has learned
39355
- * both.
39356
- * 3. Because the renderer's facts are device STATE and not a detection
39357
- * record, only a SUBSET of that vocabulary can be answered here.
39358
- * `setSlotBinding` REJECTS the rest at write time (see
39359
- * `getConditionSupport`). It does not accept-then-fail-closed: a
39360
- * condition that can never be true renders a permanently blank
39361
- * overlay, and a blank overlay looks exactly like a broken camera.
39344
+ * conditions it is shown at all. Named TEMPLATES combine many device
39345
+ * fields into one overlay string (Scrypted OSD Manager parity).
39362
39346
  */
39347
+ /** One field reference inside a named multi-device template. */
39348
+ var OsdTemplateFieldSchema = object({
39349
+ deviceId: number().int(),
39350
+ capName: string().min(1).max(64),
39351
+ valuePath: string().min(1).max(64),
39352
+ /** Convert TO this unit at render (`tryConvertUnit`). Absent = source unit. */
39353
+ displayUnit: string().max(16).optional(),
39354
+ maxDecimals: number().int().min(0).max(4).default(1)
39355
+ });
39356
+ /**
39357
+ * A named template: several device fields → one parser string with
39358
+ * `{deviceId.capName.valuePath}` tokens (Scrypted `{deviceId.sensorId}`).
39359
+ */
39360
+ var OsdNamedTemplateSchema = object({
39361
+ id: string().min(1).max(64),
39362
+ name: string().min(1).max(64),
39363
+ fields: array(OsdTemplateFieldSchema).max(32).default([]),
39364
+ /** Parser with multi-token placeholders, e.g. `T {12.thermometer.value}°`. */
39365
+ parser: string().max(256).default("")
39366
+ });
39363
39367
  /** Where a slot's value comes from. */
39364
39368
  var OsdSourceSchema = discriminatedUnion("kind", [
39365
39369
  object({
@@ -39378,11 +39382,17 @@ var OsdSourceSchema = discriminatedUnion("kind", [
39378
39382
  deviceId: number().int().optional(),
39379
39383
  capName: string().min(1).max(64),
39380
39384
  /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
39381
- valuePath: string().min(1).max(64)
39385
+ valuePath: string().min(1).max(64),
39386
+ /** Convert TO this unit at render. Absent = no conversion. */
39387
+ displayUnit: string().max(16).optional()
39382
39388
  }),
39383
39389
  object({
39384
39390
  kind: literal("latest-recognition"),
39385
39391
  recognition: _enum(["person", "plate"])
39392
+ }),
39393
+ object({
39394
+ kind: literal("template"),
39395
+ templateId: string().min(1).max(64)
39386
39396
  })
39387
39397
  ]);
39388
39398
  var OsdSlotBindingSchema = object({
@@ -39398,7 +39408,10 @@ var OsdSlotBindingSchema = object({
39398
39408
  * documented workaround for firmwares that reject `.` in overlay text.
39399
39409
  */
39400
39410
  maxDecimals: number().int().min(0).max(4).default(1),
39401
- /** Appended via `${unit}`. The state mirror does not carry units. */
39411
+ /**
39412
+ * Appended via `${unit}` when no unit conversion produces a label.
39413
+ * Prefer `source.displayUnit` for converted device-state values.
39414
+ */
39402
39415
  unitLabel: string().max(8).optional(),
39403
39416
  /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
39404
39417
  valueMap: record(string(), string()).optional(),
@@ -39552,6 +39565,21 @@ var osdManagerCapability = {
39552
39565
  renderDevice: method(object({ deviceId: number().int() }), object({ results: array(OsdRenderResultSchema) }), {
39553
39566
  kind: "mutation",
39554
39567
  auth: "admin"
39568
+ }),
39569
+ /** Every named multi-device template. */
39570
+ listTemplates: method(object({}), object({ templates: array(OsdNamedTemplateSchema) }), { auth: "admin" }),
39571
+ /**
39572
+ * Create or replace a named template by `id`. Returns the stored record
39573
+ * after Zod normalisation.
39574
+ */
39575
+ upsertTemplate: method(object({ template: OsdNamedTemplateSchema }), object({ template: OsdNamedTemplateSchema }), {
39576
+ kind: "mutation",
39577
+ auth: "admin"
39578
+ }),
39579
+ /** Drop a named template. Bindings that still reference it render unreadable. */
39580
+ deleteTemplate: method(object({ templateId: string().min(1).max(64) }), object({ success: literal(true) }), {
39581
+ kind: "mutation",
39582
+ auth: "admin"
39555
39583
  })
39556
39584
  }
39557
39585
  };
@@ -45303,6 +45331,266 @@ var zoneRulesCapability = {
45303
45331
  durability: "restored"
45304
45332
  };
45305
45333
  /**
45334
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
45335
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
45336
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
45337
+ * decimals). K derives from `°C = K − 273.15`.
45338
+ */
45339
+ var UNIT_TABLE = {
45340
+ "°C": {
45341
+ dimension: "temperature",
45342
+ factor: 1,
45343
+ offset: 0
45344
+ },
45345
+ "°F": {
45346
+ dimension: "temperature",
45347
+ factor: 5 / 9,
45348
+ offset: -160 / 9
45349
+ },
45350
+ K: {
45351
+ dimension: "temperature",
45352
+ factor: 1,
45353
+ offset: -273.15
45354
+ },
45355
+ hPa: {
45356
+ dimension: "pressure",
45357
+ factor: 1,
45358
+ offset: 0
45359
+ },
45360
+ kPa: {
45361
+ dimension: "pressure",
45362
+ factor: 10,
45363
+ offset: 0
45364
+ },
45365
+ Pa: {
45366
+ dimension: "pressure",
45367
+ factor: .01,
45368
+ offset: 0
45369
+ },
45370
+ mbar: {
45371
+ dimension: "pressure",
45372
+ factor: 1,
45373
+ offset: 0
45374
+ },
45375
+ bar: {
45376
+ dimension: "pressure",
45377
+ factor: 1e3,
45378
+ offset: 0
45379
+ },
45380
+ inHg: {
45381
+ dimension: "pressure",
45382
+ factor: 33.8639,
45383
+ offset: 0
45384
+ },
45385
+ mmHg: {
45386
+ dimension: "pressure",
45387
+ factor: 1.33322,
45388
+ offset: 0
45389
+ },
45390
+ psi: {
45391
+ dimension: "pressure",
45392
+ factor: 68.9476,
45393
+ offset: 0
45394
+ },
45395
+ "m/s": {
45396
+ dimension: "speed",
45397
+ factor: 1,
45398
+ offset: 0
45399
+ },
45400
+ "km/h": {
45401
+ dimension: "speed",
45402
+ factor: 1 / 3.6,
45403
+ offset: 0
45404
+ },
45405
+ mph: {
45406
+ dimension: "speed",
45407
+ factor: .44704,
45408
+ offset: 0
45409
+ },
45410
+ kn: {
45411
+ dimension: "speed",
45412
+ factor: .514444,
45413
+ offset: 0
45414
+ },
45415
+ "mm/h": {
45416
+ dimension: "precipitation-rate",
45417
+ factor: 1,
45418
+ offset: 0
45419
+ },
45420
+ "in/h": {
45421
+ dimension: "precipitation-rate",
45422
+ factor: 25.4,
45423
+ offset: 0
45424
+ },
45425
+ m: {
45426
+ dimension: "length",
45427
+ factor: 1,
45428
+ offset: 0
45429
+ },
45430
+ mm: {
45431
+ dimension: "length",
45432
+ factor: .001,
45433
+ offset: 0
45434
+ },
45435
+ cm: {
45436
+ dimension: "length",
45437
+ factor: .01,
45438
+ offset: 0
45439
+ },
45440
+ km: {
45441
+ dimension: "length",
45442
+ factor: 1e3,
45443
+ offset: 0
45444
+ },
45445
+ in: {
45446
+ dimension: "length",
45447
+ factor: .0254,
45448
+ offset: 0
45449
+ },
45450
+ ft: {
45451
+ dimension: "length",
45452
+ factor: .3048,
45453
+ offset: 0
45454
+ },
45455
+ mi: {
45456
+ dimension: "length",
45457
+ factor: 1609.344,
45458
+ offset: 0
45459
+ },
45460
+ lx: {
45461
+ dimension: "illuminance",
45462
+ factor: 1,
45463
+ offset: 0
45464
+ },
45465
+ "W/m²": {
45466
+ dimension: "irradiance",
45467
+ factor: 1,
45468
+ offset: 0
45469
+ },
45470
+ W: {
45471
+ dimension: "power",
45472
+ factor: 1,
45473
+ offset: 0
45474
+ },
45475
+ kW: {
45476
+ dimension: "power",
45477
+ factor: 1e3,
45478
+ offset: 0
45479
+ },
45480
+ VA: {
45481
+ dimension: "apparent-power",
45482
+ factor: 1,
45483
+ offset: 0
45484
+ },
45485
+ Wh: {
45486
+ dimension: "energy",
45487
+ factor: 1,
45488
+ offset: 0
45489
+ },
45490
+ kWh: {
45491
+ dimension: "energy",
45492
+ factor: 1e3,
45493
+ offset: 0
45494
+ },
45495
+ MWh: {
45496
+ dimension: "energy",
45497
+ factor: 1e6,
45498
+ offset: 0
45499
+ },
45500
+ V: {
45501
+ dimension: "voltage",
45502
+ factor: 1,
45503
+ offset: 0
45504
+ },
45505
+ mV: {
45506
+ dimension: "voltage",
45507
+ factor: .001,
45508
+ offset: 0
45509
+ },
45510
+ A: {
45511
+ dimension: "current",
45512
+ factor: 1,
45513
+ offset: 0
45514
+ },
45515
+ mA: {
45516
+ dimension: "current",
45517
+ factor: .001,
45518
+ offset: 0
45519
+ },
45520
+ ppm: {
45521
+ dimension: "concentration-volume",
45522
+ factor: 1,
45523
+ offset: 0
45524
+ },
45525
+ ppb: {
45526
+ dimension: "concentration-volume",
45527
+ factor: .001,
45528
+ offset: 0
45529
+ },
45530
+ "µg/m³": {
45531
+ dimension: "concentration-mass",
45532
+ factor: 1,
45533
+ offset: 0
45534
+ },
45535
+ "mg/m³": {
45536
+ dimension: "concentration-mass",
45537
+ factor: 1e3,
45538
+ offset: 0
45539
+ },
45540
+ kg: {
45541
+ dimension: "mass",
45542
+ factor: 1,
45543
+ offset: 0
45544
+ },
45545
+ g: {
45546
+ dimension: "mass",
45547
+ factor: .001,
45548
+ offset: 0
45549
+ },
45550
+ lb: {
45551
+ dimension: "mass",
45552
+ factor: .453592,
45553
+ offset: 0
45554
+ },
45555
+ oz: {
45556
+ dimension: "mass",
45557
+ factor: .0283495,
45558
+ offset: 0
45559
+ },
45560
+ "%": {
45561
+ dimension: "percentage",
45562
+ factor: 1,
45563
+ offset: 0
45564
+ }
45565
+ };
45566
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
45567
+ function specFor(unit) {
45568
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
45569
+ }
45570
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
45571
+ function toCanonical(value, spec) {
45572
+ return value * spec.factor + spec.offset;
45573
+ }
45574
+ /** Canonical-unit value → the unit described by `spec`. */
45575
+ function fromCanonical(canonical, spec) {
45576
+ return (canonical - spec.offset) / spec.factor;
45577
+ }
45578
+ /**
45579
+ * Graceful variant for the render path: returns the converted value, or
45580
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
45581
+ * Never throws — a render caller falls back to the raw value + source unit.
45582
+ */
45583
+ function tryConvertUnit(value, from, to) {
45584
+ if (from === to) return value;
45585
+ const fromSpec = specFor(from);
45586
+ if (!fromSpec) return void 0;
45587
+ const toSpec = specFor(to);
45588
+ if (!toSpec) return void 0;
45589
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
45590
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
45591
+ }
45592
+ SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
45593
+ /**
45306
45594
  * Most specific first. Extending this list is how a new device kind becomes
45307
45595
  * gateable; nothing else needs to change.
45308
45596
  */
@@ -50669,6 +50957,12 @@ Object.freeze({
50669
50957
  addonId: null,
50670
50958
  access: "create"
50671
50959
  },
50960
+ "osdManager.deleteTemplate": {
50961
+ capName: "osd-manager",
50962
+ capScope: "system",
50963
+ addonId: null,
50964
+ access: "delete"
50965
+ },
50672
50966
  "osdManager.getConditionSupport": {
50673
50967
  capName: "osd-manager",
50674
50968
  capScope: "system",
@@ -50687,6 +50981,12 @@ Object.freeze({
50687
50981
  addonId: null,
50688
50982
  access: "view"
50689
50983
  },
50984
+ "osdManager.listTemplates": {
50985
+ capName: "osd-manager",
50986
+ capScope: "system",
50987
+ addonId: null,
50988
+ access: "view"
50989
+ },
50690
50990
  "osdManager.previewSlot": {
50691
50991
  capName: "osd-manager",
50692
50992
  capScope: "system",
@@ -50705,6 +51005,12 @@ Object.freeze({
50705
51005
  addonId: null,
50706
51006
  access: "create"
50707
51007
  },
51008
+ "osdManager.upsertTemplate": {
51009
+ capName: "osd-manager",
51010
+ capScope: "system",
51011
+ addonId: null,
51012
+ access: "create"
51013
+ },
50708
51014
  "petFeeder.callPet": {
50709
51015
  capName: "pet-feeder",
50710
51016
  capScope: "device",
@@ -54091,6 +54397,11 @@ Object.freeze({
54091
54397
  }
54092
54398
  });
54093
54399
  Object.freeze({
54400
+ "accessories.getStatus": [{
54401
+ name: "deviceId",
54402
+ form: "single",
54403
+ optional: false
54404
+ }],
54094
54405
  "accessories.setChildHidden": [{
54095
54406
  name: "childDeviceId",
54096
54407
  form: "single",
@@ -54110,6 +54421,11 @@ Object.freeze({
54110
54421
  form: "single",
54111
54422
  optional: false
54112
54423
  }],
54424
+ "airQualitySensor.getStatus": [{
54425
+ name: "deviceId",
54426
+ form: "single",
54427
+ optional: false
54428
+ }],
54113
54429
  "alarmPanel.arm": [{
54114
54430
  name: "deviceId",
54115
54431
  form: "single",
@@ -54120,11 +54436,36 @@ Object.freeze({
54120
54436
  form: "single",
54121
54437
  optional: false
54122
54438
  }],
54439
+ "alarmPanel.getStatus": [{
54440
+ name: "deviceId",
54441
+ form: "single",
54442
+ optional: false
54443
+ }],
54123
54444
  "alarmPanel.trigger": [{
54124
54445
  name: "deviceId",
54125
54446
  form: "single",
54126
54447
  optional: false
54127
54448
  }],
54449
+ "ambientLightSensor.getStatus": [{
54450
+ name: "deviceId",
54451
+ form: "single",
54452
+ optional: false
54453
+ }],
54454
+ "audioAnalysis.applyDeviceSettingsPatch": [{
54455
+ name: "deviceId",
54456
+ form: "single",
54457
+ optional: false
54458
+ }],
54459
+ "audioAnalysis.getDeviceLiveContribution": [{
54460
+ name: "deviceId",
54461
+ form: "single",
54462
+ optional: false
54463
+ }],
54464
+ "audioAnalysis.getDeviceSettingsContribution": [{
54465
+ name: "deviceId",
54466
+ form: "single",
54467
+ optional: false
54468
+ }],
54128
54469
  "audioAnalysis.resolveDeviceSettings": [{
54129
54470
  name: "deviceId",
54130
54471
  form: "single",
@@ -54165,16 +54506,36 @@ Object.freeze({
54165
54506
  form: "single",
54166
54507
  optional: false
54167
54508
  }],
54509
+ "automationControl.getStatus": [{
54510
+ name: "deviceId",
54511
+ form: "single",
54512
+ optional: false
54513
+ }],
54168
54514
  "automationControl.trigger": [{
54169
54515
  name: "deviceId",
54170
54516
  form: "single",
54171
54517
  optional: false
54172
54518
  }],
54519
+ "battery.getStatus": [{
54520
+ name: "deviceId",
54521
+ form: "single",
54522
+ optional: false
54523
+ }],
54173
54524
  "battery.wakeForStream": [{
54174
54525
  name: "deviceId",
54175
54526
  form: "single",
54176
54527
  optional: false
54177
54528
  }],
54529
+ "binary.getStatus": [{
54530
+ name: "deviceId",
54531
+ form: "single",
54532
+ optional: false
54533
+ }],
54534
+ "brightness.getStatus": [{
54535
+ name: "deviceId",
54536
+ form: "single",
54537
+ optional: false
54538
+ }],
54178
54539
  "brightness.setBrightness": [{
54179
54540
  name: "deviceId",
54180
54541
  form: "single",
@@ -54190,6 +54551,26 @@ Object.freeze({
54190
54551
  form: "single",
54191
54552
  optional: false
54192
54553
  }],
54554
+ "cameraCredentials.getStatus": [{
54555
+ name: "deviceId",
54556
+ form: "single",
54557
+ optional: false
54558
+ }],
54559
+ "cameraGridLayout.applyDeviceSettingsPatch": [{
54560
+ name: "deviceId",
54561
+ form: "single",
54562
+ optional: false
54563
+ }],
54564
+ "cameraGridLayout.getDeviceLiveContribution": [{
54565
+ name: "deviceId",
54566
+ form: "single",
54567
+ optional: false
54568
+ }],
54569
+ "cameraGridLayout.getDeviceSettingsContribution": [{
54570
+ name: "deviceId",
54571
+ form: "single",
54572
+ optional: false
54573
+ }],
54193
54574
  "cameraGridLayout.getLayout": [{
54194
54575
  name: "deviceId",
54195
54576
  form: "single",
@@ -54205,6 +54586,21 @@ Object.freeze({
54205
54586
  form: "single",
54206
54587
  optional: false
54207
54588
  }],
54589
+ "cameraPipelineConfig.applyDeviceSettingsPatch": [{
54590
+ name: "deviceId",
54591
+ form: "single",
54592
+ optional: false
54593
+ }],
54594
+ "cameraPipelineConfig.getDeviceLiveContribution": [{
54595
+ name: "deviceId",
54596
+ form: "single",
54597
+ optional: false
54598
+ }],
54599
+ "cameraPipelineConfig.getDeviceSettingsContribution": [{
54600
+ name: "deviceId",
54601
+ form: "single",
54602
+ optional: false
54603
+ }],
54208
54604
  "cameraStreams.getBrokerStreams": [{
54209
54605
  name: "deviceId",
54210
54606
  form: "single",
@@ -54230,6 +54626,16 @@ Object.freeze({
54230
54626
  form: "single",
54231
54627
  optional: false
54232
54628
  }],
54629
+ "carbonMonoxide.getStatus": [{
54630
+ name: "deviceId",
54631
+ form: "single",
54632
+ optional: false
54633
+ }],
54634
+ "climateControl.getStatus": [{
54635
+ name: "deviceId",
54636
+ form: "single",
54637
+ optional: false
54638
+ }],
54233
54639
  "climateControl.setFanMode": [{
54234
54640
  name: "deviceId",
54235
54641
  form: "single",
@@ -54270,16 +54676,41 @@ Object.freeze({
54270
54676
  form: "single",
54271
54677
  optional: false
54272
54678
  }],
54679
+ "color.getStatus": [{
54680
+ name: "deviceId",
54681
+ form: "single",
54682
+ optional: false
54683
+ }],
54273
54684
  "color.setColor": [{
54274
54685
  name: "deviceId",
54275
54686
  form: "single",
54276
54687
  optional: false
54277
54688
  }],
54689
+ "connectivity.getStatus": [{
54690
+ name: "deviceId",
54691
+ form: "single",
54692
+ optional: false
54693
+ }],
54694
+ "consumables.getStatus": [{
54695
+ name: "deviceId",
54696
+ form: "single",
54697
+ optional: false
54698
+ }],
54278
54699
  "consumables.reset": [{
54279
54700
  name: "deviceId",
54280
54701
  form: "single",
54281
54702
  optional: false
54282
54703
  }],
54704
+ "contact.getStatus": [{
54705
+ name: "deviceId",
54706
+ form: "single",
54707
+ optional: false
54708
+ }],
54709
+ "control.getStatus": [{
54710
+ name: "deviceId",
54711
+ form: "single",
54712
+ optional: false
54713
+ }],
54283
54714
  "control.setValue": [{
54284
54715
  name: "deviceId",
54285
54716
  form: "single",
@@ -54290,6 +54721,11 @@ Object.freeze({
54290
54721
  form: "single",
54291
54722
  optional: false
54292
54723
  }],
54724
+ "cover.getStatus": [{
54725
+ name: "deviceId",
54726
+ form: "single",
54727
+ optional: false
54728
+ }],
54293
54729
  "cover.open": [{
54294
54730
  name: "deviceId",
54295
54731
  form: "single",
@@ -54315,6 +54751,11 @@ Object.freeze({
54315
54751
  form: "single",
54316
54752
  optional: false
54317
54753
  }],
54754
+ "dayNight.getStatus": [{
54755
+ name: "deviceId",
54756
+ form: "single",
54757
+ optional: false
54758
+ }],
54318
54759
  "dayNight.setSettings": [{
54319
54760
  name: "deviceId",
54320
54761
  form: "single",
@@ -54325,11 +54766,31 @@ Object.freeze({
54325
54766
  form: "single",
54326
54767
  optional: true
54327
54768
  }],
54769
+ "detectionPipeline.applyDeviceSettingsPatch": [{
54770
+ name: "deviceId",
54771
+ form: "single",
54772
+ optional: false
54773
+ }],
54774
+ "detectionPipeline.getDeviceLiveContribution": [{
54775
+ name: "deviceId",
54776
+ form: "single",
54777
+ optional: false
54778
+ }],
54779
+ "detectionPipeline.getDeviceSettingsContribution": [{
54780
+ name: "deviceId",
54781
+ form: "single",
54782
+ optional: false
54783
+ }],
54328
54784
  "deviceAdminLink.getAdminLink": [{
54329
54785
  name: "deviceId",
54330
54786
  form: "single",
54331
54787
  optional: false
54332
54788
  }],
54789
+ "deviceAdoption.getStatus": [{
54790
+ name: "deviceId",
54791
+ form: "single",
54792
+ optional: false
54793
+ }],
54333
54794
  "deviceAdoption.release": [{
54334
54795
  name: "camDeviceId",
54335
54796
  form: "single",
@@ -54345,6 +54806,11 @@ Object.freeze({
54345
54806
  form: "single",
54346
54807
  optional: false
54347
54808
  }],
54809
+ "deviceDiscovery.getStatus": [{
54810
+ name: "deviceId",
54811
+ form: "single",
54812
+ optional: false
54813
+ }],
54348
54814
  "deviceDiscovery.listDiscovered": [{
54349
54815
  name: "deviceId",
54350
54816
  form: "single",
@@ -54364,6 +54830,21 @@ Object.freeze({
54364
54830
  form: "single",
54365
54831
  optional: false
54366
54832
  }],
54833
+ "deviceExport.applyDeviceSettingsPatch": [{
54834
+ name: "deviceId",
54835
+ form: "single",
54836
+ optional: false
54837
+ }],
54838
+ "deviceExport.getDeviceLiveContribution": [{
54839
+ name: "deviceId",
54840
+ form: "single",
54841
+ optional: false
54842
+ }],
54843
+ "deviceExport.getDeviceSettingsContribution": [{
54844
+ name: "deviceId",
54845
+ form: "single",
54846
+ optional: false
54847
+ }],
54367
54848
  "deviceExtension.listCandidates": [{
54368
54849
  name: "deviceId",
54369
54850
  form: "single",
@@ -54712,6 +55193,26 @@ Object.freeze({
54712
55193
  form: "single",
54713
55194
  optional: false
54714
55195
  }],
55196
+ "deviceStatus.getStatus": [{
55197
+ name: "deviceId",
55198
+ form: "single",
55199
+ optional: false
55200
+ }],
55201
+ "doorbell.getStatus": [{
55202
+ name: "deviceId",
55203
+ form: "single",
55204
+ optional: false
55205
+ }],
55206
+ "enumSensor.getStatus": [{
55207
+ name: "deviceId",
55208
+ form: "single",
55209
+ optional: false
55210
+ }],
55211
+ "eventEmitter.getStatus": [{
55212
+ name: "deviceId",
55213
+ form: "single",
55214
+ optional: false
55215
+ }],
54715
55216
  "faceGallery.getFaceByTrack": [{
54716
55217
  name: "deviceId",
54717
55218
  form: "single",
@@ -54726,6 +55227,11 @@ Object.freeze({
54726
55227
  form: "array",
54727
55228
  optional: true
54728
55229
  }],
55230
+ "fanControl.getStatus": [{
55231
+ name: "deviceId",
55232
+ form: "single",
55233
+ optional: false
55234
+ }],
54729
55235
  "fanControl.setDirection": [{
54730
55236
  name: "deviceId",
54731
55237
  form: "single",
@@ -54746,6 +55252,26 @@ Object.freeze({
54746
55252
  form: "single",
54747
55253
  optional: false
54748
55254
  }],
55255
+ "featureProbe.getStatus": [{
55256
+ name: "deviceId",
55257
+ form: "single",
55258
+ optional: false
55259
+ }],
55260
+ "flood.getStatus": [{
55261
+ name: "deviceId",
55262
+ form: "single",
55263
+ optional: false
55264
+ }],
55265
+ "gas.getStatus": [{
55266
+ name: "deviceId",
55267
+ form: "single",
55268
+ optional: false
55269
+ }],
55270
+ "humidifier.getStatus": [{
55271
+ name: "deviceId",
55272
+ form: "single",
55273
+ optional: false
55274
+ }],
54749
55275
  "humidifier.setMode": [{
54750
55276
  name: "deviceId",
54751
55277
  form: "single",
@@ -54761,11 +55287,26 @@ Object.freeze({
54761
55287
  form: "single",
54762
55288
  optional: false
54763
55289
  }],
55290
+ "humiditySensor.getStatus": [{
55291
+ name: "deviceId",
55292
+ form: "single",
55293
+ optional: false
55294
+ }],
55295
+ "image.getStatus": [{
55296
+ name: "deviceId",
55297
+ form: "single",
55298
+ optional: false
55299
+ }],
54764
55300
  "imageSettings.getOptions": [{
54765
55301
  name: "deviceId",
54766
55302
  form: "single",
54767
55303
  optional: false
54768
55304
  }],
55305
+ "imageSettings.getStatus": [{
55306
+ name: "deviceId",
55307
+ form: "single",
55308
+ optional: false
55309
+ }],
54769
55310
  "imageSettings.setSettings": [{
54770
55311
  name: "deviceId",
54771
55312
  form: "single",
@@ -54776,6 +55317,11 @@ Object.freeze({
54776
55317
  form: "single",
54777
55318
  optional: false
54778
55319
  }],
55320
+ "intercom.getStatus": [{
55321
+ name: "deviceId",
55322
+ form: "single",
55323
+ optional: false
55324
+ }],
54779
55325
  "intercom.handleAnswer": [{
54780
55326
  name: "deviceId",
54781
55327
  form: "single",
@@ -54806,6 +55352,11 @@ Object.freeze({
54806
55352
  form: "single",
54807
55353
  optional: false
54808
55354
  }],
55355
+ "lawnMowerControl.getStatus": [{
55356
+ name: "deviceId",
55357
+ form: "single",
55358
+ optional: false
55359
+ }],
54809
55360
  "lawnMowerControl.pause": [{
54810
55361
  name: "deviceId",
54811
55362
  form: "single",
@@ -54816,6 +55367,11 @@ Object.freeze({
54816
55367
  form: "single",
54817
55368
  optional: false
54818
55369
  }],
55370
+ "lockControl.getStatus": [{
55371
+ name: "deviceId",
55372
+ form: "single",
55373
+ optional: false
55374
+ }],
54819
55375
  "lockControl.lock": [{
54820
55376
  name: "deviceId",
54821
55377
  form: "single",
@@ -54831,6 +55387,11 @@ Object.freeze({
54831
55387
  form: "single",
54832
55388
  optional: false
54833
55389
  }],
55390
+ "mediaPlayer.getStatus": [{
55391
+ name: "deviceId",
55392
+ form: "single",
55393
+ optional: false
55394
+ }],
54834
55395
  "mediaPlayer.next": [{
54835
55396
  name: "deviceId",
54836
55397
  form: "single",
@@ -54891,6 +55452,11 @@ Object.freeze({
54891
55452
  form: "single",
54892
55453
  optional: false
54893
55454
  }],
55455
+ "motion.getStatus": [{
55456
+ name: "deviceId",
55457
+ form: "single",
55458
+ optional: false
55459
+ }],
54894
55460
  "motion.isDetected": [{
54895
55461
  name: "deviceId",
54896
55462
  form: "single",
@@ -54901,11 +55467,31 @@ Object.freeze({
54901
55467
  form: "single",
54902
55468
  optional: false
54903
55469
  }],
55470
+ "motionDetection.applyDeviceSettingsPatch": [{
55471
+ name: "deviceId",
55472
+ form: "single",
55473
+ optional: false
55474
+ }],
55475
+ "motionDetection.getDeviceLiveContribution": [{
55476
+ name: "deviceId",
55477
+ form: "single",
55478
+ optional: false
55479
+ }],
55480
+ "motionDetection.getDeviceSettingsContribution": [{
55481
+ name: "deviceId",
55482
+ form: "single",
55483
+ optional: false
55484
+ }],
54904
55485
  "motionDetection.removeCamera": [{
54905
55486
  name: "deviceId",
54906
55487
  form: "single",
54907
55488
  optional: false
54908
55489
  }],
55490
+ "motionTrigger.getStatus": [{
55491
+ name: "deviceId",
55492
+ form: "single",
55493
+ optional: false
55494
+ }],
54909
55495
  "motionTrigger.setMotionTrigger": [{
54910
55496
  name: "deviceId",
54911
55497
  form: "single",
@@ -54916,6 +55502,11 @@ Object.freeze({
54916
55502
  form: "single",
54917
55503
  optional: false
54918
55504
  }],
55505
+ "motionZones.getStatus": [{
55506
+ name: "deviceId",
55507
+ form: "single",
55508
+ optional: false
55509
+ }],
54919
55510
  "motionZones.setZone": [{
54920
55511
  name: "deviceId",
54921
55512
  form: "single",
@@ -54926,6 +55517,11 @@ Object.freeze({
54926
55517
  form: "single",
54927
55518
  optional: false
54928
55519
  }],
55520
+ "nativeObjectDetection.getStatus": [{
55521
+ name: "deviceId",
55522
+ form: "single",
55523
+ optional: false
55524
+ }],
54929
55525
  "nativeObjectDetection.setEnabled": [{
54930
55526
  name: "deviceId",
54931
55527
  form: "single",
@@ -54941,6 +55537,11 @@ Object.freeze({
54941
55537
  form: "single",
54942
55538
  optional: false
54943
55539
  }],
55540
+ "navigation.getStatus": [{
55541
+ name: "deviceId",
55542
+ form: "single",
55543
+ optional: false
55544
+ }],
54944
55545
  "navigation.goToPoint": [{
54945
55546
  name: "deviceId",
54946
55547
  form: "single",
@@ -54986,6 +55587,11 @@ Object.freeze({
54986
55587
  form: "single",
54987
55588
  optional: false
54988
55589
  }],
55590
+ "networkLink.getStatus": [{
55591
+ name: "deviceId",
55592
+ form: "single",
55593
+ optional: false
55594
+ }],
54989
55595
  "networkQuality.getDeviceStats": [{
54990
55596
  name: "deviceId",
54991
55597
  form: "single",
@@ -55011,11 +55617,26 @@ Object.freeze({
55011
55617
  form: "single",
55012
55618
  optional: false
55013
55619
  }],
55620
+ "notifier.getStatus": [{
55621
+ name: "deviceId",
55622
+ form: "single",
55623
+ optional: false
55624
+ }],
55014
55625
  "notifier.send": [{
55015
55626
  name: "deviceId",
55016
55627
  form: "single",
55017
55628
  optional: false
55018
55629
  }],
55630
+ "numericSensor.getStatus": [{
55631
+ name: "deviceId",
55632
+ form: "single",
55633
+ optional: false
55634
+ }],
55635
+ "osd.getStatus": [{
55636
+ name: "deviceId",
55637
+ form: "single",
55638
+ optional: false
55639
+ }],
55019
55640
  "osd.setOverlay": [{
55020
55641
  name: "deviceId",
55021
55642
  form: "single",
@@ -55075,6 +55696,11 @@ Object.freeze({
55075
55696
  form: "single",
55076
55697
  optional: false
55077
55698
  }],
55699
+ "petFeeder.getStatus": [{
55700
+ name: "deviceId",
55701
+ form: "single",
55702
+ optional: false
55703
+ }],
55078
55704
  "petFeeder.markFoodReplenished": [{
55079
55705
  name: "deviceId",
55080
55706
  form: "single",
@@ -55110,6 +55736,11 @@ Object.freeze({
55110
55736
  form: "single",
55111
55737
  optional: false
55112
55738
  }],
55739
+ "pipelineAnalytics.applyDeviceSettingsPatch": [{
55740
+ name: "deviceId",
55741
+ form: "single",
55742
+ optional: false
55743
+ }],
55113
55744
  "pipelineAnalytics.clearTrackRelation": [{
55114
55745
  name: "deviceId",
55115
55746
  form: "single",
@@ -55150,6 +55781,16 @@ Object.freeze({
55150
55781
  form: "single",
55151
55782
  optional: false
55152
55783
  }],
55784
+ "pipelineAnalytics.getDeviceLiveContribution": [{
55785
+ name: "deviceId",
55786
+ form: "single",
55787
+ optional: false
55788
+ }],
55789
+ "pipelineAnalytics.getDeviceSettingsContribution": [{
55790
+ name: "deviceId",
55791
+ form: "single",
55792
+ optional: false
55793
+ }],
55153
55794
  "pipelineAnalytics.getEventDensity": [{
55154
55795
  name: "deviceId",
55155
55796
  form: "single",
@@ -55365,6 +56006,11 @@ Object.freeze({
55365
56006
  form: "single",
55366
56007
  optional: true
55367
56008
  }],
56009
+ "pipelineOrchestrator.applyDeviceSettingsPatch": [{
56010
+ name: "deviceId",
56011
+ form: "single",
56012
+ optional: false
56013
+ }],
55368
56014
  "pipelineOrchestrator.assignAudio": [{
55369
56015
  name: "deviceId",
55370
56016
  form: "single",
@@ -55410,6 +56056,16 @@ Object.freeze({
55410
56056
  form: "single",
55411
56057
  optional: false
55412
56058
  }],
56059
+ "pipelineOrchestrator.getDeviceLiveContribution": [{
56060
+ name: "deviceId",
56061
+ form: "single",
56062
+ optional: false
56063
+ }],
56064
+ "pipelineOrchestrator.getDeviceSettingsContribution": [{
56065
+ name: "deviceId",
56066
+ form: "single",
56067
+ optional: false
56068
+ }],
55413
56069
  "pipelineOrchestrator.getPipelineAssignment": [{
55414
56070
  name: "deviceId",
55415
56071
  form: "single",
@@ -55530,11 +56186,31 @@ Object.freeze({
55530
56186
  form: "single",
55531
56187
  optional: true
55532
56188
  }],
56189
+ "powerMeter.getStatus": [{
56190
+ name: "deviceId",
56191
+ form: "single",
56192
+ optional: false
56193
+ }],
56194
+ "presence.getStatus": [{
56195
+ name: "deviceId",
56196
+ form: "single",
56197
+ optional: false
56198
+ }],
56199
+ "pressureSensor.getStatus": [{
56200
+ name: "deviceId",
56201
+ form: "single",
56202
+ optional: false
56203
+ }],
55533
56204
  "privacyMask.getOptions": [{
55534
56205
  name: "deviceId",
55535
56206
  form: "single",
55536
56207
  optional: false
55537
56208
  }],
56209
+ "privacyMask.getStatus": [{
56210
+ name: "deviceId",
56211
+ form: "single",
56212
+ optional: false
56213
+ }],
55538
56214
  "privacyMask.setAudioEnabled": [{
55539
56215
  name: "deviceId",
55540
56216
  form: "single",
@@ -55585,6 +56261,11 @@ Object.freeze({
55585
56261
  form: "single",
55586
56262
  optional: false
55587
56263
  }],
56264
+ "ptz.getStatus": [{
56265
+ name: "deviceId",
56266
+ form: "single",
56267
+ optional: false
56268
+ }],
55588
56269
  "ptz.goHome": [{
55589
56270
  name: "deviceId",
55590
56271
  form: "single",
@@ -55675,6 +56356,11 @@ Object.freeze({
55675
56356
  form: "single",
55676
56357
  optional: false
55677
56358
  }],
56359
+ "recordingArchive.applyDeviceSettingsPatch": [{
56360
+ name: "deviceId",
56361
+ form: "single",
56362
+ optional: false
56363
+ }],
55678
56364
  "recordingArchive.deleteFootprint": [{
55679
56365
  name: "deviceId",
55680
56366
  form: "single",
@@ -55695,6 +56381,21 @@ Object.freeze({
55695
56381
  form: "single",
55696
56382
  optional: false
55697
56383
  }],
56384
+ "recordingArchive.getDeviceLiveContribution": [{
56385
+ name: "deviceId",
56386
+ form: "single",
56387
+ optional: false
56388
+ }],
56389
+ "recordingArchive.getDeviceSettingsContribution": [{
56390
+ name: "deviceId",
56391
+ form: "single",
56392
+ optional: false
56393
+ }],
56394
+ "recordingArchive.getStatus": [{
56395
+ name: "deviceId",
56396
+ form: "single",
56397
+ optional: false
56398
+ }],
55698
56399
  "recordingArchive.listOpsLog": [{
55699
56400
  name: "deviceId",
55700
56401
  form: "single",
@@ -55780,6 +56481,11 @@ Object.freeze({
55780
56481
  form: "single",
55781
56482
  optional: false
55782
56483
  }],
56484
+ "recordingOnboard.getStatus": [{
56485
+ name: "deviceId",
56486
+ form: "single",
56487
+ optional: false
56488
+ }],
55783
56489
  "recordingOnboard.setSettings": [{
55784
56490
  name: "deviceId",
55785
56491
  form: "single",
@@ -55810,6 +56516,11 @@ Object.freeze({
55810
56516
  form: "single",
55811
56517
  optional: false
55812
56518
  }],
56519
+ "sceneMonitor.getStatus": [{
56520
+ name: "deviceId",
56521
+ form: "single",
56522
+ optional: false
56523
+ }],
55813
56524
  "sceneMonitor.listScenes": [{
55814
56525
  name: "deviceId",
55815
56526
  form: "single",
@@ -55835,6 +56546,11 @@ Object.freeze({
55835
56546
  form: "single",
55836
56547
  optional: false
55837
56548
  }],
56549
+ "scriptRunner.getStatus": [{
56550
+ name: "deviceId",
56551
+ form: "single",
56552
+ optional: false
56553
+ }],
55838
56554
  "scriptRunner.run": [{
55839
56555
  name: "deviceId",
55840
56556
  form: "single",
@@ -55845,11 +56561,31 @@ Object.freeze({
55845
56561
  form: "single",
55846
56562
  optional: false
55847
56563
  }],
56564
+ "smoke.getStatus": [{
56565
+ name: "deviceId",
56566
+ form: "single",
56567
+ optional: false
56568
+ }],
56569
+ "snapshot.applyDeviceSettingsPatch": [{
56570
+ name: "deviceId",
56571
+ form: "single",
56572
+ optional: false
56573
+ }],
55848
56574
  "snapshot.getDebugState": [{
55849
56575
  name: "deviceId",
55850
56576
  form: "single",
55851
56577
  optional: false
55852
56578
  }],
56579
+ "snapshot.getDeviceLiveContribution": [{
56580
+ name: "deviceId",
56581
+ form: "single",
56582
+ optional: false
56583
+ }],
56584
+ "snapshot.getDeviceSettingsContribution": [{
56585
+ name: "deviceId",
56586
+ form: "single",
56587
+ optional: false
56588
+ }],
55853
56589
  "snapshot.getSnapshot": [{
55854
56590
  name: "deviceId",
55855
56591
  form: "single",
@@ -55866,6 +56602,11 @@ Object.freeze({
55866
56602
  form: "array",
55867
56603
  optional: false
55868
56604
  }],
56605
+ "snapshot.getStatus": [{
56606
+ name: "deviceId",
56607
+ form: "single",
56608
+ optional: false
56609
+ }],
55869
56610
  "snapshot.invalidateCache": [{
55870
56611
  name: "deviceId",
55871
56612
  form: "single",
@@ -55876,6 +56617,11 @@ Object.freeze({
55876
56617
  form: "single",
55877
56618
  optional: false
55878
56619
  }],
56620
+ "streamBroker.applyDeviceSettingsPatch": [{
56621
+ name: "deviceId",
56622
+ form: "single",
56623
+ optional: false
56624
+ }],
55879
56625
  "streamBroker.assignProfile": [{
55880
56626
  name: "deviceId",
55881
56627
  form: "single",
@@ -55891,6 +56637,16 @@ Object.freeze({
55891
56637
  form: "single",
55892
56638
  optional: false
55893
56639
  }],
56640
+ "streamBroker.getDeviceLiveContribution": [{
56641
+ name: "deviceId",
56642
+ form: "single",
56643
+ optional: false
56644
+ }],
56645
+ "streamBroker.getDeviceSettingsContribution": [{
56646
+ name: "deviceId",
56647
+ form: "single",
56648
+ optional: false
56649
+ }],
55894
56650
  "streamBroker.getStreamWithCodec": [{
55895
56651
  name: "deviceId",
55896
56652
  form: "single",
@@ -55951,16 +56707,46 @@ Object.freeze({
55951
56707
  form: "single",
55952
56708
  optional: false
55953
56709
  }],
56710
+ "streamParams.getStatus": [{
56711
+ name: "deviceId",
56712
+ form: "single",
56713
+ optional: false
56714
+ }],
55954
56715
  "streamParams.setProfile": [{
55955
56716
  name: "deviceId",
55956
56717
  form: "single",
55957
56718
  optional: false
55958
56719
  }],
56720
+ "switch.getStatus": [{
56721
+ name: "deviceId",
56722
+ form: "single",
56723
+ optional: false
56724
+ }],
55959
56725
  "switch.setState": [{
55960
56726
  name: "deviceId",
55961
56727
  form: "single",
55962
56728
  optional: false
55963
56729
  }],
56730
+ "tamper.getStatus": [{
56731
+ name: "deviceId",
56732
+ form: "single",
56733
+ optional: false
56734
+ }],
56735
+ "temperatureSensor.getStatus": [{
56736
+ name: "deviceId",
56737
+ form: "single",
56738
+ optional: false
56739
+ }],
56740
+ "update.getStatus": [{
56741
+ name: "deviceId",
56742
+ form: "single",
56743
+ optional: false
56744
+ }],
56745
+ "vacuumControl.getStatus": [{
56746
+ name: "deviceId",
56747
+ form: "single",
56748
+ optional: false
56749
+ }],
55964
56750
  "vacuumControl.locate": [{
55965
56751
  name: "deviceId",
55966
56752
  form: "single",
@@ -55996,6 +56782,11 @@ Object.freeze({
55996
56782
  form: "single",
55997
56783
  optional: false
55998
56784
  }],
56785
+ "valve.getStatus": [{
56786
+ name: "deviceId",
56787
+ form: "single",
56788
+ optional: false
56789
+ }],
55999
56790
  "valve.open": [{
56000
56791
  name: "deviceId",
56001
56792
  form: "single",
@@ -56011,6 +56802,11 @@ Object.freeze({
56011
56802
  form: "single",
56012
56803
  optional: false
56013
56804
  }],
56805
+ "vibration.getStatus": [{
56806
+ name: "deviceId",
56807
+ form: "single",
56808
+ optional: false
56809
+ }],
56014
56810
  "videoclips.dialClipStream": [{
56015
56811
  name: "deviceId",
56016
56812
  form: "single",
@@ -56046,6 +56842,11 @@ Object.freeze({
56046
56842
  form: "single",
56047
56843
  optional: false
56048
56844
  }],
56845
+ "waterHeater.getStatus": [{
56846
+ name: "deviceId",
56847
+ form: "single",
56848
+ optional: false
56849
+ }],
56049
56850
  "waterHeater.setAway": [{
56050
56851
  name: "deviceId",
56051
56852
  form: "single",
@@ -56061,6 +56862,11 @@ Object.freeze({
56061
56862
  form: "single",
56062
56863
  optional: false
56063
56864
  }],
56865
+ "weather.getStatus": [{
56866
+ name: "deviceId",
56867
+ form: "single",
56868
+ optional: false
56869
+ }],
56064
56870
  "webrtcSession.addIceCandidate": [{
56065
56871
  name: "deviceId",
56066
56872
  form: "single",
@@ -57453,10 +58259,10 @@ function limitText(text, max) {
57453
58259
  return `${text.slice(0, max - 3)}...`;
57454
58260
  }
57455
58261
  /** Turn a raw state value into the string that goes into `${value}`. */
57456
- function displayValue(raw, binding) {
58262
+ function displayValue(raw, binding, maxDecimals = binding.maxDecimals) {
57457
58263
  const mapped = binding.valueMap?.[String(raw)];
57458
58264
  if (mapped !== void 0) return mapped;
57459
- if (typeof raw === "number") return truncateDecimals(raw, binding.maxDecimals);
58265
+ if (typeof raw === "number") return truncateDecimals(raw, maxDecimals);
57460
58266
  if (typeof raw === "boolean") return raw ? "ON" : "OFF";
57461
58267
  if (raw === null || raw === void 0) return "";
57462
58268
  if (typeof raw === "string") return raw;
@@ -57470,9 +58276,10 @@ function applyTemplate(template, value, unit) {
57470
58276
  return template.replaceAll("${value}", value).replaceAll("${unit}", unit);
57471
58277
  }
57472
58278
  /** The whole formatting chain: raw value → the text to burn into the slot. */
57473
- function formatSlotText(raw, binding) {
57474
- const value = displayValue(raw, binding);
57475
- return limitText(applyTemplate(binding.template, value, binding.unitLabel ?? "").trim(), binding.maxCharacters);
58279
+ function formatSlotText(raw, binding, options = {}) {
58280
+ const value = displayValue(options.displayRaw ?? raw, binding);
58281
+ const unit = options.unit ?? binding.unitLabel ?? "";
58282
+ return limitText(applyTemplate(binding.template, value, unit).trim(), binding.maxCharacters);
57476
58283
  }
57477
58284
  //#endregion
57478
58285
  //#region src/state-leaves.ts
@@ -57560,6 +58367,92 @@ function readStatePath(slice, path) {
57560
58367
  return current;
57561
58368
  }
57562
58369
  //#endregion
58370
+ //#region src/template-tokens.ts
58371
+ /** Build the placeholder token for one template field. */
58372
+ function fieldToken(field) {
58373
+ return `{${field.deviceId}.${field.capName}.${field.valuePath}}`;
58374
+ }
58375
+ /** Replace every occurrence of `token` in `parser` with `value`. */
58376
+ function replaceToken(parser, token, value) {
58377
+ return parser.replaceAll(token, value);
58378
+ }
58379
+ //#endregion
58380
+ //#region src/unit-display.ts
58381
+ /**
58382
+ * Convert a numeric state value for OSD display using `@camstack/types`
58383
+ * UNIT_TABLE / tryConvertUnit (same set as D699).
58384
+ */
58385
+ /**
58386
+ * Convert `raw` from `sourceUnit` to `displayUnit` when both are known and
58387
+ * same-dimension. On refusal, keep the raw value and the source unit so the
58388
+ * label never lies about the number.
58389
+ */
58390
+ function convertForDisplay(raw, sourceUnit, displayUnit) {
58391
+ const from = sourceUnit !== void 0 ? normalizeUnit(sourceUnit) : void 0;
58392
+ const to = displayUnit !== void 0 ? normalizeUnit(displayUnit) : void 0;
58393
+ if (from === void 0 || to === void 0 || from === to) return {
58394
+ value: raw,
58395
+ unit: to ?? from ?? ""
58396
+ };
58397
+ const converted = tryConvertUnit(raw, from, to);
58398
+ if (converted === void 0) return {
58399
+ value: raw,
58400
+ unit: from
58401
+ };
58402
+ return {
58403
+ value: converted,
58404
+ unit: to
58405
+ };
58406
+ }
58407
+ /**
58408
+ * Best-effort unit from a state slice: sibling `unit` at the same depth as
58409
+ * `valuePath`, else top-level `unit`.
58410
+ */
58411
+ function inferSourceUnit(slice, valuePath) {
58412
+ if (typeof slice !== "object" || slice === null || Array.isArray(slice)) return void 0;
58413
+ const root = { ...slice };
58414
+ const segments = valuePath.split(".");
58415
+ if (segments.length >= 1) {
58416
+ let parent = root;
58417
+ for (const segment of segments.slice(0, -1)) {
58418
+ if (typeof parent !== "object" || parent === null || Array.isArray(parent)) {
58419
+ parent = void 0;
58420
+ break;
58421
+ }
58422
+ parent = { ...parent }[segment];
58423
+ }
58424
+ if (typeof parent === "object" && parent !== null && !Array.isArray(parent)) {
58425
+ const unit = { ...parent }.unit;
58426
+ if (typeof unit === "string" && unit.length > 0) return unit;
58427
+ }
58428
+ }
58429
+ const top = root.unit;
58430
+ return typeof top === "string" && top.length > 0 ? top : void 0;
58431
+ }
58432
+ //#endregion
58433
+ //#region src/named-template-render.ts
58434
+ /** Render one named template's parser from live states. */
58435
+ function renderNamedTemplate(template, facts) {
58436
+ let text = template.parser;
58437
+ if (template.fields.length === 0) return text.trim() === "" ? { reason: `template "${template.id}" has no fields` } : { text };
58438
+ for (const field of template.fields) {
58439
+ const token = fieldToken(field);
58440
+ const deviceState = facts.stateByDevice.get(field.deviceId);
58441
+ if (deviceState === void 0) return { reason: `no state mirrored for device ${field.deviceId}` };
58442
+ const slice = deviceState[field.capName];
58443
+ if (slice === void 0) return { reason: `device ${field.deviceId} publishes no "${field.capName}" state` };
58444
+ const raw = readStatePath(slice, field.valuePath);
58445
+ if (raw === void 0) return { reason: `"${field.capName}.${field.valuePath}" is absent on device ${field.deviceId}` };
58446
+ let rendered;
58447
+ if (typeof raw === "number") rendered = truncateDecimals(convertForDisplay(raw, inferSourceUnit(slice, field.valuePath), field.displayUnit).value, field.maxDecimals);
58448
+ else if (typeof raw === "boolean") rendered = raw ? "ON" : "OFF";
58449
+ else if (raw === null) rendered = "";
58450
+ else rendered = String(raw);
58451
+ text = replaceToken(text, token, rendered);
58452
+ }
58453
+ return { text };
58454
+ }
58455
+ //#endregion
57563
58456
  //#region src/render.ts
57564
58457
  /**
57565
58458
  * Read the source's value. `null` means "unreadable" — a missing device, a
@@ -57574,6 +58467,16 @@ function resolveSource(binding, cameraDeviceId, facts) {
57574
58467
  const value = facts.recognitionByDevice.get(cameraDeviceId)?.[source.recognition];
57575
58468
  return value === void 0 ? { reason: `no recognized ${source.recognition} has been persisted for device ${cameraDeviceId}` } : { value: { raw: value } };
57576
58469
  }
58470
+ if (source.kind === "template") {
58471
+ const named = facts.templatesById.get(source.templateId);
58472
+ if (named === void 0) return { reason: `named template "${source.templateId}" is missing` };
58473
+ const rendered = renderNamedTemplate(named, { stateByDevice: facts.stateByDevice });
58474
+ if ("reason" in rendered) return rendered;
58475
+ return { value: {
58476
+ raw: rendered.text,
58477
+ preformatted: rendered.text
58478
+ } };
58479
+ }
57577
58480
  const deviceId = source.deviceId ?? cameraDeviceId;
57578
58481
  const deviceState = facts.stateByDevice.get(deviceId);
57579
58482
  if (deviceState === void 0) return { reason: `no state mirrored for device ${deviceId}` };
@@ -57581,6 +58484,14 @@ function resolveSource(binding, cameraDeviceId, facts) {
57581
58484
  if (slice === void 0) return { reason: `device ${deviceId} publishes no "${source.capName}" state` };
57582
58485
  const raw = readStatePath(slice, source.valuePath);
57583
58486
  if (raw === void 0) return { reason: `"${source.capName}.${source.valuePath}" is absent on device ${deviceId}` };
58487
+ if (typeof raw === "number") {
58488
+ const converted = convertForDisplay(raw, inferSourceUnit(slice, source.valuePath), source.displayUnit);
58489
+ return { value: {
58490
+ raw,
58491
+ displayRaw: converted.value,
58492
+ unit: converted.unit || binding.unitLabel
58493
+ } };
58494
+ }
57584
58495
  return { value: { raw } };
57585
58496
  }
57586
58497
  /**
@@ -57613,7 +58524,12 @@ function renderSlot(slotId, binding, cameraDeviceId, facts, previousText) {
57613
58524
  if (!gate.open) return settle(slotId, fallback, previousText, "gated", `condition "${gate.failedCondition}"`);
57614
58525
  const resolved = resolveSource(binding, cameraDeviceId, facts);
57615
58526
  if ("reason" in resolved) return settle(slotId, fallback, previousText, "unreadable", resolved.reason);
57616
- return settle(slotId, formatSlotText(resolved.value.raw, binding), previousText, "written");
58527
+ if (resolved.value.preformatted !== void 0 && binding.source.kind === "template") return settle(slotId, limitText(resolved.value.preformatted.trim(), binding.maxCharacters), previousText, "written");
58528
+ const unit = resolved.value.unit ?? binding.unitLabel;
58529
+ return settle(slotId, formatSlotText(resolved.value.raw, binding, {
58530
+ ...resolved.value.displayRaw !== void 0 ? { displayRaw: resolved.value.displayRaw } : {},
58531
+ ...unit !== void 0 ? { unit } : {}
58532
+ }), previousText, "written");
57617
58533
  }
57618
58534
  function settle(slotId, text, previousText, outcome, reason) {
57619
58535
  if (previousText !== void 0 && previousText === text) return {
@@ -57673,6 +58589,24 @@ function sourceOptionsFor(cameraDeviceId, devices, capsByDevice) {
57673
58589
  return options;
57674
58590
  }
57675
58591
  //#endregion
58592
+ //#region src/templates-store.ts
58593
+ /**
58594
+ * Named OSD templates — durable in the addon's own (global) store.
58595
+ */
58596
+ var OSD_TEMPLATES_KEY = "namedTemplates";
58597
+ var OsdNamedTemplatesSchema = array(OsdNamedTemplateSchema);
58598
+ var EMPTY_TEMPLATES = [];
58599
+ function upsertNamedTemplate(prev, template) {
58600
+ const parsed = OsdNamedTemplateSchema.parse(template);
58601
+ return [...prev.filter((t) => t.id !== parsed.id), parsed].toSorted((a, b) => a.name.localeCompare(b.name));
58602
+ }
58603
+ function deleteNamedTemplate(prev, templateId) {
58604
+ return prev.filter((t) => t.id !== templateId);
58605
+ }
58606
+ function findNamedTemplate(templates, templateId) {
58607
+ return templates.find((t) => t.id === templateId);
58608
+ }
58609
+ //#endregion
57676
58610
  //#region src/osd-manager.ts
57677
58611
  /**
57678
58612
  * The `osd-manager` provider — the render loop and the operator surface.
@@ -58036,6 +58970,26 @@ var OsdManager = class {
58036
58970
  renderDevice = async ({ deviceId }) => {
58037
58971
  return { results: [...await this.renderDeviceInternal(deviceId, null)] };
58038
58972
  };
58973
+ listTemplates = async () => {
58974
+ return { templates: [...await this.deps.templates.get()] };
58975
+ };
58976
+ upsertTemplate = async ({ template }) => {
58977
+ const next = upsertNamedTemplate(await this.deps.templates.get(), template);
58978
+ await this.deps.templates.set(next);
58979
+ const stored = findNamedTemplate(next, template.id);
58980
+ if (stored === void 0) throw new Error(`osd-manager: template "${template.id}" missing after upsert`);
58981
+ this.logger.info("osd-manager: named template upserted", { meta: {
58982
+ templateId: stored.id,
58983
+ fieldCount: stored.fields.length
58984
+ } });
58985
+ return { template: stored };
58986
+ };
58987
+ deleteTemplate = async ({ templateId }) => {
58988
+ const prev = await this.deps.templates.get();
58989
+ await this.deps.templates.set(deleteNamedTemplate(prev, templateId));
58990
+ this.logger.info("osd-manager: named template deleted", { meta: { templateId } });
58991
+ return { success: true };
58992
+ };
58039
58993
  async renderAllBound() {
58040
58994
  for (const deviceId of [...this.boundDevices]) try {
58041
58995
  await this.renderDeviceInternal(deviceId, null);
@@ -58103,11 +59057,19 @@ var OsdManager = class {
58103
59057
  async collectFacts(cameraDeviceId, bindings) {
58104
59058
  const wanted = new Set([cameraDeviceId]);
58105
59059
  const needsRecognition = Object.values(bindings).some((binding) => binding.source.kind === "latest-recognition");
59060
+ const templateIds = /* @__PURE__ */ new Set();
58106
59061
  for (const binding of Object.values(bindings)) {
58107
59062
  if (binding.source.kind === "device-state" && binding.source.deviceId !== void 0) wanted.add(binding.source.deviceId);
59063
+ if (binding.source.kind === "template") templateIds.add(binding.source.templateId);
58108
59064
  const gated = binding.conditions?.deviceState?.deviceId;
58109
59065
  if (gated !== void 0) wanted.add(gated);
58110
59066
  }
59067
+ const allTemplates = await this.deps.templates.get();
59068
+ const templatesById = /* @__PURE__ */ new Map();
59069
+ for (const named of allTemplates) {
59070
+ templatesById.set(named.id, named);
59071
+ if (templateIds.has(named.id)) for (const field of named.fields) wanted.add(field.deviceId);
59072
+ }
58111
59073
  const stateByDevice = /* @__PURE__ */ new Map();
58112
59074
  const gateState = /* @__PURE__ */ new Map();
58113
59075
  for (const id of wanted) try {
@@ -58132,7 +59094,8 @@ var OsdManager = class {
58132
59094
  atMs: this.now(),
58133
59095
  stateByDevice,
58134
59096
  recognitionByDevice,
58135
- deviceState: (id) => gateState.get(id)
59097
+ deviceState: (id) => gateState.get(id),
59098
+ templatesById
58136
59099
  };
58137
59100
  }
58138
59101
  async hydrateRecognitions(deviceId) {
@@ -58273,6 +59236,7 @@ var OsdManagerAddon = class extends BaseAddon {
58273
59236
  const ctx = this.ctx;
58274
59237
  const store = new OsdBindingsStore((deviceId, key, schema, fallback) => this.deviceState(deviceId, key, schema, fallback));
58275
59238
  const boundIndex = this.state(OSD_BOUND_INDEX_KEY, OsdBoundIndexSchema, null);
59239
+ const templates = this.state(OSD_TEMPLATES_KEY, OsdNamedTemplatesSchema, EMPTY_TEMPLATES);
58276
59240
  const manager = new OsdManager({
58277
59241
  logger: ctx.logger,
58278
59242
  readOsdStatus: async (deviceId) => {
@@ -58346,7 +59310,8 @@ var OsdManagerAddon = class extends BaseAddon {
58346
59310
  };
58347
59311
  },
58348
59312
  bindings: (deviceId) => store.forDevice(deviceId),
58349
- boundIndex
59313
+ boundIndex,
59314
+ templates
58350
59315
  });
58351
59316
  this.manager = manager;
58352
59317
  this.subscribe({ category: EventCategory.PipelineAnalyticsTrackLifecycle }, (event) => {