@camstack/addon-osd-manager 0.1.156 → 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-Ci65qYWB.mjs → PrivacyMaskSettings-BiQ78hst.mjs} +35 -60
  4. package/dist/{SceneMonitorEditor-BG9mHyrC.mjs → SceneMonitorEditor-CJtJIyMf.mjs} +307 -307
  5. package/dist/_stub.js +11764 -4485
  6. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_osd_manager_page-BDKvyj3r.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-DdhUwSkK.mjs → hostInit-0Mc7Fm1R.mjs} +2 -2
  13. package/dist/index.js +537 -31
  14. package/dist/index.mjs +537 -31
  15. package/dist/{player-overlays-C_Gmuzev.mjs → player-overlays-BPyna-eI.mjs} +14 -14
  16. package/dist/remoteEntry.js +1 -1
  17. package/dist/{responsive-C-9Ko8kt.mjs → responsive-B0yDP9vf.mjs} +31 -27
  18. package/dist/{scene-monitor-copy-DudW1LZg.mjs → scene-monitor-copy-BzX20O6q.mjs} +13 -13
  19. package/dist/{square-CpweDmax.mjs → square-BOMOn_6j.mjs} +29 -4
  20. package/dist/{trash-2-cb_YtxOy.mjs → trash-2-BolGRl2P.mjs} +1 -1
  21. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-Dly_n9Qj.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_osd_manager_page__remoteEntry_js-UY9P_4V2.mjs} +1 -1
  22. package/package.json +1 -1
  23. package/dist/MotionZonesSettings-DaDfau7A.mjs +0 -310
  24. package/dist/_virtual_mf___mfe_internal__addon_osd_manager_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-BTkfkwrM.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
@@ -15445,6 +15445,51 @@ var SourceInfoSchema = object({
15445
15445
  raw: record(string(), unknown()).optional()
15446
15446
  });
15447
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
+ /**
15448
15493
  * device-discovery — device-scoped capability for parents that host /
15449
15494
  * enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
15450
15495
  * integrations). The parent advertises a list of discoverable children,
@@ -39296,28 +39341,29 @@ setOverlay: method(object({
39296
39341
  *
39297
39342
  * This cap owns the other half: a per-(camera, slot) BINDING that says
39298
39343
  * which value feeds the slot, how it is formatted, and under which
39299
- * conditions it is shown at all. One addon renders every binding on every
39300
- * camera, so a new source costs zero driver code.
39301
- *
39302
- * Three deliberate choices, each with a rejected alternative:
39303
- *
39304
- * 1. A source is `(capName, valuePath)` over the kernel's device
39305
- * runtime-state mirror — NOT a closed enum of source kinds. Every
39306
- * cap-keyed slice a device publishes is bindable the day the cap
39307
- * ships. The rejected alternative (one enum member per source, with
39308
- * a resolver branch each) is what makes "add the humidity too" a
39309
- * code change.
39310
- * 2. The display gate reuses `NcConditionsSchema` verbatim — the
39311
- * notification centre's condition vocabulary — rather than a parallel
39312
- * model. An operator who has learned one condition editor has learned
39313
- * both.
39314
- * 3. Because the renderer's facts are device STATE and not a detection
39315
- * record, only a SUBSET of that vocabulary can be answered here.
39316
- * `setSlotBinding` REJECTS the rest at write time (see
39317
- * `getConditionSupport`). It does not accept-then-fail-closed: a
39318
- * condition that can never be true renders a permanently blank
39319
- * 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).
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}`).
39320
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
+ });
39321
39367
  /** Where a slot's value comes from. */
39322
39368
  var OsdSourceSchema = discriminatedUnion("kind", [
39323
39369
  object({
@@ -39336,11 +39382,17 @@ var OsdSourceSchema = discriminatedUnion("kind", [
39336
39382
  deviceId: number().int().optional(),
39337
39383
  capName: string().min(1).max(64),
39338
39384
  /** Dot path inside the slice, e.g. `detected`, `value`, `mode`. */
39339
- 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()
39340
39388
  }),
39341
39389
  object({
39342
39390
  kind: literal("latest-recognition"),
39343
39391
  recognition: _enum(["person", "plate"])
39392
+ }),
39393
+ object({
39394
+ kind: literal("template"),
39395
+ templateId: string().min(1).max(64)
39344
39396
  })
39345
39397
  ]);
39346
39398
  var OsdSlotBindingSchema = object({
@@ -39356,7 +39408,10 @@ var OsdSlotBindingSchema = object({
39356
39408
  * documented workaround for firmwares that reject `.` in overlay text.
39357
39409
  */
39358
39410
  maxDecimals: number().int().min(0).max(4).default(1),
39359
- /** 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
+ */
39360
39415
  unitLabel: string().max(8).optional(),
39361
39416
  /** Raw value → display text, e.g. `{"true":"MOTION","false":""}`. */
39362
39417
  valueMap: record(string(), string()).optional(),
@@ -39510,6 +39565,21 @@ var osdManagerCapability = {
39510
39565
  renderDevice: method(object({ deviceId: number().int() }), object({ results: array(OsdRenderResultSchema) }), {
39511
39566
  kind: "mutation",
39512
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"
39513
39583
  })
39514
39584
  }
39515
39585
  };
@@ -45260,6 +45330,265 @@ var zoneRulesCapability = {
45260
45330
  */
45261
45331
  durability: "restored"
45262
45332
  };
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
+ }
45263
45592
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
45264
45593
  /**
45265
45594
  * Most specific first. Extending this list is how a new device kind becomes
@@ -50628,6 +50957,12 @@ Object.freeze({
50628
50957
  addonId: null,
50629
50958
  access: "create"
50630
50959
  },
50960
+ "osdManager.deleteTemplate": {
50961
+ capName: "osd-manager",
50962
+ capScope: "system",
50963
+ addonId: null,
50964
+ access: "delete"
50965
+ },
50631
50966
  "osdManager.getConditionSupport": {
50632
50967
  capName: "osd-manager",
50633
50968
  capScope: "system",
@@ -50646,6 +50981,12 @@ Object.freeze({
50646
50981
  addonId: null,
50647
50982
  access: "view"
50648
50983
  },
50984
+ "osdManager.listTemplates": {
50985
+ capName: "osd-manager",
50986
+ capScope: "system",
50987
+ addonId: null,
50988
+ access: "view"
50989
+ },
50649
50990
  "osdManager.previewSlot": {
50650
50991
  capName: "osd-manager",
50651
50992
  capScope: "system",
@@ -50664,6 +51005,12 @@ Object.freeze({
50664
51005
  addonId: null,
50665
51006
  access: "create"
50666
51007
  },
51008
+ "osdManager.upsertTemplate": {
51009
+ capName: "osd-manager",
51010
+ capScope: "system",
51011
+ addonId: null,
51012
+ access: "create"
51013
+ },
50667
51014
  "petFeeder.callPet": {
50668
51015
  capName: "pet-feeder",
50669
51016
  capScope: "device",
@@ -57912,10 +58259,10 @@ function limitText(text, max) {
57912
58259
  return `${text.slice(0, max - 3)}...`;
57913
58260
  }
57914
58261
  /** Turn a raw state value into the string that goes into `${value}`. */
57915
- function displayValue(raw, binding) {
58262
+ function displayValue(raw, binding, maxDecimals = binding.maxDecimals) {
57916
58263
  const mapped = binding.valueMap?.[String(raw)];
57917
58264
  if (mapped !== void 0) return mapped;
57918
- if (typeof raw === "number") return truncateDecimals(raw, binding.maxDecimals);
58265
+ if (typeof raw === "number") return truncateDecimals(raw, maxDecimals);
57919
58266
  if (typeof raw === "boolean") return raw ? "ON" : "OFF";
57920
58267
  if (raw === null || raw === void 0) return "";
57921
58268
  if (typeof raw === "string") return raw;
@@ -57929,9 +58276,10 @@ function applyTemplate(template, value, unit) {
57929
58276
  return template.replaceAll("${value}", value).replaceAll("${unit}", unit);
57930
58277
  }
57931
58278
  /** The whole formatting chain: raw value → the text to burn into the slot. */
57932
- function formatSlotText(raw, binding) {
57933
- const value = displayValue(raw, binding);
57934
- 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);
57935
58283
  }
57936
58284
  //#endregion
57937
58285
  //#region src/state-leaves.ts
@@ -58019,6 +58367,92 @@ function readStatePath(slice, path) {
58019
58367
  return current;
58020
58368
  }
58021
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
58022
58456
  //#region src/render.ts
58023
58457
  /**
58024
58458
  * Read the source's value. `null` means "unreadable" — a missing device, a
@@ -58033,6 +58467,16 @@ function resolveSource(binding, cameraDeviceId, facts) {
58033
58467
  const value = facts.recognitionByDevice.get(cameraDeviceId)?.[source.recognition];
58034
58468
  return value === void 0 ? { reason: `no recognized ${source.recognition} has been persisted for device ${cameraDeviceId}` } : { value: { raw: value } };
58035
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
+ }
58036
58480
  const deviceId = source.deviceId ?? cameraDeviceId;
58037
58481
  const deviceState = facts.stateByDevice.get(deviceId);
58038
58482
  if (deviceState === void 0) return { reason: `no state mirrored for device ${deviceId}` };
@@ -58040,6 +58484,14 @@ function resolveSource(binding, cameraDeviceId, facts) {
58040
58484
  if (slice === void 0) return { reason: `device ${deviceId} publishes no "${source.capName}" state` };
58041
58485
  const raw = readStatePath(slice, source.valuePath);
58042
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
+ }
58043
58495
  return { value: { raw } };
58044
58496
  }
58045
58497
  /**
@@ -58072,7 +58524,12 @@ function renderSlot(slotId, binding, cameraDeviceId, facts, previousText) {
58072
58524
  if (!gate.open) return settle(slotId, fallback, previousText, "gated", `condition "${gate.failedCondition}"`);
58073
58525
  const resolved = resolveSource(binding, cameraDeviceId, facts);
58074
58526
  if ("reason" in resolved) return settle(slotId, fallback, previousText, "unreadable", resolved.reason);
58075
- 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");
58076
58533
  }
58077
58534
  function settle(slotId, text, previousText, outcome, reason) {
58078
58535
  if (previousText !== void 0 && previousText === text) return {
@@ -58132,6 +58589,24 @@ function sourceOptionsFor(cameraDeviceId, devices, capsByDevice) {
58132
58589
  return options;
58133
58590
  }
58134
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
58135
58610
  //#region src/osd-manager.ts
58136
58611
  /**
58137
58612
  * The `osd-manager` provider — the render loop and the operator surface.
@@ -58495,6 +58970,26 @@ var OsdManager = class {
58495
58970
  renderDevice = async ({ deviceId }) => {
58496
58971
  return { results: [...await this.renderDeviceInternal(deviceId, null)] };
58497
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
+ };
58498
58993
  async renderAllBound() {
58499
58994
  for (const deviceId of [...this.boundDevices]) try {
58500
58995
  await this.renderDeviceInternal(deviceId, null);
@@ -58562,11 +59057,19 @@ var OsdManager = class {
58562
59057
  async collectFacts(cameraDeviceId, bindings) {
58563
59058
  const wanted = new Set([cameraDeviceId]);
58564
59059
  const needsRecognition = Object.values(bindings).some((binding) => binding.source.kind === "latest-recognition");
59060
+ const templateIds = /* @__PURE__ */ new Set();
58565
59061
  for (const binding of Object.values(bindings)) {
58566
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);
58567
59064
  const gated = binding.conditions?.deviceState?.deviceId;
58568
59065
  if (gated !== void 0) wanted.add(gated);
58569
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
+ }
58570
59073
  const stateByDevice = /* @__PURE__ */ new Map();
58571
59074
  const gateState = /* @__PURE__ */ new Map();
58572
59075
  for (const id of wanted) try {
@@ -58591,7 +59094,8 @@ var OsdManager = class {
58591
59094
  atMs: this.now(),
58592
59095
  stateByDevice,
58593
59096
  recognitionByDevice,
58594
- deviceState: (id) => gateState.get(id)
59097
+ deviceState: (id) => gateState.get(id),
59098
+ templatesById
58595
59099
  };
58596
59100
  }
58597
59101
  async hydrateRecognitions(deviceId) {
@@ -58732,6 +59236,7 @@ var OsdManagerAddon = class extends BaseAddon {
58732
59236
  const ctx = this.ctx;
58733
59237
  const store = new OsdBindingsStore((deviceId, key, schema, fallback) => this.deviceState(deviceId, key, schema, fallback));
58734
59238
  const boundIndex = this.state(OSD_BOUND_INDEX_KEY, OsdBoundIndexSchema, null);
59239
+ const templates = this.state(OSD_TEMPLATES_KEY, OsdNamedTemplatesSchema, EMPTY_TEMPLATES);
58735
59240
  const manager = new OsdManager({
58736
59241
  logger: ctx.logger,
58737
59242
  readOsdStatus: async (deviceId) => {
@@ -58805,7 +59310,8 @@ var OsdManagerAddon = class extends BaseAddon {
58805
59310
  };
58806
59311
  },
58807
59312
  bindings: (deviceId) => store.forDevice(deviceId),
58808
- boundIndex
59313
+ boundIndex,
59314
+ templates
58809
59315
  });
58810
59316
  this.manager = manager;
58811
59317
  this.subscribe({ category: EventCategory.PipelineAnalyticsTrackLifecycle }, (event) => {