@camstack/addon-agent-ui 1.2.294 → 1.2.296

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -5456,7 +5456,7 @@ var ZodIssueCode = {
5456
5456
  var ZodFirstPartyTypeKind;
5457
5457
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5458
5458
  //#endregion
5459
- //#region ../types/dist/sleep-6eG9C8Xl.mjs
5459
+ //#region ../types/dist/sleep-63gM_1JR.mjs
5460
5460
  /**
5461
5461
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5462
5462
  * window to float samples (D455).
@@ -8737,6 +8737,7 @@ var CAP_NAMES = Object.freeze([
8737
8737
  "recording-signal",
8738
8738
  "records",
8739
8739
  "remote",
8740
+ "remote-layout-components",
8740
8741
  "running-state",
8741
8742
  "scene-monitor",
8742
8743
  "script-runner",
@@ -15455,6 +15456,478 @@ object({
15455
15456
  path: ["expr"]
15456
15457
  });
15457
15458
  });
15459
+ /**
15460
+ * Identity envelope for a device's upstream-system metadata.
15461
+ *
15462
+ * Two jobs:
15463
+ *
15464
+ * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
15465
+ * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
15466
+ * WS-Discovery endpoint UUID, …). Provider code keeps a
15467
+ * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
15468
+ * without round-tripping through `stableId` / `id` mappings.
15469
+ *
15470
+ * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
15471
+ * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
15472
+ * for vendors where MAC is the natural key). For systems where the
15473
+ * live dispatch key can be renamed (HA `entity_id` can be edited),
15474
+ * providers detect renames by matching `uniqueId` and patch `id`
15475
+ * in-place so dispatch keeps working.
15476
+ *
15477
+ * **Display unit and precision are NOT stored here.** They are a read-time
15478
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
15479
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
15480
+ * neither is repeated on each sample. The `device_class` tag is an
15481
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
15482
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
15483
+ *
15484
+ * The `raw` escape hatch carries provider-specific attributes that don't
15485
+ * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
15486
+ * details, …). Providers control what lands here.
15487
+ */
15488
+ var SourceInfoSchema = object({
15489
+ /** Live dispatch key — mutable when the source system allows rename. */
15490
+ id: string(),
15491
+ /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
15492
+ system: string(),
15493
+ /** Immutable upstream identifier when available (HA `unique_id`, … ).
15494
+ * Used to detect rename when `id` changes. */
15495
+ uniqueId: string().optional(),
15496
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
15497
+ quantity: ReadingQuantitySchema.optional(),
15498
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
15499
+ dateTimeShape: _enum([
15500
+ "date",
15501
+ "time",
15502
+ "datetime"
15503
+ ]).optional(),
15504
+ /** Provider-specific extras that don't fit the structured slots.
15505
+ * Whitelist what goes here per provider — do NOT dump entire HA
15506
+ * attribute blobs (would explode the meta JSON column). */
15507
+ raw: record(string(), unknown()).optional()
15508
+ });
15509
+ /**
15510
+ * Frozen unit → spec table. `•` in the doc marks each dimension's canonical
15511
+ * unit (`factor: 1, offset: 0`). °F derives from `°C = (°F − 32) × 5/9` ⇒
15512
+ * `factor = 5/9`, `offset = -160/9` (kept as exact expressions, not rounded
15513
+ * decimals). K derives from `°C = K − 273.15`.
15514
+ */
15515
+ var UNIT_TABLE = {
15516
+ "°C": {
15517
+ dimension: "temperature",
15518
+ factor: 1,
15519
+ offset: 0
15520
+ },
15521
+ "°F": {
15522
+ dimension: "temperature",
15523
+ factor: 5 / 9,
15524
+ offset: -160 / 9
15525
+ },
15526
+ K: {
15527
+ dimension: "temperature",
15528
+ factor: 1,
15529
+ offset: -273.15
15530
+ },
15531
+ hPa: {
15532
+ dimension: "pressure",
15533
+ factor: 1,
15534
+ offset: 0
15535
+ },
15536
+ kPa: {
15537
+ dimension: "pressure",
15538
+ factor: 10,
15539
+ offset: 0
15540
+ },
15541
+ Pa: {
15542
+ dimension: "pressure",
15543
+ factor: .01,
15544
+ offset: 0
15545
+ },
15546
+ mbar: {
15547
+ dimension: "pressure",
15548
+ factor: 1,
15549
+ offset: 0
15550
+ },
15551
+ bar: {
15552
+ dimension: "pressure",
15553
+ factor: 1e3,
15554
+ offset: 0
15555
+ },
15556
+ inHg: {
15557
+ dimension: "pressure",
15558
+ factor: 33.8639,
15559
+ offset: 0
15560
+ },
15561
+ mmHg: {
15562
+ dimension: "pressure",
15563
+ factor: 1.33322,
15564
+ offset: 0
15565
+ },
15566
+ psi: {
15567
+ dimension: "pressure",
15568
+ factor: 68.9476,
15569
+ offset: 0
15570
+ },
15571
+ "m/s": {
15572
+ dimension: "speed",
15573
+ factor: 1,
15574
+ offset: 0
15575
+ },
15576
+ "km/h": {
15577
+ dimension: "speed",
15578
+ factor: 1 / 3.6,
15579
+ offset: 0
15580
+ },
15581
+ mph: {
15582
+ dimension: "speed",
15583
+ factor: .44704,
15584
+ offset: 0
15585
+ },
15586
+ kn: {
15587
+ dimension: "speed",
15588
+ factor: .514444,
15589
+ offset: 0
15590
+ },
15591
+ "mm/h": {
15592
+ dimension: "precipitation-rate",
15593
+ factor: 1,
15594
+ offset: 0
15595
+ },
15596
+ "in/h": {
15597
+ dimension: "precipitation-rate",
15598
+ factor: 25.4,
15599
+ offset: 0
15600
+ },
15601
+ m: {
15602
+ dimension: "length",
15603
+ factor: 1,
15604
+ offset: 0
15605
+ },
15606
+ mm: {
15607
+ dimension: "length",
15608
+ factor: .001,
15609
+ offset: 0
15610
+ },
15611
+ cm: {
15612
+ dimension: "length",
15613
+ factor: .01,
15614
+ offset: 0
15615
+ },
15616
+ km: {
15617
+ dimension: "length",
15618
+ factor: 1e3,
15619
+ offset: 0
15620
+ },
15621
+ in: {
15622
+ dimension: "length",
15623
+ factor: .0254,
15624
+ offset: 0
15625
+ },
15626
+ ft: {
15627
+ dimension: "length",
15628
+ factor: .3048,
15629
+ offset: 0
15630
+ },
15631
+ mi: {
15632
+ dimension: "length",
15633
+ factor: 1609.344,
15634
+ offset: 0
15635
+ },
15636
+ "m²": {
15637
+ dimension: "area",
15638
+ factor: 1,
15639
+ offset: 0
15640
+ },
15641
+ lx: {
15642
+ dimension: "illuminance",
15643
+ factor: 1,
15644
+ offset: 0
15645
+ },
15646
+ "W/m²": {
15647
+ dimension: "irradiance",
15648
+ factor: 1,
15649
+ offset: 0
15650
+ },
15651
+ W: {
15652
+ dimension: "power",
15653
+ factor: 1,
15654
+ offset: 0
15655
+ },
15656
+ kW: {
15657
+ dimension: "power",
15658
+ factor: 1e3,
15659
+ offset: 0
15660
+ },
15661
+ VA: {
15662
+ dimension: "apparent-power",
15663
+ factor: 1,
15664
+ offset: 0
15665
+ },
15666
+ Wh: {
15667
+ dimension: "energy",
15668
+ factor: 1,
15669
+ offset: 0
15670
+ },
15671
+ kWh: {
15672
+ dimension: "energy",
15673
+ factor: 1e3,
15674
+ offset: 0
15675
+ },
15676
+ MWh: {
15677
+ dimension: "energy",
15678
+ factor: 1e6,
15679
+ offset: 0
15680
+ },
15681
+ V: {
15682
+ dimension: "voltage",
15683
+ factor: 1,
15684
+ offset: 0
15685
+ },
15686
+ mV: {
15687
+ dimension: "voltage",
15688
+ factor: .001,
15689
+ offset: 0
15690
+ },
15691
+ A: {
15692
+ dimension: "current",
15693
+ factor: 1,
15694
+ offset: 0
15695
+ },
15696
+ mA: {
15697
+ dimension: "current",
15698
+ factor: .001,
15699
+ offset: 0
15700
+ },
15701
+ ppm: {
15702
+ dimension: "concentration-volume",
15703
+ factor: 1,
15704
+ offset: 0
15705
+ },
15706
+ ppb: {
15707
+ dimension: "concentration-volume",
15708
+ factor: .001,
15709
+ offset: 0
15710
+ },
15711
+ "µg/m³": {
15712
+ dimension: "concentration-mass",
15713
+ factor: 1,
15714
+ offset: 0
15715
+ },
15716
+ "mg/m³": {
15717
+ dimension: "concentration-mass",
15718
+ factor: 1e3,
15719
+ offset: 0
15720
+ },
15721
+ kg: {
15722
+ dimension: "mass",
15723
+ factor: 1,
15724
+ offset: 0
15725
+ },
15726
+ g: {
15727
+ dimension: "mass",
15728
+ factor: .001,
15729
+ offset: 0
15730
+ },
15731
+ lb: {
15732
+ dimension: "mass",
15733
+ factor: .453592,
15734
+ offset: 0
15735
+ },
15736
+ oz: {
15737
+ dimension: "mass",
15738
+ factor: .0283495,
15739
+ offset: 0
15740
+ },
15741
+ "%": {
15742
+ dimension: "percentage",
15743
+ factor: 1,
15744
+ offset: 0
15745
+ },
15746
+ L: {
15747
+ dimension: "volume",
15748
+ factor: 1,
15749
+ offset: 0
15750
+ },
15751
+ mL: {
15752
+ dimension: "volume",
15753
+ factor: .001,
15754
+ offset: 0
15755
+ },
15756
+ gal: {
15757
+ dimension: "volume",
15758
+ factor: 3.785411784,
15759
+ offset: 0
15760
+ },
15761
+ s: {
15762
+ dimension: "duration",
15763
+ factor: 1,
15764
+ offset: 0
15765
+ },
15766
+ ms: {
15767
+ dimension: "duration",
15768
+ factor: .001,
15769
+ offset: 0
15770
+ },
15771
+ min: {
15772
+ dimension: "duration",
15773
+ factor: 60,
15774
+ offset: 0
15775
+ },
15776
+ h: {
15777
+ dimension: "duration",
15778
+ factor: 3600,
15779
+ offset: 0
15780
+ },
15781
+ d: {
15782
+ dimension: "duration",
15783
+ factor: 86400,
15784
+ offset: 0
15785
+ },
15786
+ B: {
15787
+ dimension: "information",
15788
+ factor: 1,
15789
+ offset: 0
15790
+ },
15791
+ KiB: {
15792
+ dimension: "information",
15793
+ factor: 1024,
15794
+ offset: 0
15795
+ },
15796
+ MiB: {
15797
+ dimension: "information",
15798
+ factor: 1024 ** 2,
15799
+ offset: 0
15800
+ },
15801
+ GiB: {
15802
+ dimension: "information",
15803
+ factor: 1024 ** 3,
15804
+ offset: 0
15805
+ },
15806
+ TiB: {
15807
+ dimension: "information",
15808
+ factor: 1024 ** 4,
15809
+ offset: 0
15810
+ },
15811
+ kB: {
15812
+ dimension: "information",
15813
+ factor: 1e3,
15814
+ offset: 0
15815
+ },
15816
+ MB: {
15817
+ dimension: "information",
15818
+ factor: 1e6,
15819
+ offset: 0
15820
+ },
15821
+ GB: {
15822
+ dimension: "information",
15823
+ factor: 1e9,
15824
+ offset: 0
15825
+ },
15826
+ "°": {
15827
+ dimension: "angle",
15828
+ factor: 1,
15829
+ offset: 0
15830
+ },
15831
+ dBFS: {
15832
+ dimension: "sound-level",
15833
+ factor: 1,
15834
+ offset: 0
15835
+ },
15836
+ dBm: {
15837
+ dimension: "signal-level",
15838
+ factor: 1,
15839
+ offset: 0
15840
+ },
15841
+ mired: {
15842
+ dimension: "colour-temperature",
15843
+ factor: 1,
15844
+ offset: 0
15845
+ },
15846
+ kbps: {
15847
+ dimension: "bitrate",
15848
+ factor: 1,
15849
+ offset: 0
15850
+ },
15851
+ Mbps: {
15852
+ dimension: "bitrate",
15853
+ factor: 1e3,
15854
+ offset: 0
15855
+ },
15856
+ fps: {
15857
+ dimension: "rate",
15858
+ factor: 1,
15859
+ offset: 0
15860
+ }
15861
+ };
15862
+ /** Resolve a unit's `UnitSpec`, or `undefined` when the spelling is unknown. */
15863
+ function specFor(unit) {
15864
+ return Object.hasOwn(UNIT_TABLE, unit) ? UNIT_TABLE[unit] : void 0;
15865
+ }
15866
+ /** Value in `spec`'s unit → its dimension's canonical unit. */
15867
+ function toCanonical(value, spec) {
15868
+ return value * spec.factor + spec.offset;
15869
+ }
15870
+ /** Canonical-unit value → the unit described by `spec`. */
15871
+ function fromCanonical(canonical, spec) {
15872
+ return (canonical - spec.offset) / spec.factor;
15873
+ }
15874
+ /**
15875
+ * Graceful variant for the render path: returns the converted value, or
15876
+ * `undefined` when the conversion is refused (unknown unit / cross-dimension).
15877
+ * Never throws — a render caller falls back to the raw value + source unit.
15878
+ */
15879
+ function tryConvertUnit(value, from, to) {
15880
+ if (from === to) return value;
15881
+ const fromSpec = specFor(from);
15882
+ if (!fromSpec) return void 0;
15883
+ const toSpec = specFor(to);
15884
+ if (!toSpec) return void 0;
15885
+ if (fromSpec.dimension !== toSpec.dimension) return void 0;
15886
+ return fromCanonical(toCanonical(value, fromSpec), toSpec);
15887
+ }
15888
+ /**
15889
+ * A number a person types for a physical quantity — the input side of the
15890
+ * value + unit contract (D972).
15891
+ *
15892
+ * Readings are stored in the base unit of their quantity (D944); a cap method
15893
+ * param is stored in the unit its input schema DECLARES (`paramMeta`). The
15894
+ * wire pair {@link QuantityValue} says "this number is expressed in this
15895
+ * unit"; {@link convertQuantityInput} brings it to the stored unit and refuses,
15896
+ * with a typed {@link QuantityUnitError}, a unit that cannot be that quantity.
15897
+ */
15898
+ /** A unit that cannot be applied to the number it was given with. */
15899
+ var QuantityUnitError = class extends Error {
15900
+ code;
15901
+ constructor(code, message) {
15902
+ super(`[${code}] ${message}`);
15903
+ this.name = "QuantityUnitError";
15904
+ this.code = code;
15905
+ }
15906
+ };
15907
+ /** The wire pair: `value` is expressed in `unit`. */
15908
+ var QuantityValueSchema = object({
15909
+ value: number(),
15910
+ unit: string().min(1)
15911
+ });
15912
+ /**
15913
+ * Zod `.meta()` bag for a cap METHOD param that is a physical quantity — the
15914
+ * input-side twin of `readingMeta`. The value is carried in `unit` (a gram
15915
+ * portion stays in grams); the unit must belong to the quantity, checked once
15916
+ * when the cap is declared.
15917
+ */
15918
+ function paramMeta(declared) {
15919
+ const base = baseUnitOf(declared.quantity);
15920
+ if (base === void 0) throw new QuantityUnitError("unknown-unit", `quantity "${declared.quantity}" has no unit`);
15921
+ if (tryConvertUnit(1, declared.unit, base) === void 0) throw new QuantityUnitError("cross-dimension", `"${declared.unit}" is not a unit of ${declared.quantity}`);
15922
+ return {
15923
+ quantity: declared.quantity,
15924
+ unit: declared.unit
15925
+ };
15926
+ }
15927
+ object({
15928
+ quantity: ReadingQuantitySchema,
15929
+ unit: string().min(1)
15930
+ });
15458
15931
  /** How a leaf compares a device field to a value. Derived from the field's
15459
15932
  * `kind` in `deviceManager.getWireableFields`, never hand-maintained. */
15460
15933
  var AutomationConditionOperatorSchema = _enum([
@@ -15477,8 +15950,10 @@ var AutomationConditionLeafSchema = object({
15477
15950
  string(),
15478
15951
  number(),
15479
15952
  boolean(),
15480
- array(union([string(), number()]))
15481
- ])
15953
+ array(union([string(), number()])),
15954
+ QuantityValueSchema
15955
+ ]),
15956
+ unit: string().min(1).optional()
15482
15957
  });
15483
15958
  var AutomationConditionStationaryPresentSchema = object({
15484
15959
  kind: literal("stationary-present"),
@@ -15631,7 +16106,13 @@ var AutomationCapStepSchema = object({
15631
16106
  /** Values may carry `{{vars.x}}` slots, or be exactly `{ $var: 'name' }`, which
15632
16107
  * SUBSTITUTE and do not evaluate (§3.2.3). Anything beyond substitution is
15633
16108
  * the expression leaf. */
15634
- args: record(string(), unknown()).optional()
16109
+ args: record(string(), unknown()).optional(),
16110
+ /**
16111
+ * The unit the person picked per arg (display only). `args` always hold the
16112
+ * value in the unit the method's input DECLARES (`paramMeta`); on a write an
16113
+ * arg may be a {@link QuantityValue}, which the save boundary converts.
16114
+ */
16115
+ argUnits: record(string(), string().min(1)).optional()
15635
16116
  });
15636
16117
  /**
15637
16118
  * The steps an internal Action may contain: cap and wait, and nothing that can
@@ -16011,6 +16492,16 @@ object({
16011
16492
  lastRunAt: number().nullable(),
16012
16493
  lastError: string().nullable()
16013
16494
  });
16495
+ /** A bare number accepted as a base unit — the save succeeded, say so. */
16496
+ var UnitWarningSchema = object({
16497
+ /** Where in the saved record, e.g. `conditions.children.0` or `steps.2.args.grams`. */
16498
+ path: string(),
16499
+ deviceId: number().int().optional(),
16500
+ field: string(),
16501
+ value: number(),
16502
+ assumedUnit: string(),
16503
+ message: string()
16504
+ });
16014
16505
  /**
16015
16506
  * notification-output — canonical, capability-gated notification delivery.
16016
16507
  *
@@ -21355,56 +21846,6 @@ var DeviceAdminLinkSchema = object({
21355
21846
  });
21356
21847
  method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema.nullable(), { auth: "admin" });
21357
21848
  /**
21358
- * Identity envelope for a device's upstream-system metadata.
21359
- *
21360
- * Two jobs:
21361
- *
21362
- * 1. **Dispatch key.** `id` is the natural identifier the SOURCE system
21363
- * uses to address this device (HA `entity_id`, Reolink MAC, ONVIF
21364
- * WS-Discovery endpoint UUID, …). Provider code keeps a
21365
- * `Map<sourceInfo.id, IDevice>` to route inbound push events in O(1)
21366
- * without round-tripping through `stableId` / `id` mappings.
21367
- *
21368
- * 2. **Stability bridge.** `uniqueId` is the *immutable* upstream id when
21369
- * one exists (HA `entity.unique_id`, Reolink MAC again — same as `id`
21370
- * for vendors where MAC is the natural key). For systems where the
21371
- * live dispatch key can be renamed (HA `entity_id` can be edited),
21372
- * providers detect renames by matching `uniqueId` and patch `id`
21373
- * in-place so dispatch keeps working.
21374
- *
21375
- * **Display unit and precision are NOT stored here.** They are a read-time
21376
- * preference (D944). A polymorphic reading stores its `quantity` here once,
21377
- * at adopt, and a date/time value stores `dateTimeShape` the same way —
21378
- * neither is repeated on each sample. The `device_class` tag is an
21379
- * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
21380
- * to derive the CamStack `DeviceRole`; it is never stored after that point.
21381
- *
21382
- * The `raw` escape hatch carries provider-specific attributes that don't
21383
- * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
21384
- * details, …). Providers control what lands here.
21385
- */
21386
- var SourceInfoSchema = object({
21387
- /** Live dispatch key — mutable when the source system allows rename. */
21388
- id: string(),
21389
- /** Source system tag — e.g. 'homeassistant' | 'reolink' | 'onvif'. */
21390
- system: string(),
21391
- /** Immutable upstream identifier when available (HA `unique_id`, … ).
21392
- * Used to detect rename when `id` changes. */
21393
- uniqueId: string().optional(),
21394
- /** Quantity of a polymorphic reading, written once at adopt (D944). */
21395
- quantity: ReadingQuantitySchema.optional(),
21396
- /** Date / time / datetime shape of a string reading, written once at adopt. */
21397
- dateTimeShape: _enum([
21398
- "date",
21399
- "time",
21400
- "datetime"
21401
- ]).optional(),
21402
- /** Provider-specific extras that don't fit the structured slots.
21403
- * Whitelist what goes here per provider — do NOT dump entire HA
21404
- * attribute blobs (would explode the meta JSON column). */
21405
- raw: record(string(), unknown()).optional()
21406
- });
21407
- /**
21408
21849
  * device-discovery — device-scoped capability for parents that host /
21409
21850
  * enumerate child devices (Reolink Hub / NVR, ONVIF gateway, future
21410
21851
  * integrations). The parent advertises a list of discoverable children,
@@ -57411,13 +57852,19 @@ method(object({}), array(ActionEntrySchema), { auth: "admin" }), method(object({
57411
57852
  }), method(ActionIdInput, _void(), {
57412
57853
  kind: "mutation",
57413
57854
  auth: "admin"
57414
- }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({ action: ActionEntrySchema }), {
57855
+ }), method(ActionIdInput, object({ traces: array(AutomationRunTraceSchema) }), { auth: "admin" }), method(object({ action: ActionInputSchema }), object({
57856
+ action: ActionEntrySchema,
57857
+ warnings: array(UnitWarningSchema).optional()
57858
+ }), {
57415
57859
  kind: "mutation",
57416
57860
  auth: "admin"
57417
57861
  }), method(object({
57418
57862
  actionId: string().min(1),
57419
57863
  action: ActionInputSchema.partial()
57420
- }), object({ action: ActionEntrySchema }), {
57864
+ }), object({
57865
+ action: ActionEntrySchema,
57866
+ warnings: array(UnitWarningSchema).optional()
57867
+ }), {
57421
57868
  kind: "mutation",
57422
57869
  auth: "admin"
57423
57870
  }), method(ActionIdInput, _void(), {
@@ -57723,6 +58170,30 @@ object({
57723
58170
  at: number()
57724
58171
  }).nullable()
57725
58172
  });
58173
+ var RemoteButtonShapeSchema = _enum([
58174
+ "rect",
58175
+ "rounded",
58176
+ "round",
58177
+ "pill"
58178
+ ]);
58179
+ var RemoteButtonToneSchema = _enum([
58180
+ "neutral",
58181
+ "accent",
58182
+ "danger",
58183
+ "red",
58184
+ "green",
58185
+ "yellow",
58186
+ "blue"
58187
+ ]);
58188
+ var RemoteGroupFrameSchema = _enum([
58189
+ "none",
58190
+ "panel",
58191
+ "divider",
58192
+ "dpad",
58193
+ "rocker-v",
58194
+ "rocker-h",
58195
+ "keypad"
58196
+ ]);
57726
58197
  var RemoteLayoutButtonSchema = object({
57727
58198
  id: string().min(1).max(40),
57728
58199
  /** 0-based grid position. */
@@ -57732,12 +58203,94 @@ var RemoteLayoutButtonSchema = object({
57732
58203
  colSpan: number().int().min(1).max(24),
57733
58204
  icon: string().min(1).max(60).optional(),
57734
58205
  label: string().min(1).max(40).optional(),
57735
- role: RemoteRoleSchema
58206
+ role: RemoteRoleSchema,
58207
+ /** Absent = the plain rendering every layout had before shapes existed. */
58208
+ shape: RemoteButtonShapeSchema.optional(),
58209
+ tone: RemoteButtonToneSchema.optional(),
58210
+ /** The group this button belongs to; it must lie inside that group's rectangle. */
58211
+ groupId: string().min(1).max(40).optional()
58212
+ });
58213
+ /**
58214
+ * A rectangle of the grid that moves as ONE thing and is drawn with a frame: a
58215
+ * titled panel, a section divider, a D-pad ring. `componentId` names the palette
58216
+ * entry it came from (informational: the layout is self-contained).
58217
+ */
58218
+ var RemoteLayoutGroupSchema = object({
58219
+ id: string().min(1).max(40),
58220
+ componentId: string().min(1).max(80),
58221
+ frame: RemoteGroupFrameSchema,
58222
+ row: number().int().min(0).max(24),
58223
+ col: number().int().min(0).max(24),
58224
+ rowSpan: number().int().min(1).max(24),
58225
+ colSpan: number().int().min(1).max(24),
58226
+ title: string().min(1).max(40).optional()
57736
58227
  });
58228
+ function rectsOverlap(a, b) {
58229
+ return a.row < b.row + b.rowSpan && b.row < a.row + a.rowSpan && a.col < b.col + b.colSpan && b.col < a.col + a.colSpan;
58230
+ }
58231
+ function rectContains(outer, inner) {
58232
+ return inner.row >= outer.row && inner.col >= outer.col && inner.row + inner.rowSpan <= outer.row + outer.rowSpan && inner.col + inner.colSpan <= outer.col + outer.colSpan;
58233
+ }
58234
+ /**
58235
+ * The group rules of a layout: inside the grid, no two groups overlap, a
58236
+ * member lies inside its group, a divider holds nothing, and a button that is
58237
+ * not a member never sits inside someone else's rectangle (a group moves as
58238
+ * one thing, so a stray key under it would be left behind).
58239
+ */
58240
+ function refineGroups(grid, ctx) {
58241
+ const groups = grid.groups ?? [];
58242
+ const byId = /* @__PURE__ */ new Map();
58243
+ const fail = (message, path) => {
58244
+ ctx.addIssue({
58245
+ code: "custom",
58246
+ message,
58247
+ path
58248
+ });
58249
+ };
58250
+ groups.forEach((group, index) => {
58251
+ if (byId.has(group.id)) fail(`group id "${group.id}" is used twice`, [
58252
+ "groups",
58253
+ index,
58254
+ "id"
58255
+ ]);
58256
+ byId.set(group.id, group);
58257
+ if (group.row + group.rowSpan > grid.rows || group.col + group.colSpan > grid.columns) fail(`group "${group.id}" leaves the grid`, ["groups", index]);
58258
+ for (let other = 0; other < index; other += 1) {
58259
+ const earlier = groups[other];
58260
+ if (earlier !== void 0 && rectsOverlap(earlier, group)) {
58261
+ fail(`groups "${earlier.id}" and "${group.id}" overlap`, ["groups", index]);
58262
+ return;
58263
+ }
58264
+ }
58265
+ });
58266
+ grid.buttons.forEach((button, index) => {
58267
+ if (button.groupId !== void 0) {
58268
+ const group = byId.get(button.groupId);
58269
+ if (group === void 0) {
58270
+ fail(`button "${button.id}" names the unknown group "${button.groupId}"`, [
58271
+ "buttons",
58272
+ index,
58273
+ "groupId"
58274
+ ]);
58275
+ return;
58276
+ }
58277
+ if (group.frame === "divider") fail(`a divider group ("${group.id}") holds no buttons`, [
58278
+ "buttons",
58279
+ index,
58280
+ "groupId"
58281
+ ]);
58282
+ if (!rectContains(group, button)) fail(`button "${button.id}" lies outside its group "${group.id}"`, ["buttons", index]);
58283
+ return;
58284
+ }
58285
+ const foreign = groups.find((group) => rectsOverlap(group, button));
58286
+ if (foreign !== void 0) fail(`button "${button.id}" lies inside group "${foreign.id}" but is not a member`, ["buttons", index]);
58287
+ });
58288
+ }
57737
58289
  object({
57738
58290
  columns: number().int().min(1).max(24),
57739
58291
  rows: number().int().min(1).max(24),
57740
- buttons: array(RemoteLayoutButtonSchema).max(200)
58292
+ buttons: array(RemoteLayoutButtonSchema).max(200),
58293
+ groups: array(RemoteLayoutGroupSchema).max(50).optional()
57741
58294
  }).superRefine((grid, ctx) => {
57742
58295
  const ids = /* @__PURE__ */ new Set();
57743
58296
  const taken = /* @__PURE__ */ new Map();
@@ -57774,6 +58327,7 @@ object({
57774
58327
  taken.set(cell, button.id);
57775
58328
  }
57776
58329
  });
58330
+ refineGroups(grid, ctx);
57777
58331
  });
57778
58332
  var RemoteBoundCommandSchema = object({
57779
58333
  role: RemoteRoleSchema,
@@ -57905,6 +58459,91 @@ method(object({ kind: TemplateKindSchema.optional() }), array(TemplateRecordSche
57905
58459
  kind: "mutation",
57906
58460
  auth: "admin"
57907
58461
  });
58462
+ var RemoteComponentCategorySchema = _enum([
58463
+ "power",
58464
+ "navigation",
58465
+ "volume",
58466
+ "numbers",
58467
+ "media",
58468
+ "colour",
58469
+ "apps",
58470
+ "layout"
58471
+ ]);
58472
+ /** `<namespace>:<name>`, both lowercase slugs — `core:dpad`, `<addonId>:<name>`. */
58473
+ var COMPONENT_ID_RE = /^[a-z0-9][a-z0-9_.-]*:[a-z0-9][a-z0-9_-]*$/;
58474
+ var RemoteComponentButtonSchema = object({
58475
+ /** Joined to the instance id when the component is inserted. */
58476
+ idSuffix: string().regex(/^[a-z0-9][a-z0-9_-]{0,19}$/, { message: "a short lowercase slug" }),
58477
+ row: number().int().min(0).max(24),
58478
+ col: number().int().min(0).max(24),
58479
+ rowSpan: number().int().min(1).max(24),
58480
+ colSpan: number().int().min(1).max(24),
58481
+ role: RemoteRoleSchema,
58482
+ icon: string().min(1).max(60).optional(),
58483
+ label: string().min(1).max(40).optional(),
58484
+ shape: RemoteButtonShapeSchema.optional(),
58485
+ tone: RemoteButtonToneSchema.optional()
58486
+ });
58487
+ var RemoteLayoutComponentSchema = object({
58488
+ id: string().max(80).regex(COMPONENT_ID_RE, { message: "<namespace>:<name>, lowercase" }),
58489
+ name: string().min(1).max(60),
58490
+ nameI18n: object({
58491
+ en: string().min(1).max(60),
58492
+ it: string().min(1).max(60)
58493
+ }).optional(),
58494
+ /** A display icon key, resolved by the UI like a button icon. */
58495
+ icon: string().min(1).max(60),
58496
+ category: RemoteComponentCategorySchema,
58497
+ size: object({
58498
+ rows: number().int().min(1).max(24),
58499
+ cols: number().int().min(1).max(24)
58500
+ }),
58501
+ /** The frame drawn around the whole component, or null for loose buttons. */
58502
+ group: object({
58503
+ frame: RemoteGroupFrameSchema,
58504
+ title: string().min(1).max(40).optional()
58505
+ }).nullable(),
58506
+ buttons: array(RemoteComponentButtonSchema).max(40)
58507
+ }).superRefine((component, ctx) => {
58508
+ const suffixes = /* @__PURE__ */ new Set();
58509
+ component.buttons.forEach((button, index) => {
58510
+ if (suffixes.has(button.idSuffix)) ctx.addIssue({
58511
+ code: "custom",
58512
+ message: `button "${button.idSuffix}" is listed twice`,
58513
+ path: [
58514
+ "buttons",
58515
+ index,
58516
+ "idSuffix"
58517
+ ]
58518
+ });
58519
+ suffixes.add(button.idSuffix);
58520
+ if (button.row + button.rowSpan > component.size.rows || button.col + button.colSpan > component.size.cols) {
58521
+ ctx.addIssue({
58522
+ code: "custom",
58523
+ message: `button "${button.idSuffix}" leaves the component`,
58524
+ path: ["buttons", index]
58525
+ });
58526
+ return;
58527
+ }
58528
+ for (let other = 0; other < index; other += 1) {
58529
+ const earlier = component.buttons[other];
58530
+ if (earlier !== void 0 && rectsOverlap(earlier, button)) {
58531
+ ctx.addIssue({
58532
+ code: "custom",
58533
+ message: `buttons "${earlier.idSuffix}" and "${button.idSuffix}" overlap`,
58534
+ path: ["buttons", index]
58535
+ });
58536
+ return;
58537
+ }
58538
+ }
58539
+ });
58540
+ if (component.group?.frame === "divider" && component.buttons.length > 0) ctx.addIssue({
58541
+ code: "custom",
58542
+ message: "a divider component holds no buttons",
58543
+ path: ["buttons"]
58544
+ });
58545
+ });
58546
+ method(object({}), array(RemoteLayoutComponentSchema), { auth: "protected" });
57908
58547
  DeviceType.Button, method(object({ deviceId: number().int().nonnegative() }), _void(), {
57909
58548
  kind: "mutation",
57910
58549
  auth: "protected"
@@ -62064,6 +62703,14 @@ var PetFeederDeviceStatusSchema = _enum([
62064
62703
  "on_batteries"
62065
62704
  ]);
62066
62705
  var gramsPortion = number().int().min(4).max(200);
62706
+ /** A portion REQUESTED of the feeder is a mass the method takes in GRAMS (the
62707
+ * hardware's own unit). The bounds are grams; a caller entering another unit
62708
+ * converts to grams first (`paramMeta`, D972). The STATUS fields above stay
62709
+ * plain grams — they are not readings in the kilogram base. */
62710
+ var gramsParam = gramsPortion.meta(paramMeta({
62711
+ quantity: "mass",
62712
+ unit: "g"
62713
+ }));
62067
62714
  object({
62068
62715
  /** Food in the TANK (hopper), as the feeder's own sensor reports it
62069
62716
  * (`state.food`). A coarse level code, not grams — `0` is empty. The
@@ -62138,9 +62785,9 @@ object({
62138
62785
  });
62139
62786
  DeviceType.PetFeeder, method(object({
62140
62787
  deviceId: number().int().nonnegative(),
62141
- grams: gramsPortion.optional(),
62142
- hopper1: gramsPortion.optional(),
62143
- hopper2: gramsPortion.optional()
62788
+ grams: gramsParam.optional(),
62789
+ hopper1: gramsParam.optional(),
62790
+ hopper2: gramsParam.optional()
62144
62791
  }), _void(), {
62145
62792
  kind: "mutation",
62146
62793
  auth: "protected"
@@ -66832,6 +67479,87 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
66832
67479
  kind: "mutation",
66833
67480
  auth: "admin"
66834
67481
  });
67482
+ var btn = (idSuffix, row, col, init) => ({
67483
+ idSuffix,
67484
+ row,
67485
+ col,
67486
+ rowSpan: 1,
67487
+ colSpan: 1,
67488
+ ...init
67489
+ });
67490
+ function rocker(id, name, it, frame, title, buttons, size) {
67491
+ return {
67492
+ id,
67493
+ name,
67494
+ nameI18n: {
67495
+ en: name,
67496
+ it
67497
+ },
67498
+ icon: frame === "rocker-v" ? "chevrons-up-down" : "chevrons-left-right",
67499
+ category: id.includes("channel") || id.includes("volume") ? "volume" : "navigation",
67500
+ size,
67501
+ group: title === null ? { frame } : {
67502
+ frame,
67503
+ title
67504
+ },
67505
+ buttons
67506
+ };
67507
+ }
67508
+ btn("power", 0, 0, {
67509
+ role: "power",
67510
+ shape: "round",
67511
+ tone: "danger"
67512
+ }), btn("up", 0, 1, { role: "up" }), btn("left", 1, 0, { role: "left" }), btn("ok", 1, 1, {
67513
+ role: "ok",
67514
+ shape: "round",
67515
+ tone: "accent"
67516
+ }), btn("right", 1, 2, { role: "right" }), btn("down", 2, 1, { role: "down" }), rocker("core:rocker-volume-v", "Volume rocker", "Bilanciere volume", "rocker-v", "VOL", [btn("up", 0, 0, {
67517
+ role: "volume_up",
67518
+ shape: "pill"
67519
+ }), btn("down", 1, 0, {
67520
+ role: "volume_down",
67521
+ shape: "pill"
67522
+ })], {
67523
+ rows: 2,
67524
+ cols: 1
67525
+ }), rocker("core:rocker-channel-v", "Channel rocker", "Bilanciere canali", "rocker-v", "CH", [btn("up", 0, 0, {
67526
+ role: "channel_up",
67527
+ shape: "pill"
67528
+ }), btn("down", 1, 0, {
67529
+ role: "channel_down",
67530
+ shape: "pill"
67531
+ })], {
67532
+ rows: 2,
67533
+ cols: 1
67534
+ }), rocker("core:rocker-h", "Minus / plus rocker", "Bilanciere meno / più", "rocker-h", null, [btn("minus", 0, 0, {
67535
+ role: "custom:minus",
67536
+ label: "−",
67537
+ shape: "pill"
67538
+ }), btn("plus", 0, 1, {
67539
+ role: "custom:plus",
67540
+ label: "+",
67541
+ shape: "pill"
67542
+ })], {
67543
+ rows: 1,
67544
+ cols: 2
67545
+ }), [...Array.from({ length: 9 }, (_, i) => btn(`d${i + 1}`, Math.floor(i / 3), i % 3, { role: `digit_${i + 1}` })), btn("d0", 3, 1, { role: "digit_0" })], btn("red", 0, 0, {
67546
+ role: "custom:color_red",
67547
+ tone: "red"
67548
+ }), btn("green", 0, 1, {
67549
+ role: "custom:color_green",
67550
+ tone: "green"
67551
+ }), btn("yellow", 0, 2, {
67552
+ role: "custom:color_yellow",
67553
+ tone: "yellow"
67554
+ }), btn("blue", 0, 3, {
67555
+ role: "custom:color_blue",
67556
+ tone: "blue"
67557
+ }), btn("rewind", 0, 0, { role: "rewind" }), btn("play", 0, 1, { role: "play" }), btn("pause", 0, 2, { role: "pause" }), btn("forward", 0, 3, { role: "fast_forward" }), btn("stop", 0, 4, { role: "stop" }), [
67558
+ 1,
67559
+ 2,
67560
+ 3,
67561
+ 4
67562
+ ].map((n) => btn(`app${n}`, 0, n - 1, { role: `custom:app_slot_${n}` })), btn("back", 0, 0, { role: "back" }), btn("home", 0, 1, { role: "home" }), btn("menu", 0, 2, { role: "menu" });
66835
67563
  discriminatedUnion("kind", [object({
66836
67564
  kind: literal("raw"),
66837
67565
  timings: array(number().int()),
@@ -74696,6 +75424,13 @@ Object.freeze({
74696
75424
  access: "create",
74697
75425
  role: "admin"
74698
75426
  },
75427
+ "remoteLayoutComponents.listComponents": {
75428
+ capName: "remote-layout-components",
75429
+ capScope: "system",
75430
+ addonId: null,
75431
+ access: "view",
75432
+ role: "admin"
75433
+ },
74699
75434
  "runningState.getStatus": {
74700
75435
  capName: "running-state",
74701
75436
  capScope: "device",
@@ -78739,7 +79474,8 @@ var APPLIANCE_CAP_VOCABULARY = {
78739
79474
  min: 4,
78740
79475
  max: 200,
78741
79476
  step: 1,
78742
- unit: "g"
79477
+ unit: "g",
79478
+ quantity: "mass"
78743
79479
  },
78744
79480
  optional: true
78745
79481
  }]
@@ -78758,7 +79494,8 @@ var APPLIANCE_CAP_VOCABULARY = {
78758
79494
  min: 4,
78759
79495
  max: 200,
78760
79496
  step: 1,
78761
- unit: "g"
79497
+ unit: "g",
79498
+ quantity: "mass"
78762
79499
  },
78763
79500
  optional: true
78764
79501
  }, {
@@ -78769,7 +79506,8 @@ var APPLIANCE_CAP_VOCABULARY = {
78769
79506
  min: 4,
78770
79507
  max: 200,
78771
79508
  step: 1,
78772
- unit: "g"
79509
+ unit: "g",
79510
+ quantity: "mass"
78773
79511
  },
78774
79512
  optional: true
78775
79513
  }]
@@ -80869,6 +81607,7 @@ var SYSTEM_CAP_VOCABULARY = {
80869
81607
  "recording-export": system("recording exports — requested from the timeline, not an automation step"),
80870
81608
  records: system(INTERNAL),
80871
81609
  remote: system("the per-device remote setup — authored in the Customize remote editor, sent through the remote UI"),
81610
+ "remote-layout-components": system("the palette of the remote template editor — read-only data, never actuated"),
80872
81611
  "server-management": system(CLUSTER),
80873
81612
  "settings-store": system(INTERNAL),
80874
81613
  "smtp-provider": system(INTERNAL),
@@ -85200,7 +85939,7 @@ var AgentUIAddon = class extends BaseAddon {
85200
85939
  capability: adminUiCapability,
85201
85940
  provider: {
85202
85941
  getStaticDir: async () => ({ staticDir: path.resolve(__dirname) }),
85203
- getVersion: async () => ({ version: "1.2.294" })
85942
+ getVersion: async () => ({ version: "1.2.296" })
85204
85943
  }
85205
85944
  }];
85206
85945
  }