@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.mjs CHANGED
@@ -7519,7 +7519,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
7519
7519
  return DeviceRole;
7520
7520
  }({});
7521
7521
  //#endregion
7522
- //#region ../types/dist/composition-D7ULvLdD.mjs
7522
+ //#region ../types/dist/composition-Bzr7GwgP.mjs
7523
7523
  /**
7524
7524
  * The ffmpeg build every CamStack node downloads, per platform and arch.
7525
7525
  *
@@ -7607,6 +7607,8 @@ var SOURCE_CAP_ACTIVE_FIELD = {
7607
7607
  vibration: "detected",
7608
7608
  tamper: "tampered"
7609
7609
  };
7610
+ /** Cap names whose presence in a device's bindings qualify it as a source. */
7611
+ var SOURCE_CAPS = Object.keys(SOURCE_CAP_ACTIVE_FIELD);
7610
7612
  /**
7611
7613
  * Error types for the safe expression engine. Two distinct classes so callers
7612
7614
  * can tell a compile-time (grammar) failure from a runtime (evaluation)
@@ -12476,7 +12478,7 @@ var BrokerStatusEnum = _enum([
12476
12478
  "unreachable",
12477
12479
  "error"
12478
12480
  ]);
12479
- var BrokerInfoSchema$1 = object({
12481
+ var BrokerInfoSchema = object({
12480
12482
  /** Stable broker id. Persisted; survives addon restarts. */
12481
12483
  id: string(),
12482
12484
  /** Addon id of the provider that OWNS this broker.
@@ -12619,8 +12621,8 @@ var brokerCapability = {
12619
12621
  kind: "poll"
12620
12622
  },
12621
12623
  methods: {
12622
- list: method(ListInputSchema, array(BrokerInfoSchema$1)),
12623
- get: method(GetInputSchema, BrokerInfoSchema$1.nullable()),
12624
+ list: method(ListInputSchema, array(BrokerInfoSchema)),
12625
+ get: method(GetInputSchema, BrokerInfoSchema.nullable()),
12624
12626
  /** Enumerate which addon provides which broker kind(s) for the
12625
12627
  * unified create picker. The auto-mount fans this array across
12626
12628
  * every registered `broker` provider (array-output method), so the
@@ -15439,6 +15441,51 @@ var SourceInfoSchema = object({
15439
15441
  raw: record(string(), unknown()).optional()
15440
15442
  });
15441
15443
  /**
15444
+ * Coerce common unit-of-measurement string variants to a canonical form
15445
+ * so the UI's display templates can rely on stable strings. Pure
15446
+ * passthrough for unknown units — kept conservative on purpose, the goal
15447
+ * is to normalise the long tail of HA `unit_of_measurement` values that
15448
+ * have multiple historical spellings (`'°C'` vs `'C'` vs `'celsius'`).
15449
+ *
15450
+ * Extend the map as we encounter real-world data. Returning the raw
15451
+ * input for unknown values is intentional: providers should never
15452
+ * lose information silently here.
15453
+ */
15454
+ function normalizeUnit(raw) {
15455
+ if (raw == null) return void 0;
15456
+ const trimmed = raw.trim();
15457
+ if (trimmed === "") return void 0;
15458
+ const lower = trimmed.toLowerCase();
15459
+ if (lower === "°c" || lower === "c" || lower === "celsius") return "°C";
15460
+ if (lower === "°f" || lower === "f" || lower === "fahrenheit") return "°F";
15461
+ if (lower === "k" || lower === "kelvin") return "K";
15462
+ if (lower === "%" || lower === "percent" || lower === "percentage") return "%";
15463
+ if (lower === "lx" || lower === "lux") return "lx";
15464
+ if (lower === "hpa" || lower === "hectopascal") return "hPa";
15465
+ if (lower === "kpa") return "kPa";
15466
+ if (lower === "pa") return "Pa";
15467
+ if (lower === "mbar" || lower === "millibar") return "mbar";
15468
+ if (lower === "w" || lower === "watt") return "W";
15469
+ if (lower === "kw" || lower === "kilowatt") return "kW";
15470
+ if (lower === "wh" || lower === "watt-hour") return "Wh";
15471
+ if (lower === "kwh" || lower === "kilowatt-hour") return "kWh";
15472
+ if (lower === "v" || lower === "volt") return "V";
15473
+ if (lower === "a" || lower === "amp" || lower === "ampere") return "A";
15474
+ if (lower === "ma" || lower === "milliampere") return "mA";
15475
+ if (lower === "va") return "VA";
15476
+ if (lower === "ppm") return "ppm";
15477
+ if (lower === "ppb") return "ppb";
15478
+ if (lower === "µg/m³" || lower === "ug/m3" || lower === "µg/m3") return "µg/m³";
15479
+ if (lower === "km/h" || lower === "kph") return "km/h";
15480
+ if (lower === "mph") return "mph";
15481
+ if (lower === "m/s") return "m/s";
15482
+ if (lower === "m") return "m";
15483
+ if (lower === "km") return "km";
15484
+ if (lower === "kg") return "kg";
15485
+ if (lower === "g") return "g";
15486
+ return trimmed;
15487
+ }
15488
+ /**
15442
15489
  * device-discovery — device-scoped capability for parents that host /
15443
15490
  * enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
15444
15491
  * integrations). The parent advertises a list of discoverable children,
@@ -19675,7 +19722,7 @@ var BrokerStatusSchema$1 = _enum([
19675
19722
  "unreachable",
19676
19723
  "tls-error"
19677
19724
  ]);
19678
- var BrokerInfoSchema = object({
19725
+ var BrokerInfoSchema$1 = object({
19679
19726
  id: string(),
19680
19727
  name: string(),
19681
19728
  url: string(),
@@ -19748,7 +19795,7 @@ var mqttBrokerCapability = {
19748
19795
  kind: "poll"
19749
19796
  },
19750
19797
  methods: {
19751
- listBrokers: method(_void(), array(BrokerInfoSchema)),
19798
+ listBrokers: method(_void(), array(BrokerInfoSchema$1)),
19752
19799
  getBrokerConfig: method(IdInputSchema, BrokerConnectionDetailsSchema, { auth: "admin" }),
19753
19800
  addBroker: method(AddBrokerInputSchema, AddBrokerResultSchema, {
19754
19801
  kind: "mutation",
@@ -22002,78 +22049,16 @@ var NcRuleInputSchema = object({
22002
22049
  */
22003
22050
  confirm: NcConfirmSchema.optional(),
22004
22051
  /**
22005
- * WAIT for face/plate recognition before saying anything.
22006
- *
22007
- * A notification's TEXT is frozen at enqueue and its media is re-resolved at
22008
- * send; the identity is neither. A face is confirmed after `confirmFrames`
22009
- * agreeing observations — p50 **11.4 s** after the track was first seen,
22010
- * measured on this hub — and an `immediate` rule enqueues on the first object
22011
- * event, seconds before that. So "Gianluca è arrivato" is unsayable on the
22012
- * immediate path, and no amount of media re-resolution fixes a sentence.
22013
- *
22014
- * Only two honest answers exist, and this flag picks between them. It has
22015
- * effect ONLY on a rule that declares a recognition scope
22016
- * ({@link NcConditions.identities} or {@link NcConditions.plates}) — on any
22017
- * other rule there is nothing to wait for and the flag is inert.
22018
- *
22019
- * | value | what happens |
22020
- * | --- | --- |
22021
- * | `true` | the rule stops firing on the object event and fires at TRACK CLOSE instead, once, with the name — later, and complete |
22022
- * | 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) |
22023
- *
22024
- * `.optional()` and deliberately NOT `.default()`: a Zod default does not run
22025
- * on the addon cap path, and absent has to keep meaning exactly what every
22026
- * rule authored before this field meant.
22027
- *
22028
- * The cost of `true` is stated here because the editor states it too: a rule
22029
- * that waits also inherits track-close SEMANTICS — its `zones` condition
22030
- * tests every zone the track visited and a `crossing` condition can no longer
22031
- * be satisfied, because a closed track carries no crossing.
22052
+ * IMMEDIATE GROUPED (D692): the first matching subject opens a window of this many
22053
+ * seconds; the rule sends ONE message when the window closes — early, once every
22054
+ * subject in it is final, never before {@link NC_GROUPING_MIN_SEC} — naming everybody
22055
+ * the rule matched, with one mosaic tile per subject.
22056
+ * Absent = plain immediate (one message per track, at the first match).
22057
+ * Only an `immediate` DETECTION rule may carry it (`ruleGroupingApplies`, enforced on
22058
+ * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
22059
+ * path does not run Zod defaults.
22032
22060
  */
22033
- waitForEnhancement: boolean().optional(),
22034
- /**
22035
- * GROUP a burst of subjects into ONE notification that grows.
22036
- *
22037
- * Seconds of quiet after the last matching subject before the burst is
22038
- * considered over. While it is open, the first subject enqueues immediately —
22039
- * **exactly as today, with no added latency** — and every real growth (a new
22040
- * subject, or a name confirmed on one already in it) REPLACES that
22041
- * notification with an updated one naming everybody. The push carries the
22042
- * group's own coalescing tag, so the phone replaces rather than stacks.
22043
- *
22044
- * `0` / absent = off, and off is today's behaviour byte for byte.
22045
- *
22046
- * ### Why an idle cutoff and not a window
22047
- *
22048
- * The measured seven-person arrival on device 590 spans 110 s with every
22049
- * internal gap under 30 s. A 12 s fixed window cuts it into three groups; an
22050
- * idle cutoff holds it as one and ends it when the arrival actually ends.
22051
- * 30 s is a widely shipped value for the same decision.
22052
- *
22053
- * ### What it replaces
22054
- *
22055
- * The blind cooldown, which collapses a burst by DISCARDING it. Measured on
22056
- * device 615 / *Persona su Uscio* over six days: 116 qualifying tracks → 74
22057
- * notifications, **44 (37.9%) suppressed outright**, 23 of them overlapping a
22058
- * track that did fire and 7 carrying a confirmed identity nobody heard about.
22059
- * A group collapses the same volume by MERGING, so the cooldown becomes a
22060
- * budget over GROUPS — which is what it always meant — and a growth is never
22061
- * throttled by the window its own first member spent.
22062
- *
22063
- * ### Interaction with {@link waitForEnhancement}
22064
- *
22065
- * They compose, and the order matters. `waitForEnhancement` defers the rule to
22066
- * TRACK CLOSE, so with both set the group is opened by the first member to
22067
- * CLOSE — already carrying its name — and grows as later members close. That
22068
- * is later, and complete. With grouping alone the group opens on the first
22069
- * object event and picks up names as they are confirmed, through the growth
22070
- * path. Neither combination fires twice for one subject.
22071
- *
22072
- * `.optional()` and deliberately NOT `.default()`: a Zod default does not run
22073
- * on the addon cap path, so absent must keep meaning what it meant before this
22074
- * field existed.
22075
- */
22076
- groupIdleSec: number().int().min(0).max(600).optional()
22061
+ groupingSec: number().int().min(5).max(60).optional()
22077
22062
  });
22078
22063
  /**
22079
22064
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -22121,9 +22106,10 @@ var NcRuleClearableKeySchema = _enum([
22121
22106
  "targetUsers",
22122
22107
  "snoozeOptions",
22123
22108
  "snoozeAllowGlobal",
22124
- "waitForEnhancement",
22125
22109
  "actions",
22126
22110
  "confirm",
22111
+ "groupingSec",
22112
+ "waitForEnhancement",
22127
22113
  "groupIdleSec"
22128
22114
  ]);
22129
22115
  var NcRuleUpdateInputSchema = object({
@@ -22145,9 +22131,25 @@ var NcRuleSchema = NcRuleInputSchema.extend({
22145
22131
  */
22146
22132
  disabledTargetIds: array(string()).default([])
22147
22133
  });
22134
+ /**
22135
+ * The state-summary SECTIONS (D647 / D698): the D637 kind groups that report a
22136
+ * still-true state. `backups-exports` and `alarm` are not sections — a completed
22137
+ * job is not an open state, and the alarm stays instant.
22138
+ */
22139
+ var NcStateSummarySectionIdSchema = _enum([
22140
+ "device-state",
22141
+ "streams",
22142
+ "battery",
22143
+ "consumables",
22144
+ "tanks",
22145
+ "detection-blind",
22146
+ "nodes",
22147
+ "addons",
22148
+ "updates"
22149
+ ]);
22148
22150
  /** One section of a previewed state summary. `unavailable` = its state could not be read. */
22149
22151
  var NcStateSummaryPreviewSectionSchema = object({
22150
- id: string(),
22152
+ id: NcStateSummarySectionIdSchema,
22151
22153
  status: _enum(["read", "unavailable"]),
22152
22154
  items: number().int().nonnegative(),
22153
22155
  reason: string().optional()
@@ -22157,7 +22159,7 @@ var NcStateSummaryPreviewSectionSchema = object({
22157
22159
  * a `rule` split (no `section`), one section's on a `section` split.
22158
22160
  */
22159
22161
  var NcStateSummaryPreviewRowSchema = object({
22160
- section: string().optional(),
22162
+ section: NcStateSummarySectionIdSchema.optional(),
22161
22163
  /** The section's name on a per-section row; every reported section's, joined, on a combined one. */
22162
22164
  title: string(),
22163
22165
  text: string(),
@@ -22675,7 +22677,7 @@ var NC_CONDITION_CATALOG = [
22675
22677
  operator: "in",
22676
22678
  appliesTo: ["immediate", "track-end"],
22677
22679
  phase: "P1",
22678
- 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."
22680
+ 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)."
22679
22681
  },
22680
22682
  {
22681
22683
  id: "plates",
@@ -25398,6 +25400,7 @@ var KeyEventSchema = object({
25398
25400
  bestEventId: string(),
25399
25401
  /** Track lifetime in ms (lastSeen - firstSeen). */
25400
25402
  windowMs: number().optional(),
25403
+ audioLabels: array(TrackAudioLabelSchema).readonly().optional(),
25401
25404
  ...TrackFlagFields,
25402
25405
  ...TrackRetrainFields
25403
25406
  });
@@ -39334,28 +39337,29 @@ setOverlay: method(object({
39334
39337
  *
39335
39338
  * This cap owns the other half: a per-(camera, slot) BINDING that says
39336
39339
  * which value feeds the slot, how it is formatted, and under which
39337
- * conditions it is shown at all. One addon renders every binding on every
39338
- * camera, so a new source costs zero driver code.
39339
- *
39340
- * Three deliberate choices, each with a rejected alternative:
39341
- *
39342
- * 1. A source is `(capName, valuePath)` over the kernel's device
39343
- * runtime-state mirror — NOT a closed enum of source kinds. Every
39344
- * cap-keyed slice a device publishes is bindable the day the cap
39345
- * ships. The rejected alternative (one enum member per source, with
39346
- * a resolver branch each) is what makes "add the humidity too" a
39347
- * code change.
39348
- * 2. The display gate reuses `NcConditionsSchema` verbatim — the
39349
- * notification centre's condition vocabulary — rather than a parallel
39350
- * model. An operator who has learned one condition editor has learned
39351
- * both.
39352
- * 3. Because the renderer's facts are device STATE and not a detection
39353
- * record, only a SUBSET of that vocabulary can be answered here.
39354
- * `setSlotBinding` REJECTS the rest at write time (see
39355
- * `getConditionSupport`). It does not accept-then-fail-closed: a
39356
- * condition that can never be true renders a permanently blank
39357
- * overlay, and a blank overlay looks exactly like a broken camera.
39340
+ * conditions it is shown at all. Named TEMPLATES combine many device
39341
+ * fields into one overlay string (Scrypted OSD Manager parity).
39358
39342
  */
39343
+ /** One field reference inside a named multi-device template. */
39344
+ var OsdTemplateFieldSchema = object({
39345
+ deviceId: number().int(),
39346
+ capName: string().min(1).max(64),
39347
+ valuePath: string().min(1).max(64),
39348
+ /** Convert TO this unit at render (`tryConvertUnit`). Absent = source unit. */
39349
+ displayUnit: string().max(16).optional(),
39350
+ maxDecimals: number().int().min(0).max(4).default(1)
39351
+ });
39352
+ /**
39353
+ * A named template: several device fields → one parser string with
39354
+ * `{deviceId.capName.valuePath}` tokens (Scrypted `{deviceId.sensorId}`).
39355
+ */
39356
+ var OsdNamedTemplateSchema = object({
39357
+ id: string().min(1).max(64),
39358
+ name: string().min(1).max(64),
39359
+ fields: array(OsdTemplateFieldSchema).max(32).default([]),
39360
+ /** Parser with multi-token placeholders, e.g. `T {12.thermometer.value}°`. */
39361
+ parser: string().max(256).default("")
39362
+ });
39359
39363
  /** Where a slot's value comes from. */
39360
39364
  var OsdSourceSchema = discriminatedUnion("kind", [
39361
39365
  object({
@@ -39374,11 +39378,17 @@ var OsdSourceSchema = discriminatedUnion("kind", [
39374
39378
  deviceId: number().int().optional(),
39375
39379
  capName: string().min(1).max(64),
39376
39380
  /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
39377
- valuePath: string().min(1).max(64)
39381
+ valuePath: string().min(1).max(64),
39382
+ /** Convert TO this unit at render. Absent = no conversion. */
39383
+ displayUnit: string().max(16).optional()
39378
39384
  }),
39379
39385
  object({
39380
39386
  kind: literal("latest-recognition"),
39381
39387
  recognition: _enum(["person", "plate"])
39388
+ }),
39389
+ object({
39390
+ kind: literal("template"),
39391
+ templateId: string().min(1).max(64)
39382
39392
  })
39383
39393
  ]);
39384
39394
  var OsdSlotBindingSchema = object({
@@ -39394,7 +39404,10 @@ var OsdSlotBindingSchema = object({
39394
39404
  * documented workaround for firmwares that reject `.` in overlay text.
39395
39405
  */
39396
39406
  maxDecimals: number().int().min(0).max(4).default(1),
39397
- /** Appended via `${unit}`. The state mirror does not carry units. */
39407
+ /**
39408
+ * Appended via `${unit}` when no unit conversion produces a label.
39409
+ * Prefer `source.displayUnit` for converted device-state values.
39410
+ */
39398
39411
  unitLabel: string().max(8).optional(),
39399
39412
  /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
39400
39413
  valueMap: record(string(), string()).optional(),
@@ -39548,6 +39561,21 @@ var osdManagerCapability = {
39548
39561
  renderDevice: method(object({ deviceId: number().int() }), object({ results: array(OsdRenderResultSchema) }), {
39549
39562
  kind: "mutation",
39550
39563
  auth: "admin"
39564
+ }),
39565
+ /** Every named multi-device template. */
39566
+ listTemplates: method(object({}), object({ templates: array(OsdNamedTemplateSchema) }), { auth: "admin" }),
39567
+ /**
39568
+ * Create or replace a named template by `id`. Returns the stored record
39569
+ * after Zod normalisation.
39570
+ */
39571
+ upsertTemplate: method(object({ template: OsdNamedTemplateSchema }), object({ template: OsdNamedTemplateSchema }), {
39572
+ kind: "mutation",
39573
+ auth: "admin"
39574
+ }),
39575
+ /** Drop a named template. Bindings that still reference it render unreadable. */
39576
+ deleteTemplate: method(object({ templateId: string().min(1).max(64) }), object({ success: literal(true) }), {
39577
+ kind: "mutation",
39578
+ auth: "admin"
39551
39579
  })
39552
39580
  }
39553
39581
  };
@@ -45299,6 +45327,266 @@ var zoneRulesCapability = {
45299
45327
  durability: "restored"
45300
45328
  };
45301
45329
  /**
45330
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
45331
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
45332
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
45333
+ * decimals). K derives from `°C = K − 273.15`.
45334
+ */
45335
+ var UNIT_TABLE = {
45336
+ "°C": {
45337
+ dimension: "temperature",
45338
+ factor: 1,
45339
+ offset: 0
45340
+ },
45341
+ "°F": {
45342
+ dimension: "temperature",
45343
+ factor: 5 / 9,
45344
+ offset: -160 / 9
45345
+ },
45346
+ K: {
45347
+ dimension: "temperature",
45348
+ factor: 1,
45349
+ offset: -273.15
45350
+ },
45351
+ hPa: {
45352
+ dimension: "pressure",
45353
+ factor: 1,
45354
+ offset: 0
45355
+ },
45356
+ kPa: {
45357
+ dimension: "pressure",
45358
+ factor: 10,
45359
+ offset: 0
45360
+ },
45361
+ Pa: {
45362
+ dimension: "pressure",
45363
+ factor: .01,
45364
+ offset: 0
45365
+ },
45366
+ mbar: {
45367
+ dimension: "pressure",
45368
+ factor: 1,
45369
+ offset: 0
45370
+ },
45371
+ bar: {
45372
+ dimension: "pressure",
45373
+ factor: 1e3,
45374
+ offset: 0
45375
+ },
45376
+ inHg: {
45377
+ dimension: "pressure",
45378
+ factor: 33.8639,
45379
+ offset: 0
45380
+ },
45381
+ mmHg: {
45382
+ dimension: "pressure",
45383
+ factor: 1.33322,
45384
+ offset: 0
45385
+ },
45386
+ psi: {
45387
+ dimension: "pressure",
45388
+ factor: 68.9476,
45389
+ offset: 0
45390
+ },
45391
+ "m/s": {
45392
+ dimension: "speed",
45393
+ factor: 1,
45394
+ offset: 0
45395
+ },
45396
+ "km/h": {
45397
+ dimension: "speed",
45398
+ factor: 1 / 3.6,
45399
+ offset: 0
45400
+ },
45401
+ mph: {
45402
+ dimension: "speed",
45403
+ factor: .44704,
45404
+ offset: 0
45405
+ },
45406
+ kn: {
45407
+ dimension: "speed",
45408
+ factor: .514444,
45409
+ offset: 0
45410
+ },
45411
+ "mm/h": {
45412
+ dimension: "precipitation-rate",
45413
+ factor: 1,
45414
+ offset: 0
45415
+ },
45416
+ "in/h": {
45417
+ dimension: "precipitation-rate",
45418
+ factor: 25.4,
45419
+ offset: 0
45420
+ },
45421
+ m: {
45422
+ dimension: "length",
45423
+ factor: 1,
45424
+ offset: 0
45425
+ },
45426
+ mm: {
45427
+ dimension: "length",
45428
+ factor: .001,
45429
+ offset: 0
45430
+ },
45431
+ cm: {
45432
+ dimension: "length",
45433
+ factor: .01,
45434
+ offset: 0
45435
+ },
45436
+ km: {
45437
+ dimension: "length",
45438
+ factor: 1e3,
45439
+ offset: 0
45440
+ },
45441
+ in: {
45442
+ dimension: "length",
45443
+ factor: .0254,
45444
+ offset: 0
45445
+ },
45446
+ ft: {
45447
+ dimension: "length",
45448
+ factor: .3048,
45449
+ offset: 0
45450
+ },
45451
+ mi: {
45452
+ dimension: "length",
45453
+ factor: 1609.344,
45454
+ offset: 0
45455
+ },
45456
+ lx: {
45457
+ dimension: "illuminance",
45458
+ factor: 1,
45459
+ offset: 0
45460
+ },
45461
+ "W/m²": {
45462
+ dimension: "irradiance",
45463
+ factor: 1,
45464
+ offset: 0
45465
+ },
45466
+ W: {
45467
+ dimension: "power",
45468
+ factor: 1,
45469
+ offset: 0
45470
+ },
45471
+ kW: {
45472
+ dimension: "power",
45473
+ factor: 1e3,
45474
+ offset: 0
45475
+ },
45476
+ VA: {
45477
+ dimension: "apparent-power",
45478
+ factor: 1,
45479
+ offset: 0
45480
+ },
45481
+ Wh: {
45482
+ dimension: "energy",
45483
+ factor: 1,
45484
+ offset: 0
45485
+ },
45486
+ kWh: {
45487
+ dimension: "energy",
45488
+ factor: 1e3,
45489
+ offset: 0
45490
+ },
45491
+ MWh: {
45492
+ dimension: "energy",
45493
+ factor: 1e6,
45494
+ offset: 0
45495
+ },
45496
+ V: {
45497
+ dimension: "voltage",
45498
+ factor: 1,
45499
+ offset: 0
45500
+ },
45501
+ mV: {
45502
+ dimension: "voltage",
45503
+ factor: .001,
45504
+ offset: 0
45505
+ },
45506
+ A: {
45507
+ dimension: "current",
45508
+ factor: 1,
45509
+ offset: 0
45510
+ },
45511
+ mA: {
45512
+ dimension: "current",
45513
+ factor: .001,
45514
+ offset: 0
45515
+ },
45516
+ ppm: {
45517
+ dimension: "concentration-volume",
45518
+ factor: 1,
45519
+ offset: 0
45520
+ },
45521
+ ppb: {
45522
+ dimension: "concentration-volume",
45523
+ factor: .001,
45524
+ offset: 0
45525
+ },
45526
+ "µg/m³": {
45527
+ dimension: "concentration-mass",
45528
+ factor: 1,
45529
+ offset: 0
45530
+ },
45531
+ "mg/m³": {
45532
+ dimension: "concentration-mass",
45533
+ factor: 1e3,
45534
+ offset: 0
45535
+ },
45536
+ kg: {
45537
+ dimension: "mass",
45538
+ factor: 1,
45539
+ offset: 0
45540
+ },
45541
+ g: {
45542
+ dimension: "mass",
45543
+ factor: .001,
45544
+ offset: 0
45545
+ },
45546
+ lb: {
45547
+ dimension: "mass",
45548
+ factor: .453592,
45549
+ offset: 0
45550
+ },
45551
+ oz: {
45552
+ dimension: "mass",
45553
+ factor: .0283495,
45554
+ offset: 0
45555
+ },
45556
+ "%": {
45557
+ dimension: "percentage",
45558
+ factor: 1,
45559
+ offset: 0
45560
+ }
45561
+ };
45562
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
45563
+ function specFor(unit) {
45564
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
45565
+ }
45566
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
45567
+ function toCanonical(value, spec) {
45568
+ return value * spec.factor + spec.offset;
45569
+ }
45570
+ /** Canonical-unit value → the unit described by `spec`. */
45571
+ function fromCanonical(canonical, spec) {
45572
+ return (canonical - spec.offset) / spec.factor;
45573
+ }
45574
+ /**
45575
+ * Graceful variant for the render path: returns the converted value, or
45576
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
45577
+ * Never throws — a render caller falls back to the raw value + source unit.
45578
+ */
45579
+ function tryConvertUnit(value, from, to) {
45580
+ if (from === to) return value;
45581
+ const fromSpec = specFor(from);
45582
+ if (!fromSpec) return void 0;
45583
+ const toSpec = specFor(to);
45584
+ if (!toSpec) return void 0;
45585
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
45586
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
45587
+ }
45588
+ SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
45589
+ /**
45302
45590
  * Most specific first. Extending this list is how a new device kind becomes
45303
45591
  * gateable; nothing else needs to change.
45304
45592
  */
@@ -50665,6 +50953,12 @@ Object.freeze({
50665
50953
  addonId: null,
50666
50954
  access: "create"
50667
50955
  },
50956
+ "osdManager.deleteTemplate": {
50957
+ capName: "osd-manager",
50958
+ capScope: "system",
50959
+ addonId: null,
50960
+ access: "delete"
50961
+ },
50668
50962
  "osdManager.getConditionSupport": {
50669
50963
  capName: "osd-manager",
50670
50964
  capScope: "system",
@@ -50683,6 +50977,12 @@ Object.freeze({
50683
50977
  addonId: null,
50684
50978
  access: "view"
50685
50979
  },
50980
+ "osdManager.listTemplates": {
50981
+ capName: "osd-manager",
50982
+ capScope: "system",
50983
+ addonId: null,
50984
+ access: "view"
50985
+ },
50686
50986
  "osdManager.previewSlot": {
50687
50987
  capName: "osd-manager",
50688
50988
  capScope: "system",
@@ -50701,6 +51001,12 @@ Object.freeze({
50701
51001
  addonId: null,
50702
51002
  access: "create"
50703
51003
  },
51004
+ "osdManager.upsertTemplate": {
51005
+ capName: "osd-manager",
51006
+ capScope: "system",
51007
+ addonId: null,
51008
+ access: "create"
51009
+ },
50704
51010
  "petFeeder.callPet": {
50705
51011
  capName: "pet-feeder",
50706
51012
  capScope: "device",
@@ -54087,6 +54393,11 @@ Object.freeze({
54087
54393
  }
54088
54394
  });
54089
54395
  Object.freeze({
54396
+ "accessories.getStatus": [{
54397
+ name: "deviceId",
54398
+ form: "single",
54399
+ optional: false
54400
+ }],
54090
54401
  "accessories.setChildHidden": [{
54091
54402
  name: "childDeviceId",
54092
54403
  form: "single",
@@ -54106,6 +54417,11 @@ Object.freeze({
54106
54417
  form: "single",
54107
54418
  optional: false
54108
54419
  }],
54420
+ "airQualitySensor.getStatus": [{
54421
+ name: "deviceId",
54422
+ form: "single",
54423
+ optional: false
54424
+ }],
54109
54425
  "alarmPanel.arm": [{
54110
54426
  name: "deviceId",
54111
54427
  form: "single",
@@ -54116,11 +54432,36 @@ Object.freeze({
54116
54432
  form: "single",
54117
54433
  optional: false
54118
54434
  }],
54435
+ "alarmPanel.getStatus": [{
54436
+ name: "deviceId",
54437
+ form: "single",
54438
+ optional: false
54439
+ }],
54119
54440
  "alarmPanel.trigger": [{
54120
54441
  name: "deviceId",
54121
54442
  form: "single",
54122
54443
  optional: false
54123
54444
  }],
54445
+ "ambientLightSensor.getStatus": [{
54446
+ name: "deviceId",
54447
+ form: "single",
54448
+ optional: false
54449
+ }],
54450
+ "audioAnalysis.applyDeviceSettingsPatch": [{
54451
+ name: "deviceId",
54452
+ form: "single",
54453
+ optional: false
54454
+ }],
54455
+ "audioAnalysis.getDeviceLiveContribution": [{
54456
+ name: "deviceId",
54457
+ form: "single",
54458
+ optional: false
54459
+ }],
54460
+ "audioAnalysis.getDeviceSettingsContribution": [{
54461
+ name: "deviceId",
54462
+ form: "single",
54463
+ optional: false
54464
+ }],
54124
54465
  "audioAnalysis.resolveDeviceSettings": [{
54125
54466
  name: "deviceId",
54126
54467
  form: "single",
@@ -54161,16 +54502,36 @@ Object.freeze({
54161
54502
  form: "single",
54162
54503
  optional: false
54163
54504
  }],
54505
+ "automationControl.getStatus": [{
54506
+ name: "deviceId",
54507
+ form: "single",
54508
+ optional: false
54509
+ }],
54164
54510
  "automationControl.trigger": [{
54165
54511
  name: "deviceId",
54166
54512
  form: "single",
54167
54513
  optional: false
54168
54514
  }],
54515
+ "battery.getStatus": [{
54516
+ name: "deviceId",
54517
+ form: "single",
54518
+ optional: false
54519
+ }],
54169
54520
  "battery.wakeForStream": [{
54170
54521
  name: "deviceId",
54171
54522
  form: "single",
54172
54523
  optional: false
54173
54524
  }],
54525
+ "binary.getStatus": [{
54526
+ name: "deviceId",
54527
+ form: "single",
54528
+ optional: false
54529
+ }],
54530
+ "brightness.getStatus": [{
54531
+ name: "deviceId",
54532
+ form: "single",
54533
+ optional: false
54534
+ }],
54174
54535
  "brightness.setBrightness": [{
54175
54536
  name: "deviceId",
54176
54537
  form: "single",
@@ -54186,6 +54547,26 @@ Object.freeze({
54186
54547
  form: "single",
54187
54548
  optional: false
54188
54549
  }],
54550
+ "cameraCredentials.getStatus": [{
54551
+ name: "deviceId",
54552
+ form: "single",
54553
+ optional: false
54554
+ }],
54555
+ "cameraGridLayout.applyDeviceSettingsPatch": [{
54556
+ name: "deviceId",
54557
+ form: "single",
54558
+ optional: false
54559
+ }],
54560
+ "cameraGridLayout.getDeviceLiveContribution": [{
54561
+ name: "deviceId",
54562
+ form: "single",
54563
+ optional: false
54564
+ }],
54565
+ "cameraGridLayout.getDeviceSettingsContribution": [{
54566
+ name: "deviceId",
54567
+ form: "single",
54568
+ optional: false
54569
+ }],
54189
54570
  "cameraGridLayout.getLayout": [{
54190
54571
  name: "deviceId",
54191
54572
  form: "single",
@@ -54201,6 +54582,21 @@ Object.freeze({
54201
54582
  form: "single",
54202
54583
  optional: false
54203
54584
  }],
54585
+ "cameraPipelineConfig.applyDeviceSettingsPatch": [{
54586
+ name: "deviceId",
54587
+ form: "single",
54588
+ optional: false
54589
+ }],
54590
+ "cameraPipelineConfig.getDeviceLiveContribution": [{
54591
+ name: "deviceId",
54592
+ form: "single",
54593
+ optional: false
54594
+ }],
54595
+ "cameraPipelineConfig.getDeviceSettingsContribution": [{
54596
+ name: "deviceId",
54597
+ form: "single",
54598
+ optional: false
54599
+ }],
54204
54600
  "cameraStreams.getBrokerStreams": [{
54205
54601
  name: "deviceId",
54206
54602
  form: "single",
@@ -54226,6 +54622,16 @@ Object.freeze({
54226
54622
  form: "single",
54227
54623
  optional: false
54228
54624
  }],
54625
+ "carbonMonoxide.getStatus": [{
54626
+ name: "deviceId",
54627
+ form: "single",
54628
+ optional: false
54629
+ }],
54630
+ "climateControl.getStatus": [{
54631
+ name: "deviceId",
54632
+ form: "single",
54633
+ optional: false
54634
+ }],
54229
54635
  "climateControl.setFanMode": [{
54230
54636
  name: "deviceId",
54231
54637
  form: "single",
@@ -54266,16 +54672,41 @@ Object.freeze({
54266
54672
  form: "single",
54267
54673
  optional: false
54268
54674
  }],
54675
+ "color.getStatus": [{
54676
+ name: "deviceId",
54677
+ form: "single",
54678
+ optional: false
54679
+ }],
54269
54680
  "color.setColor": [{
54270
54681
  name: "deviceId",
54271
54682
  form: "single",
54272
54683
  optional: false
54273
54684
  }],
54685
+ "connectivity.getStatus": [{
54686
+ name: "deviceId",
54687
+ form: "single",
54688
+ optional: false
54689
+ }],
54690
+ "consumables.getStatus": [{
54691
+ name: "deviceId",
54692
+ form: "single",
54693
+ optional: false
54694
+ }],
54274
54695
  "consumables.reset": [{
54275
54696
  name: "deviceId",
54276
54697
  form: "single",
54277
54698
  optional: false
54278
54699
  }],
54700
+ "contact.getStatus": [{
54701
+ name: "deviceId",
54702
+ form: "single",
54703
+ optional: false
54704
+ }],
54705
+ "control.getStatus": [{
54706
+ name: "deviceId",
54707
+ form: "single",
54708
+ optional: false
54709
+ }],
54279
54710
  "control.setValue": [{
54280
54711
  name: "deviceId",
54281
54712
  form: "single",
@@ -54286,6 +54717,11 @@ Object.freeze({
54286
54717
  form: "single",
54287
54718
  optional: false
54288
54719
  }],
54720
+ "cover.getStatus": [{
54721
+ name: "deviceId",
54722
+ form: "single",
54723
+ optional: false
54724
+ }],
54289
54725
  "cover.open": [{
54290
54726
  name: "deviceId",
54291
54727
  form: "single",
@@ -54311,6 +54747,11 @@ Object.freeze({
54311
54747
  form: "single",
54312
54748
  optional: false
54313
54749
  }],
54750
+ "dayNight.getStatus": [{
54751
+ name: "deviceId",
54752
+ form: "single",
54753
+ optional: false
54754
+ }],
54314
54755
  "dayNight.setSettings": [{
54315
54756
  name: "deviceId",
54316
54757
  form: "single",
@@ -54321,11 +54762,31 @@ Object.freeze({
54321
54762
  form: "single",
54322
54763
  optional: true
54323
54764
  }],
54765
+ "detectionPipeline.applyDeviceSettingsPatch": [{
54766
+ name: "deviceId",
54767
+ form: "single",
54768
+ optional: false
54769
+ }],
54770
+ "detectionPipeline.getDeviceLiveContribution": [{
54771
+ name: "deviceId",
54772
+ form: "single",
54773
+ optional: false
54774
+ }],
54775
+ "detectionPipeline.getDeviceSettingsContribution": [{
54776
+ name: "deviceId",
54777
+ form: "single",
54778
+ optional: false
54779
+ }],
54324
54780
  "deviceAdminLink.getAdminLink": [{
54325
54781
  name: "deviceId",
54326
54782
  form: "single",
54327
54783
  optional: false
54328
54784
  }],
54785
+ "deviceAdoption.getStatus": [{
54786
+ name: "deviceId",
54787
+ form: "single",
54788
+ optional: false
54789
+ }],
54329
54790
  "deviceAdoption.release": [{
54330
54791
  name: "camDeviceId",
54331
54792
  form: "single",
@@ -54341,6 +54802,11 @@ Object.freeze({
54341
54802
  form: "single",
54342
54803
  optional: false
54343
54804
  }],
54805
+ "deviceDiscovery.getStatus": [{
54806
+ name: "deviceId",
54807
+ form: "single",
54808
+ optional: false
54809
+ }],
54344
54810
  "deviceDiscovery.listDiscovered": [{
54345
54811
  name: "deviceId",
54346
54812
  form: "single",
@@ -54360,6 +54826,21 @@ Object.freeze({
54360
54826
  form: "single",
54361
54827
  optional: false
54362
54828
  }],
54829
+ "deviceExport.applyDeviceSettingsPatch": [{
54830
+ name: "deviceId",
54831
+ form: "single",
54832
+ optional: false
54833
+ }],
54834
+ "deviceExport.getDeviceLiveContribution": [{
54835
+ name: "deviceId",
54836
+ form: "single",
54837
+ optional: false
54838
+ }],
54839
+ "deviceExport.getDeviceSettingsContribution": [{
54840
+ name: "deviceId",
54841
+ form: "single",
54842
+ optional: false
54843
+ }],
54363
54844
  "deviceExtension.listCandidates": [{
54364
54845
  name: "deviceId",
54365
54846
  form: "single",
@@ -54708,6 +55189,26 @@ Object.freeze({
54708
55189
  form: "single",
54709
55190
  optional: false
54710
55191
  }],
55192
+ "deviceStatus.getStatus": [{
55193
+ name: "deviceId",
55194
+ form: "single",
55195
+ optional: false
55196
+ }],
55197
+ "doorbell.getStatus": [{
55198
+ name: "deviceId",
55199
+ form: "single",
55200
+ optional: false
55201
+ }],
55202
+ "enumSensor.getStatus": [{
55203
+ name: "deviceId",
55204
+ form: "single",
55205
+ optional: false
55206
+ }],
55207
+ "eventEmitter.getStatus": [{
55208
+ name: "deviceId",
55209
+ form: "single",
55210
+ optional: false
55211
+ }],
54711
55212
  "faceGallery.getFaceByTrack": [{
54712
55213
  name: "deviceId",
54713
55214
  form: "single",
@@ -54722,6 +55223,11 @@ Object.freeze({
54722
55223
  form: "array",
54723
55224
  optional: true
54724
55225
  }],
55226
+ "fanControl.getStatus": [{
55227
+ name: "deviceId",
55228
+ form: "single",
55229
+ optional: false
55230
+ }],
54725
55231
  "fanControl.setDirection": [{
54726
55232
  name: "deviceId",
54727
55233
  form: "single",
@@ -54742,6 +55248,26 @@ Object.freeze({
54742
55248
  form: "single",
54743
55249
  optional: false
54744
55250
  }],
55251
+ "featureProbe.getStatus": [{
55252
+ name: "deviceId",
55253
+ form: "single",
55254
+ optional: false
55255
+ }],
55256
+ "flood.getStatus": [{
55257
+ name: "deviceId",
55258
+ form: "single",
55259
+ optional: false
55260
+ }],
55261
+ "gas.getStatus": [{
55262
+ name: "deviceId",
55263
+ form: "single",
55264
+ optional: false
55265
+ }],
55266
+ "humidifier.getStatus": [{
55267
+ name: "deviceId",
55268
+ form: "single",
55269
+ optional: false
55270
+ }],
54745
55271
  "humidifier.setMode": [{
54746
55272
  name: "deviceId",
54747
55273
  form: "single",
@@ -54757,11 +55283,26 @@ Object.freeze({
54757
55283
  form: "single",
54758
55284
  optional: false
54759
55285
  }],
55286
+ "humiditySensor.getStatus": [{
55287
+ name: "deviceId",
55288
+ form: "single",
55289
+ optional: false
55290
+ }],
55291
+ "image.getStatus": [{
55292
+ name: "deviceId",
55293
+ form: "single",
55294
+ optional: false
55295
+ }],
54760
55296
  "imageSettings.getOptions": [{
54761
55297
  name: "deviceId",
54762
55298
  form: "single",
54763
55299
  optional: false
54764
55300
  }],
55301
+ "imageSettings.getStatus": [{
55302
+ name: "deviceId",
55303
+ form: "single",
55304
+ optional: false
55305
+ }],
54765
55306
  "imageSettings.setSettings": [{
54766
55307
  name: "deviceId",
54767
55308
  form: "single",
@@ -54772,6 +55313,11 @@ Object.freeze({
54772
55313
  form: "single",
54773
55314
  optional: false
54774
55315
  }],
55316
+ "intercom.getStatus": [{
55317
+ name: "deviceId",
55318
+ form: "single",
55319
+ optional: false
55320
+ }],
54775
55321
  "intercom.handleAnswer": [{
54776
55322
  name: "deviceId",
54777
55323
  form: "single",
@@ -54802,6 +55348,11 @@ Object.freeze({
54802
55348
  form: "single",
54803
55349
  optional: false
54804
55350
  }],
55351
+ "lawnMowerControl.getStatus": [{
55352
+ name: "deviceId",
55353
+ form: "single",
55354
+ optional: false
55355
+ }],
54805
55356
  "lawnMowerControl.pause": [{
54806
55357
  name: "deviceId",
54807
55358
  form: "single",
@@ -54812,6 +55363,11 @@ Object.freeze({
54812
55363
  form: "single",
54813
55364
  optional: false
54814
55365
  }],
55366
+ "lockControl.getStatus": [{
55367
+ name: "deviceId",
55368
+ form: "single",
55369
+ optional: false
55370
+ }],
54815
55371
  "lockControl.lock": [{
54816
55372
  name: "deviceId",
54817
55373
  form: "single",
@@ -54827,6 +55383,11 @@ Object.freeze({
54827
55383
  form: "single",
54828
55384
  optional: false
54829
55385
  }],
55386
+ "mediaPlayer.getStatus": [{
55387
+ name: "deviceId",
55388
+ form: "single",
55389
+ optional: false
55390
+ }],
54830
55391
  "mediaPlayer.next": [{
54831
55392
  name: "deviceId",
54832
55393
  form: "single",
@@ -54887,6 +55448,11 @@ Object.freeze({
54887
55448
  form: "single",
54888
55449
  optional: false
54889
55450
  }],
55451
+ "motion.getStatus": [{
55452
+ name: "deviceId",
55453
+ form: "single",
55454
+ optional: false
55455
+ }],
54890
55456
  "motion.isDetected": [{
54891
55457
  name: "deviceId",
54892
55458
  form: "single",
@@ -54897,11 +55463,31 @@ Object.freeze({
54897
55463
  form: "single",
54898
55464
  optional: false
54899
55465
  }],
55466
+ "motionDetection.applyDeviceSettingsPatch": [{
55467
+ name: "deviceId",
55468
+ form: "single",
55469
+ optional: false
55470
+ }],
55471
+ "motionDetection.getDeviceLiveContribution": [{
55472
+ name: "deviceId",
55473
+ form: "single",
55474
+ optional: false
55475
+ }],
55476
+ "motionDetection.getDeviceSettingsContribution": [{
55477
+ name: "deviceId",
55478
+ form: "single",
55479
+ optional: false
55480
+ }],
54900
55481
  "motionDetection.removeCamera": [{
54901
55482
  name: "deviceId",
54902
55483
  form: "single",
54903
55484
  optional: false
54904
55485
  }],
55486
+ "motionTrigger.getStatus": [{
55487
+ name: "deviceId",
55488
+ form: "single",
55489
+ optional: false
55490
+ }],
54905
55491
  "motionTrigger.setMotionTrigger": [{
54906
55492
  name: "deviceId",
54907
55493
  form: "single",
@@ -54912,6 +55498,11 @@ Object.freeze({
54912
55498
  form: "single",
54913
55499
  optional: false
54914
55500
  }],
55501
+ "motionZones.getStatus": [{
55502
+ name: "deviceId",
55503
+ form: "single",
55504
+ optional: false
55505
+ }],
54915
55506
  "motionZones.setZone": [{
54916
55507
  name: "deviceId",
54917
55508
  form: "single",
@@ -54922,6 +55513,11 @@ Object.freeze({
54922
55513
  form: "single",
54923
55514
  optional: false
54924
55515
  }],
55516
+ "nativeObjectDetection.getStatus": [{
55517
+ name: "deviceId",
55518
+ form: "single",
55519
+ optional: false
55520
+ }],
54925
55521
  "nativeObjectDetection.setEnabled": [{
54926
55522
  name: "deviceId",
54927
55523
  form: "single",
@@ -54937,6 +55533,11 @@ Object.freeze({
54937
55533
  form: "single",
54938
55534
  optional: false
54939
55535
  }],
55536
+ "navigation.getStatus": [{
55537
+ name: "deviceId",
55538
+ form: "single",
55539
+ optional: false
55540
+ }],
54940
55541
  "navigation.goToPoint": [{
54941
55542
  name: "deviceId",
54942
55543
  form: "single",
@@ -54982,6 +55583,11 @@ Object.freeze({
54982
55583
  form: "single",
54983
55584
  optional: false
54984
55585
  }],
55586
+ "networkLink.getStatus": [{
55587
+ name: "deviceId",
55588
+ form: "single",
55589
+ optional: false
55590
+ }],
54985
55591
  "networkQuality.getDeviceStats": [{
54986
55592
  name: "deviceId",
54987
55593
  form: "single",
@@ -55007,11 +55613,26 @@ Object.freeze({
55007
55613
  form: "single",
55008
55614
  optional: false
55009
55615
  }],
55616
+ "notifier.getStatus": [{
55617
+ name: "deviceId",
55618
+ form: "single",
55619
+ optional: false
55620
+ }],
55010
55621
  "notifier.send": [{
55011
55622
  name: "deviceId",
55012
55623
  form: "single",
55013
55624
  optional: false
55014
55625
  }],
55626
+ "numericSensor.getStatus": [{
55627
+ name: "deviceId",
55628
+ form: "single",
55629
+ optional: false
55630
+ }],
55631
+ "osd.getStatus": [{
55632
+ name: "deviceId",
55633
+ form: "single",
55634
+ optional: false
55635
+ }],
55015
55636
  "osd.setOverlay": [{
55016
55637
  name: "deviceId",
55017
55638
  form: "single",
@@ -55071,6 +55692,11 @@ Object.freeze({
55071
55692
  form: "single",
55072
55693
  optional: false
55073
55694
  }],
55695
+ "petFeeder.getStatus": [{
55696
+ name: "deviceId",
55697
+ form: "single",
55698
+ optional: false
55699
+ }],
55074
55700
  "petFeeder.markFoodReplenished": [{
55075
55701
  name: "deviceId",
55076
55702
  form: "single",
@@ -55106,6 +55732,11 @@ Object.freeze({
55106
55732
  form: "single",
55107
55733
  optional: false
55108
55734
  }],
55735
+ "pipelineAnalytics.applyDeviceSettingsPatch": [{
55736
+ name: "deviceId",
55737
+ form: "single",
55738
+ optional: false
55739
+ }],
55109
55740
  "pipelineAnalytics.clearTrackRelation": [{
55110
55741
  name: "deviceId",
55111
55742
  form: "single",
@@ -55146,6 +55777,16 @@ Object.freeze({
55146
55777
  form: "single",
55147
55778
  optional: false
55148
55779
  }],
55780
+ "pipelineAnalytics.getDeviceLiveContribution": [{
55781
+ name: "deviceId",
55782
+ form: "single",
55783
+ optional: false
55784
+ }],
55785
+ "pipelineAnalytics.getDeviceSettingsContribution": [{
55786
+ name: "deviceId",
55787
+ form: "single",
55788
+ optional: false
55789
+ }],
55149
55790
  "pipelineAnalytics.getEventDensity": [{
55150
55791
  name: "deviceId",
55151
55792
  form: "single",
@@ -55361,6 +56002,11 @@ Object.freeze({
55361
56002
  form: "single",
55362
56003
  optional: true
55363
56004
  }],
56005
+ "pipelineOrchestrator.applyDeviceSettingsPatch": [{
56006
+ name: "deviceId",
56007
+ form: "single",
56008
+ optional: false
56009
+ }],
55364
56010
  "pipelineOrchestrator.assignAudio": [{
55365
56011
  name: "deviceId",
55366
56012
  form: "single",
@@ -55406,6 +56052,16 @@ Object.freeze({
55406
56052
  form: "single",
55407
56053
  optional: false
55408
56054
  }],
56055
+ "pipelineOrchestrator.getDeviceLiveContribution": [{
56056
+ name: "deviceId",
56057
+ form: "single",
56058
+ optional: false
56059
+ }],
56060
+ "pipelineOrchestrator.getDeviceSettingsContribution": [{
56061
+ name: "deviceId",
56062
+ form: "single",
56063
+ optional: false
56064
+ }],
55409
56065
  "pipelineOrchestrator.getPipelineAssignment": [{
55410
56066
  name: "deviceId",
55411
56067
  form: "single",
@@ -55526,11 +56182,31 @@ Object.freeze({
55526
56182
  form: "single",
55527
56183
  optional: true
55528
56184
  }],
56185
+ "powerMeter.getStatus": [{
56186
+ name: "deviceId",
56187
+ form: "single",
56188
+ optional: false
56189
+ }],
56190
+ "presence.getStatus": [{
56191
+ name: "deviceId",
56192
+ form: "single",
56193
+ optional: false
56194
+ }],
56195
+ "pressureSensor.getStatus": [{
56196
+ name: "deviceId",
56197
+ form: "single",
56198
+ optional: false
56199
+ }],
55529
56200
  "privacyMask.getOptions": [{
55530
56201
  name: "deviceId",
55531
56202
  form: "single",
55532
56203
  optional: false
55533
56204
  }],
56205
+ "privacyMask.getStatus": [{
56206
+ name: "deviceId",
56207
+ form: "single",
56208
+ optional: false
56209
+ }],
55534
56210
  "privacyMask.setAudioEnabled": [{
55535
56211
  name: "deviceId",
55536
56212
  form: "single",
@@ -55581,6 +56257,11 @@ Object.freeze({
55581
56257
  form: "single",
55582
56258
  optional: false
55583
56259
  }],
56260
+ "ptz.getStatus": [{
56261
+ name: "deviceId",
56262
+ form: "single",
56263
+ optional: false
56264
+ }],
55584
56265
  "ptz.goHome": [{
55585
56266
  name: "deviceId",
55586
56267
  form: "single",
@@ -55671,6 +56352,11 @@ Object.freeze({
55671
56352
  form: "single",
55672
56353
  optional: false
55673
56354
  }],
56355
+ "recordingArchive.applyDeviceSettingsPatch": [{
56356
+ name: "deviceId",
56357
+ form: "single",
56358
+ optional: false
56359
+ }],
55674
56360
  "recordingArchive.deleteFootprint": [{
55675
56361
  name: "deviceId",
55676
56362
  form: "single",
@@ -55691,6 +56377,21 @@ Object.freeze({
55691
56377
  form: "single",
55692
56378
  optional: false
55693
56379
  }],
56380
+ "recordingArchive.getDeviceLiveContribution": [{
56381
+ name: "deviceId",
56382
+ form: "single",
56383
+ optional: false
56384
+ }],
56385
+ "recordingArchive.getDeviceSettingsContribution": [{
56386
+ name: "deviceId",
56387
+ form: "single",
56388
+ optional: false
56389
+ }],
56390
+ "recordingArchive.getStatus": [{
56391
+ name: "deviceId",
56392
+ form: "single",
56393
+ optional: false
56394
+ }],
55694
56395
  "recordingArchive.listOpsLog": [{
55695
56396
  name: "deviceId",
55696
56397
  form: "single",
@@ -55776,6 +56477,11 @@ Object.freeze({
55776
56477
  form: "single",
55777
56478
  optional: false
55778
56479
  }],
56480
+ "recordingOnboard.getStatus": [{
56481
+ name: "deviceId",
56482
+ form: "single",
56483
+ optional: false
56484
+ }],
55779
56485
  "recordingOnboard.setSettings": [{
55780
56486
  name: "deviceId",
55781
56487
  form: "single",
@@ -55806,6 +56512,11 @@ Object.freeze({
55806
56512
  form: "single",
55807
56513
  optional: false
55808
56514
  }],
56515
+ "sceneMonitor.getStatus": [{
56516
+ name: "deviceId",
56517
+ form: "single",
56518
+ optional: false
56519
+ }],
55809
56520
  "sceneMonitor.listScenes": [{
55810
56521
  name: "deviceId",
55811
56522
  form: "single",
@@ -55831,6 +56542,11 @@ Object.freeze({
55831
56542
  form: "single",
55832
56543
  optional: false
55833
56544
  }],
56545
+ "scriptRunner.getStatus": [{
56546
+ name: "deviceId",
56547
+ form: "single",
56548
+ optional: false
56549
+ }],
55834
56550
  "scriptRunner.run": [{
55835
56551
  name: "deviceId",
55836
56552
  form: "single",
@@ -55841,11 +56557,31 @@ Object.freeze({
55841
56557
  form: "single",
55842
56558
  optional: false
55843
56559
  }],
56560
+ "smoke.getStatus": [{
56561
+ name: "deviceId",
56562
+ form: "single",
56563
+ optional: false
56564
+ }],
56565
+ "snapshot.applyDeviceSettingsPatch": [{
56566
+ name: "deviceId",
56567
+ form: "single",
56568
+ optional: false
56569
+ }],
55844
56570
  "snapshot.getDebugState": [{
55845
56571
  name: "deviceId",
55846
56572
  form: "single",
55847
56573
  optional: false
55848
56574
  }],
56575
+ "snapshot.getDeviceLiveContribution": [{
56576
+ name: "deviceId",
56577
+ form: "single",
56578
+ optional: false
56579
+ }],
56580
+ "snapshot.getDeviceSettingsContribution": [{
56581
+ name: "deviceId",
56582
+ form: "single",
56583
+ optional: false
56584
+ }],
55849
56585
  "snapshot.getSnapshot": [{
55850
56586
  name: "deviceId",
55851
56587
  form: "single",
@@ -55862,6 +56598,11 @@ Object.freeze({
55862
56598
  form: "array",
55863
56599
  optional: false
55864
56600
  }],
56601
+ "snapshot.getStatus": [{
56602
+ name: "deviceId",
56603
+ form: "single",
56604
+ optional: false
56605
+ }],
55865
56606
  "snapshot.invalidateCache": [{
55866
56607
  name: "deviceId",
55867
56608
  form: "single",
@@ -55872,6 +56613,11 @@ Object.freeze({
55872
56613
  form: "single",
55873
56614
  optional: false
55874
56615
  }],
56616
+ "streamBroker.applyDeviceSettingsPatch": [{
56617
+ name: "deviceId",
56618
+ form: "single",
56619
+ optional: false
56620
+ }],
55875
56621
  "streamBroker.assignProfile": [{
55876
56622
  name: "deviceId",
55877
56623
  form: "single",
@@ -55887,6 +56633,16 @@ Object.freeze({
55887
56633
  form: "single",
55888
56634
  optional: false
55889
56635
  }],
56636
+ "streamBroker.getDeviceLiveContribution": [{
56637
+ name: "deviceId",
56638
+ form: "single",
56639
+ optional: false
56640
+ }],
56641
+ "streamBroker.getDeviceSettingsContribution": [{
56642
+ name: "deviceId",
56643
+ form: "single",
56644
+ optional: false
56645
+ }],
55890
56646
  "streamBroker.getStreamWithCodec": [{
55891
56647
  name: "deviceId",
55892
56648
  form: "single",
@@ -55947,16 +56703,46 @@ Object.freeze({
55947
56703
  form: "single",
55948
56704
  optional: false
55949
56705
  }],
56706
+ "streamParams.getStatus": [{
56707
+ name: "deviceId",
56708
+ form: "single",
56709
+ optional: false
56710
+ }],
55950
56711
  "streamParams.setProfile": [{
55951
56712
  name: "deviceId",
55952
56713
  form: "single",
55953
56714
  optional: false
55954
56715
  }],
56716
+ "switch.getStatus": [{
56717
+ name: "deviceId",
56718
+ form: "single",
56719
+ optional: false
56720
+ }],
55955
56721
  "switch.setState": [{
55956
56722
  name: "deviceId",
55957
56723
  form: "single",
55958
56724
  optional: false
55959
56725
  }],
56726
+ "tamper.getStatus": [{
56727
+ name: "deviceId",
56728
+ form: "single",
56729
+ optional: false
56730
+ }],
56731
+ "temperatureSensor.getStatus": [{
56732
+ name: "deviceId",
56733
+ form: "single",
56734
+ optional: false
56735
+ }],
56736
+ "update.getStatus": [{
56737
+ name: "deviceId",
56738
+ form: "single",
56739
+ optional: false
56740
+ }],
56741
+ "vacuumControl.getStatus": [{
56742
+ name: "deviceId",
56743
+ form: "single",
56744
+ optional: false
56745
+ }],
55960
56746
  "vacuumControl.locate": [{
55961
56747
  name: "deviceId",
55962
56748
  form: "single",
@@ -55992,6 +56778,11 @@ Object.freeze({
55992
56778
  form: "single",
55993
56779
  optional: false
55994
56780
  }],
56781
+ "valve.getStatus": [{
56782
+ name: "deviceId",
56783
+ form: "single",
56784
+ optional: false
56785
+ }],
55995
56786
  "valve.open": [{
55996
56787
  name: "deviceId",
55997
56788
  form: "single",
@@ -56007,6 +56798,11 @@ Object.freeze({
56007
56798
  form: "single",
56008
56799
  optional: false
56009
56800
  }],
56801
+ "vibration.getStatus": [{
56802
+ name: "deviceId",
56803
+ form: "single",
56804
+ optional: false
56805
+ }],
56010
56806
  "videoclips.dialClipStream": [{
56011
56807
  name: "deviceId",
56012
56808
  form: "single",
@@ -56042,6 +56838,11 @@ Object.freeze({
56042
56838
  form: "single",
56043
56839
  optional: false
56044
56840
  }],
56841
+ "waterHeater.getStatus": [{
56842
+ name: "deviceId",
56843
+ form: "single",
56844
+ optional: false
56845
+ }],
56045
56846
  "waterHeater.setAway": [{
56046
56847
  name: "deviceId",
56047
56848
  form: "single",
@@ -56057,6 +56858,11 @@ Object.freeze({
56057
56858
  form: "single",
56058
56859
  optional: false
56059
56860
  }],
56861
+ "weather.getStatus": [{
56862
+ name: "deviceId",
56863
+ form: "single",
56864
+ optional: false
56865
+ }],
56060
56866
  "webrtcSession.addIceCandidate": [{
56061
56867
  name: "deviceId",
56062
56868
  form: "single",
@@ -57449,10 +58255,10 @@ function limitText(text, max) {
57449
58255
  return `${text.slice(0, max - 3)}...`;
57450
58256
  }
57451
58257
  /** Turn a raw state value into the string that goes into `${value}`. */
57452
- function displayValue(raw, binding) {
58258
+ function displayValue(raw, binding, maxDecimals = binding.maxDecimals) {
57453
58259
  const mapped = binding.valueMap?.[String(raw)];
57454
58260
  if (mapped !== void 0) return mapped;
57455
- if (typeof raw === "number") return truncateDecimals(raw, binding.maxDecimals);
58261
+ if (typeof raw === "number") return truncateDecimals(raw, maxDecimals);
57456
58262
  if (typeof raw === "boolean") return raw ? "ON" : "OFF";
57457
58263
  if (raw === null || raw === void 0) return "";
57458
58264
  if (typeof raw === "string") return raw;
@@ -57466,9 +58272,10 @@ function applyTemplate(template, value, unit) {
57466
58272
  return template.replaceAll("${value}", value).replaceAll("${unit}", unit);
57467
58273
  }
57468
58274
  /** The whole formatting chain: raw value → the text to burn into the slot. */
57469
- function formatSlotText(raw, binding) {
57470
- const value = displayValue(raw, binding);
57471
- return limitText(applyTemplate(binding.template, value, binding.unitLabel ?? "").trim(), binding.maxCharacters);
58275
+ function formatSlotText(raw, binding, options = {}) {
58276
+ const value = displayValue(options.displayRaw ?? raw, binding);
58277
+ const unit = options.unit ?? binding.unitLabel ?? "";
58278
+ return limitText(applyTemplate(binding.template, value, unit).trim(), binding.maxCharacters);
57472
58279
  }
57473
58280
  //#endregion
57474
58281
  //#region src/state-leaves.ts
@@ -57556,6 +58363,92 @@ function readStatePath(slice, path) {
57556
58363
  return current;
57557
58364
  }
57558
58365
  //#endregion
58366
+ //#region src/template-tokens.ts
58367
+ /** Build the placeholder token for one template field. */
58368
+ function fieldToken(field) {
58369
+ return `{${field.deviceId}.${field.capName}.${field.valuePath}}`;
58370
+ }
58371
+ /** Replace every occurrence of `token` in `parser` with `value`. */
58372
+ function replaceToken(parser, token, value) {
58373
+ return parser.replaceAll(token, value);
58374
+ }
58375
+ //#endregion
58376
+ //#region src/unit-display.ts
58377
+ /**
58378
+ * Convert a numeric state value for OSD display using `@camstack/types`
58379
+ * UNIT_TABLE / tryConvertUnit (same set as D699).
58380
+ */
58381
+ /**
58382
+ * Convert `raw` from `sourceUnit` to `displayUnit` when both are known and
58383
+ * same-dimension. On refusal, keep the raw value and the source unit so the
58384
+ * label never lies about the number.
58385
+ */
58386
+ function convertForDisplay(raw, sourceUnit, displayUnit) {
58387
+ const from = sourceUnit !== void 0 ? normalizeUnit(sourceUnit) : void 0;
58388
+ const to = displayUnit !== void 0 ? normalizeUnit(displayUnit) : void 0;
58389
+ if (from === void 0 || to === void 0 || from === to) return {
58390
+ value: raw,
58391
+ unit: to ?? from ?? ""
58392
+ };
58393
+ const converted = tryConvertUnit(raw, from, to);
58394
+ if (converted === void 0) return {
58395
+ value: raw,
58396
+ unit: from
58397
+ };
58398
+ return {
58399
+ value: converted,
58400
+ unit: to
58401
+ };
58402
+ }
58403
+ /**
58404
+ * Best-effort unit from a state slice: sibling `unit` at the same depth as
58405
+ * `valuePath`, else top-level `unit`.
58406
+ */
58407
+ function inferSourceUnit(slice, valuePath) {
58408
+ if (typeof slice !== "object" || slice === null || Array.isArray(slice)) return void 0;
58409
+ const root = { ...slice };
58410
+ const segments = valuePath.split(".");
58411
+ if (segments.length >= 1) {
58412
+ let parent = root;
58413
+ for (const segment of segments.slice(0, -1)) {
58414
+ if (typeof parent !== "object" || parent === null || Array.isArray(parent)) {
58415
+ parent = void 0;
58416
+ break;
58417
+ }
58418
+ parent = { ...parent }[segment];
58419
+ }
58420
+ if (typeof parent === "object" && parent !== null && !Array.isArray(parent)) {
58421
+ const unit = { ...parent }.unit;
58422
+ if (typeof unit === "string" && unit.length > 0) return unit;
58423
+ }
58424
+ }
58425
+ const top = root.unit;
58426
+ return typeof top === "string" && top.length > 0 ? top : void 0;
58427
+ }
58428
+ //#endregion
58429
+ //#region src/named-template-render.ts
58430
+ /** Render one named template's parser from live states. */
58431
+ function renderNamedTemplate(template, facts) {
58432
+ let text = template.parser;
58433
+ if (template.fields.length === 0) return text.trim() === "" ? { reason: `template "${template.id}" has no fields` } : { text };
58434
+ for (const field of template.fields) {
58435
+ const token = fieldToken(field);
58436
+ const deviceState = facts.stateByDevice.get(field.deviceId);
58437
+ if (deviceState === void 0) return { reason: `no state mirrored for device ${field.deviceId}` };
58438
+ const slice = deviceState[field.capName];
58439
+ if (slice === void 0) return { reason: `device ${field.deviceId} publishes no "${field.capName}" state` };
58440
+ const raw = readStatePath(slice, field.valuePath);
58441
+ if (raw === void 0) return { reason: `"${field.capName}.${field.valuePath}" is absent on device ${field.deviceId}` };
58442
+ let rendered;
58443
+ if (typeof raw === "number") rendered = truncateDecimals(convertForDisplay(raw, inferSourceUnit(slice, field.valuePath), field.displayUnit).value, field.maxDecimals);
58444
+ else if (typeof raw === "boolean") rendered = raw ? "ON" : "OFF";
58445
+ else if (raw === null) rendered = "";
58446
+ else rendered = String(raw);
58447
+ text = replaceToken(text, token, rendered);
58448
+ }
58449
+ return { text };
58450
+ }
58451
+ //#endregion
57559
58452
  //#region src/render.ts
57560
58453
  /**
57561
58454
  * Read the source's value. `null` means "unreadable" — a missing device, a
@@ -57570,6 +58463,16 @@ function resolveSource(binding, cameraDeviceId, facts) {
57570
58463
  const value = facts.recognitionByDevice.get(cameraDeviceId)?.[source.recognition];
57571
58464
  return value === void 0 ? { reason: `no recognized ${source.recognition} has been persisted for device ${cameraDeviceId}` } : { value: { raw: value } };
57572
58465
  }
58466
+ if (source.kind === "template") {
58467
+ const named = facts.templatesById.get(source.templateId);
58468
+ if (named === void 0) return { reason: `named template "${source.templateId}" is missing` };
58469
+ const rendered = renderNamedTemplate(named, { stateByDevice: facts.stateByDevice });
58470
+ if ("reason" in rendered) return rendered;
58471
+ return { value: {
58472
+ raw: rendered.text,
58473
+ preformatted: rendered.text
58474
+ } };
58475
+ }
57573
58476
  const deviceId = source.deviceId ?? cameraDeviceId;
57574
58477
  const deviceState = facts.stateByDevice.get(deviceId);
57575
58478
  if (deviceState === void 0) return { reason: `no state mirrored for device ${deviceId}` };
@@ -57577,6 +58480,14 @@ function resolveSource(binding, cameraDeviceId, facts) {
57577
58480
  if (slice === void 0) return { reason: `device ${deviceId} publishes no "${source.capName}" state` };
57578
58481
  const raw = readStatePath(slice, source.valuePath);
57579
58482
  if (raw === void 0) return { reason: `"${source.capName}.${source.valuePath}" is absent on device ${deviceId}` };
58483
+ if (typeof raw === "number") {
58484
+ const converted = convertForDisplay(raw, inferSourceUnit(slice, source.valuePath), source.displayUnit);
58485
+ return { value: {
58486
+ raw,
58487
+ displayRaw: converted.value,
58488
+ unit: converted.unit || binding.unitLabel
58489
+ } };
58490
+ }
57580
58491
  return { value: { raw } };
57581
58492
  }
57582
58493
  /**
@@ -57609,7 +58520,12 @@ function renderSlot(slotId, binding, cameraDeviceId, facts, previousText) {
57609
58520
  if (!gate.open) return settle(slotId, fallback, previousText, "gated", `condition "${gate.failedCondition}"`);
57610
58521
  const resolved = resolveSource(binding, cameraDeviceId, facts);
57611
58522
  if ("reason" in resolved) return settle(slotId, fallback, previousText, "unreadable", resolved.reason);
57612
- return settle(slotId, formatSlotText(resolved.value.raw, binding), previousText, "written");
58523
+ if (resolved.value.preformatted !== void 0 && binding.source.kind === "template") return settle(slotId, limitText(resolved.value.preformatted.trim(), binding.maxCharacters), previousText, "written");
58524
+ const unit = resolved.value.unit ?? binding.unitLabel;
58525
+ return settle(slotId, formatSlotText(resolved.value.raw, binding, {
58526
+ ...resolved.value.displayRaw !== void 0 ? { displayRaw: resolved.value.displayRaw } : {},
58527
+ ...unit !== void 0 ? { unit } : {}
58528
+ }), previousText, "written");
57613
58529
  }
57614
58530
  function settle(slotId, text, previousText, outcome, reason) {
57615
58531
  if (previousText !== void 0 && previousText === text) return {
@@ -57669,6 +58585,24 @@ function sourceOptionsFor(cameraDeviceId, devices, capsByDevice) {
57669
58585
  return options;
57670
58586
  }
57671
58587
  //#endregion
58588
+ //#region src/templates-store.ts
58589
+ /**
58590
+ * Named OSD templates — durable in the addon's own (global) store.
58591
+ */
58592
+ var OSD_TEMPLATES_KEY = "namedTemplates";
58593
+ var OsdNamedTemplatesSchema = array(OsdNamedTemplateSchema);
58594
+ var EMPTY_TEMPLATES = [];
58595
+ function upsertNamedTemplate(prev, template) {
58596
+ const parsed = OsdNamedTemplateSchema.parse(template);
58597
+ return [...prev.filter((t) => t.id !== parsed.id), parsed].toSorted((a, b) => a.name.localeCompare(b.name));
58598
+ }
58599
+ function deleteNamedTemplate(prev, templateId) {
58600
+ return prev.filter((t) => t.id !== templateId);
58601
+ }
58602
+ function findNamedTemplate(templates, templateId) {
58603
+ return templates.find((t) => t.id === templateId);
58604
+ }
58605
+ //#endregion
57672
58606
  //#region src/osd-manager.ts
57673
58607
  /**
57674
58608
  * The `osd-manager` provider — the render loop and the operator surface.
@@ -58032,6 +58966,26 @@ var OsdManager = class {
58032
58966
  renderDevice = async ({ deviceId }) => {
58033
58967
  return { results: [...await this.renderDeviceInternal(deviceId, null)] };
58034
58968
  };
58969
+ listTemplates = async () => {
58970
+ return { templates: [...await this.deps.templates.get()] };
58971
+ };
58972
+ upsertTemplate = async ({ template }) => {
58973
+ const next = upsertNamedTemplate(await this.deps.templates.get(), template);
58974
+ await this.deps.templates.set(next);
58975
+ const stored = findNamedTemplate(next, template.id);
58976
+ if (stored === void 0) throw new Error(`osd-manager: template "${template.id}" missing after upsert`);
58977
+ this.logger.info("osd-manager: named template upserted", { meta: {
58978
+ templateId: stored.id,
58979
+ fieldCount: stored.fields.length
58980
+ } });
58981
+ return { template: stored };
58982
+ };
58983
+ deleteTemplate = async ({ templateId }) => {
58984
+ const prev = await this.deps.templates.get();
58985
+ await this.deps.templates.set(deleteNamedTemplate(prev, templateId));
58986
+ this.logger.info("osd-manager: named template deleted", { meta: { templateId } });
58987
+ return { success: true };
58988
+ };
58035
58989
  async renderAllBound() {
58036
58990
  for (const deviceId of [...this.boundDevices]) try {
58037
58991
  await this.renderDeviceInternal(deviceId, null);
@@ -58099,11 +59053,19 @@ var OsdManager = class {
58099
59053
  async collectFacts(cameraDeviceId, bindings) {
58100
59054
  const wanted = new Set([cameraDeviceId]);
58101
59055
  const needsRecognition = Object.values(bindings).some((binding) => binding.source.kind === "latest-recognition");
59056
+ const templateIds = /* @__PURE__ */ new Set();
58102
59057
  for (const binding of Object.values(bindings)) {
58103
59058
  if (binding.source.kind === "device-state" && binding.source.deviceId !== void 0) wanted.add(binding.source.deviceId);
59059
+ if (binding.source.kind === "template") templateIds.add(binding.source.templateId);
58104
59060
  const gated = binding.conditions?.deviceState?.deviceId;
58105
59061
  if (gated !== void 0) wanted.add(gated);
58106
59062
  }
59063
+ const allTemplates = await this.deps.templates.get();
59064
+ const templatesById = /* @__PURE__ */ new Map();
59065
+ for (const named of allTemplates) {
59066
+ templatesById.set(named.id, named);
59067
+ if (templateIds.has(named.id)) for (const field of named.fields) wanted.add(field.deviceId);
59068
+ }
58107
59069
  const stateByDevice = /* @__PURE__ */ new Map();
58108
59070
  const gateState = /* @__PURE__ */ new Map();
58109
59071
  for (const id of wanted) try {
@@ -58128,7 +59090,8 @@ var OsdManager = class {
58128
59090
  atMs: this.now(),
58129
59091
  stateByDevice,
58130
59092
  recognitionByDevice,
58131
- deviceState: (id) => gateState.get(id)
59093
+ deviceState: (id) => gateState.get(id),
59094
+ templatesById
58132
59095
  };
58133
59096
  }
58134
59097
  async hydrateRecognitions(deviceId) {
@@ -58269,6 +59232,7 @@ var OsdManagerAddon = class extends BaseAddon {
58269
59232
  const ctx = this.ctx;
58270
59233
  const store = new OsdBindingsStore((deviceId, key, schema, fallback) => this.deviceState(deviceId, key, schema, fallback));
58271
59234
  const boundIndex = this.state(OSD_BOUND_INDEX_KEY, OsdBoundIndexSchema, null);
59235
+ const templates = this.state(OSD_TEMPLATES_KEY, OsdNamedTemplatesSchema, EMPTY_TEMPLATES);
58272
59236
  const manager = new OsdManager({
58273
59237
  logger: ctx.logger,
58274
59238
  readOsdStatus: async (deviceId) => {
@@ -58342,7 +59306,8 @@ var OsdManagerAddon = class extends BaseAddon {
58342
59306
  };
58343
59307
  },
58344
59308
  bindings: (deviceId) => store.forDevice(deviceId),
58345
- boundIndex
59309
+ boundIndex,
59310
+ templates
58346
59311
  });
58347
59312
  this.manager = manager;
58348
59313
  this.subscribe({ category: EventCategory.PipelineAnalyticsTrackLifecycle }, (event) => {