@camstack/addon-provider-tuya 0.2.145 → 0.2.147

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
@@ -819,7 +819,7 @@ async function discoverDevices(options = {}, binder = createDgramBinder()) {
819
819
  return [...byId.values()];
820
820
  }
821
821
  //#endregion
822
- //#region ../types/dist/event-category-DHXxB8t9.mjs
822
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
823
823
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
824
824
  EventCategory["SystemBoot"] = "system.boot";
825
825
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -1145,6 +1145,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
1145
1145
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
1146
1146
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
1147
1147
  /**
1148
+ * A camera's package LLM confirm has failed closed three times in a row
1149
+ * (D931, 2026-10-26). One edge per camera until
1150
+ * {@link PipelinePackageLlmAnswered}.
1151
+ */
1152
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
1153
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
1154
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
1155
+ /**
1148
1156
  * Per-camera pipeline config was mutated by the orchestrator
1149
1157
  * (a settings change via `setAgentInferenceDevices` /
1150
1158
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -6254,7 +6262,7 @@ var ZodIssueCode = {
6254
6262
  var ZodFirstPartyTypeKind;
6255
6263
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6256
6264
  //#endregion
6257
- //#region ../types/dist/sleep-BErAxhul.mjs
6265
+ //#region ../types/dist/sleep--jXS9hWo.mjs
6258
6266
  /**
6259
6267
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6260
6268
  * window to float samples (D455).
@@ -8808,6 +8816,99 @@ var RecordKindDeclarationSchema = object({
8808
8816
  }).strict();
8809
8817
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8810
8818
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8819
+ var READING_QUANTITIES = [
8820
+ "temperature",
8821
+ "pressure",
8822
+ "speed",
8823
+ "precipitation",
8824
+ "precipitation-rate",
8825
+ "length",
8826
+ "area",
8827
+ "illuminance",
8828
+ "irradiance",
8829
+ "power",
8830
+ "energy",
8831
+ "voltage",
8832
+ "current",
8833
+ "apparent-power",
8834
+ "humidity",
8835
+ "percentage",
8836
+ "concentration-volume",
8837
+ "concentration-mass",
8838
+ "concentration-ppb",
8839
+ "volume",
8840
+ "duration",
8841
+ "duration-ms",
8842
+ "information",
8843
+ "sound-level",
8844
+ "signal-level",
8845
+ "colour-temperature",
8846
+ "angle",
8847
+ "bitrate",
8848
+ "rate",
8849
+ "mass",
8850
+ "index",
8851
+ "ratio",
8852
+ "count",
8853
+ "none",
8854
+ "device"
8855
+ ];
8856
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8857
+ new Set(READING_QUANTITIES);
8858
+ /**
8859
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8860
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8861
+ */
8862
+ var QUANTITY_BASE = {
8863
+ temperature: "°C",
8864
+ pressure: "hPa",
8865
+ speed: "m/s",
8866
+ precipitation: "mm",
8867
+ "precipitation-rate": "mm/h",
8868
+ length: "m",
8869
+ area: "m²",
8870
+ illuminance: "lx",
8871
+ irradiance: "W/m²",
8872
+ power: "W",
8873
+ energy: "kWh",
8874
+ voltage: "V",
8875
+ current: "A",
8876
+ "apparent-power": "VA",
8877
+ humidity: "%",
8878
+ percentage: "%",
8879
+ "concentration-volume": "ppm",
8880
+ "concentration-mass": "µg/m³",
8881
+ "concentration-ppb": "ppb",
8882
+ volume: "L",
8883
+ duration: "s",
8884
+ "duration-ms": "ms",
8885
+ information: "B",
8886
+ "sound-level": "dBFS",
8887
+ "signal-level": "dBm",
8888
+ "colour-temperature": "mired",
8889
+ angle: "°",
8890
+ bitrate: "kbps",
8891
+ rate: "fps",
8892
+ mass: "kg",
8893
+ index: "none",
8894
+ ratio: "none",
8895
+ count: "none",
8896
+ none: "none",
8897
+ device: "device"
8898
+ };
8899
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8900
+ function baseUnitOf(quantity) {
8901
+ const symbol = QUANTITY_BASE[quantity];
8902
+ if (symbol === "none" || symbol === "device") return void 0;
8903
+ return symbol;
8904
+ }
8905
+ function readingMeta(quantity, extras) {
8906
+ return {
8907
+ quantity,
8908
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8909
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8910
+ };
8911
+ }
8811
8912
  /** Shared Zod schemas used across streaming capabilities. */
8812
8913
  var CamProfileSchema = _enum([
8813
8914
  "high",
@@ -8835,8 +8936,8 @@ var CamStreamKindSchema = _enum([
8835
8936
  "derived"
8836
8937
  ]);
8837
8938
  var CamStreamResolutionSchema = object({
8838
- width: number().int().positive(),
8839
- height: number().int().positive()
8939
+ width: number().int().positive().meta(readingMeta("none")),
8940
+ height: number().int().positive().meta(readingMeta("none"))
8840
8941
  });
8841
8942
  var CameraStreamSchema = object({
8842
8943
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -9354,6 +9455,9 @@ var CAP_NAMES = Object.freeze([
9354
9455
  "filesystem-browse",
9355
9456
  "flood",
9356
9457
  "gas",
9458
+ "group-active",
9459
+ "group-commands",
9460
+ "group-status",
9357
9461
  "heating-valve",
9358
9462
  "humidifier",
9359
9463
  "humidity-sensor",
@@ -9374,6 +9478,7 @@ var CAP_NAMES = Object.freeze([
9374
9478
  "login-method",
9375
9479
  "media-player",
9376
9480
  "mesh-network",
9481
+ "meter",
9377
9482
  "metrics-provider",
9378
9483
  "model-convert",
9379
9484
  "model-distributor",
@@ -9483,14 +9588,6 @@ Object.freeze({
9483
9588
  {
9484
9589
  "path": "aqi",
9485
9590
  "valueType": "number"
9486
- },
9487
- {
9488
- "path": "unit",
9489
- "valueType": "string"
9490
- },
9491
- {
9492
- "path": "precision",
9493
- "valueType": "number"
9494
9591
  }
9495
9592
  ],
9496
9593
  "alarm-panel": [
@@ -9526,20 +9623,10 @@ Object.freeze({
9526
9623
  "valueType": "boolean"
9527
9624
  }
9528
9625
  ],
9529
- "ambient-light-sensor": [
9530
- {
9531
- "path": "lux",
9532
- "valueType": "number"
9533
- },
9534
- {
9535
- "path": "unit",
9536
- "valueType": "string"
9537
- },
9538
- {
9539
- "path": "precision",
9540
- "valueType": "number"
9541
- }
9542
- ],
9626
+ "ambient-light-sensor": [{
9627
+ "path": "lux",
9628
+ "valueType": "number"
9629
+ }],
9543
9630
  "audio-metrics": [
9544
9631
  {
9545
9632
  "path": "ts",
@@ -9619,6 +9706,10 @@ Object.freeze({
9619
9706
  "path": "lastUpdated",
9620
9707
  "valueType": "number"
9621
9708
  },
9709
+ {
9710
+ "path": "volts",
9711
+ "valueType": "number"
9712
+ },
9622
9713
  {
9623
9714
  "path": "lastContactAt",
9624
9715
  "valueType": "number"
@@ -9861,10 +9952,6 @@ Object.freeze({
9861
9952
  "datetime"
9862
9953
  ]
9863
9954
  },
9864
- {
9865
- "path": "unit",
9866
- "valueType": "string"
9867
- },
9868
9955
  {
9869
9956
  "path": "min",
9870
9957
  "valueType": "number"
@@ -9876,19 +9963,6 @@ Object.freeze({
9876
9963
  {
9877
9964
  "path": "step",
9878
9965
  "valueType": "number"
9879
- },
9880
- {
9881
- "path": "precision",
9882
- "valueType": "number"
9883
- },
9884
- {
9885
- "path": "format",
9886
- "valueType": "enum",
9887
- "enumValues": [
9888
- "date",
9889
- "time",
9890
- "datetime"
9891
- ]
9892
9966
  }
9893
9967
  ],
9894
9968
  "cover": [
@@ -10129,13 +10203,8 @@ Object.freeze({
10129
10203
  "path": "value",
10130
10204
  "valueType": "string"
10131
10205
  }, {
10132
- "path": "format",
10133
- "valueType": "enum",
10134
- "enumValues": [
10135
- "date",
10136
- "time",
10137
- "datetime"
10138
- ]
10206
+ "path": "timestamp",
10207
+ "valueType": "boolean"
10139
10208
  }],
10140
10209
  "event-emitter": [
10141
10210
  {
@@ -10208,6 +10277,24 @@ Object.freeze({
10208
10277
  "path": "detected",
10209
10278
  "valueType": "boolean"
10210
10279
  }],
10280
+ "group-active": [{
10281
+ "path": "active",
10282
+ "valueType": "boolean"
10283
+ }],
10284
+ "group-status": [
10285
+ {
10286
+ "path": "included",
10287
+ "valueType": "number"
10288
+ },
10289
+ {
10290
+ "path": "excluded",
10291
+ "valueType": "number"
10292
+ },
10293
+ {
10294
+ "path": "total",
10295
+ "valueType": "number"
10296
+ }
10297
+ ],
10211
10298
  "heating-valve": [{
10212
10299
  "path": "position",
10213
10300
  "valueType": "number"
@@ -10242,20 +10329,10 @@ Object.freeze({
10242
10329
  "valueType": "number"
10243
10330
  }
10244
10331
  ],
10245
- "humidity-sensor": [
10246
- {
10247
- "path": "percent",
10248
- "valueType": "number"
10249
- },
10250
- {
10251
- "path": "unit",
10252
- "valueType": "string"
10253
- },
10254
- {
10255
- "path": "precision",
10256
- "valueType": "number"
10257
- }
10258
- ],
10332
+ "humidity-sensor": [{
10333
+ "path": "percent",
10334
+ "valueType": "number"
10335
+ }],
10259
10336
  "image": [{
10260
10337
  "path": "url",
10261
10338
  "valueType": "string"
@@ -10463,6 +10540,10 @@ Object.freeze({
10463
10540
  ]
10464
10541
  }
10465
10542
  ],
10543
+ "meter": [{
10544
+ "path": "value",
10545
+ "valueType": "number"
10546
+ }],
10466
10547
  "motion": [
10467
10548
  {
10468
10549
  "path": "detected",
@@ -10594,20 +10675,10 @@ Object.freeze({
10594
10675
  "valueType": "boolean"
10595
10676
  }
10596
10677
  ],
10597
- "numeric-sensor": [
10598
- {
10599
- "path": "value",
10600
- "valueType": "number"
10601
- },
10602
- {
10603
- "path": "unit",
10604
- "valueType": "string"
10605
- },
10606
- {
10607
- "path": "precision",
10608
- "valueType": "number"
10609
- }
10610
- ],
10678
+ "numeric-sensor": [{
10679
+ "path": "value",
10680
+ "valueType": "number"
10681
+ }],
10611
10682
  "pet-feeder": [
10612
10683
  {
10613
10684
  "path": "foodLevel",
@@ -10699,14 +10770,6 @@ Object.freeze({
10699
10770
  {
10700
10771
  "path": "powerFactor",
10701
10772
  "valueType": "number"
10702
- },
10703
- {
10704
- "path": "unit",
10705
- "valueType": "string"
10706
- },
10707
- {
10708
- "path": "precision",
10709
- "valueType": "number"
10710
10773
  }
10711
10774
  ],
10712
10775
  "presence": [
@@ -10735,20 +10798,10 @@ Object.freeze({
10735
10798
  "valueType": "number"
10736
10799
  }
10737
10800
  ],
10738
- "pressure-sensor": [
10739
- {
10740
- "path": "hpa",
10741
- "valueType": "number"
10742
- },
10743
- {
10744
- "path": "unit",
10745
- "valueType": "string"
10746
- },
10747
- {
10748
- "path": "precision",
10749
- "valueType": "number"
10750
- }
10751
- ],
10801
+ "pressure-sensor": [{
10802
+ "path": "hpa",
10803
+ "valueType": "number"
10804
+ }],
10752
10805
  "privacy-mask": [{
10753
10806
  "path": "enabled",
10754
10807
  "valueType": "boolean"
@@ -11059,20 +11112,10 @@ Object.freeze({
11059
11112
  "path": "tampered",
11060
11113
  "valueType": "boolean"
11061
11114
  }],
11062
- "temperature-sensor": [
11063
- {
11064
- "path": "celsius",
11065
- "valueType": "number"
11066
- },
11067
- {
11068
- "path": "unit",
11069
- "valueType": "string"
11070
- },
11071
- {
11072
- "path": "precision",
11073
- "valueType": "number"
11074
- }
11075
- ],
11115
+ "temperature-sensor": [{
11116
+ "path": "celsius",
11117
+ "valueType": "number"
11118
+ }],
11076
11119
  "update": [
11077
11120
  {
11078
11121
  "path": "currentVersion",
@@ -11183,10 +11226,6 @@ Object.freeze({
11183
11226
  "path": "temperature",
11184
11227
  "valueType": "number"
11185
11228
  },
11186
- {
11187
- "path": "temperatureUnit",
11188
- "valueType": "string"
11189
- },
11190
11229
  {
11191
11230
  "path": "humidity",
11192
11231
  "valueType": "number"
@@ -11195,18 +11234,10 @@ Object.freeze({
11195
11234
  "path": "pressure",
11196
11235
  "valueType": "number"
11197
11236
  },
11198
- {
11199
- "path": "pressureUnit",
11200
- "valueType": "string"
11201
- },
11202
11237
  {
11203
11238
  "path": "windSpeed",
11204
11239
  "valueType": "number"
11205
11240
  },
11206
- {
11207
- "path": "windSpeedUnit",
11208
- "valueType": "string"
11209
- },
11210
11241
  {
11211
11242
  "path": "windBearing",
11212
11243
  "valueType": "number"
@@ -11256,7 +11287,7 @@ object({
11256
11287
  params: record(string(), union([string(), number()])).optional()
11257
11288
  });
11258
11289
  //#endregion
11259
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
11290
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
11260
11291
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
11261
11292
  DeviceType["Camera"] = "camera";
11262
11293
  DeviceType["Hub"] = "hub";
@@ -11350,8 +11381,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
11350
11381
  DeviceType["PetFeeder"] = "pet-feeder";
11351
11382
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
11352
11383
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
11353
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
11384
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
11385
+ * A plug-built dishwasher carries running-state and power instead. */
11354
11386
  DeviceType["Dishwasher"] = "dishwasher";
11387
+ /** Washing machine. A plug-built one is running while its power stays
11388
+ * above a threshold. No program cap — that surface is the dishwasher's. */
11389
+ DeviceType["Washer"] = "washer";
11390
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
11391
+ DeviceType["Dryer"] = "dryer";
11355
11392
  return DeviceType;
11356
11393
  }({});
11357
11394
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -11609,7 +11646,7 @@ function errMsg(err) {
11609
11646
  return String(err);
11610
11647
  }
11611
11648
  //#endregion
11612
- //#region ../types/dist/composition-kEl9mevu.mjs
11649
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
11613
11650
  /**
11614
11651
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11615
11652
  *
@@ -11924,6 +11961,13 @@ var table = {
11924
11961
  apply: () => {
11925
11962
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11926
11963
  }
11964
+ },
11965
+ schmitt: {
11966
+ minArgs: 3,
11967
+ maxArgs: 5,
11968
+ apply: () => {
11969
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11970
+ }
11927
11971
  }
11928
11972
  };
11929
11973
  Object.freeze(Object.assign(Object.create(null), table));
@@ -12364,15 +12408,232 @@ function validateExpressionSource(src) {
12364
12408
  }
12365
12409
  return null;
12366
12410
  }
12367
- /** A composed device's stableId is this prefix plus the block id. */
12368
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12369
- /** Longest snippet a `code` source may carry (slice 4). */
12370
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
12411
+ /**
12412
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12413
+ *
12414
+ * Standby of the control board is under 5 W; a motor or a heater is far
12415
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
12416
+ * a cycle, so running starts only after 30 s above the line.
12417
+ *
12418
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12419
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
12420
+ * report "finished" in the middle of the program, so the stop dwell is
12421
+ * 10 minutes on every type. The number is stored per type so one machine
12422
+ * can move later without moving the others.
12423
+ */
12424
+ var APPLIANCE_TEMPLATE_ID = "appliance";
12425
+ var APPLIANCE_KINDS = [
12426
+ "washer",
12427
+ "dryer",
12428
+ "dishwasher"
12429
+ ];
12430
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12431
+ /**
12432
+ * Statistics and integrals over a history read (D944).
12433
+ *
12434
+ * Pure: the same points always give the same answer, so a restart recomputes
12435
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
12436
+ * never the mean of the samples. A hole with no held value is not integrated
12437
+ * as zero. A truncated read is not published.
12438
+ */
12439
+ var STATISTIC_STATS = [
12440
+ "min",
12441
+ "max",
12442
+ "mean",
12443
+ "sum",
12444
+ "count",
12445
+ "change"
12446
+ ];
12447
+ /**
12448
+ * The range a statistics or integral helper asks history for (D944).
12449
+ *
12450
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
12451
+ * or 25 hours across a DST change. A tier is eligible only when its retention
12452
+ * is strictly longer than the longest that range can be. Forever qualifies.
12453
+ * Equal to a tier's retention is a refusal, not a short answer.
12454
+ */
12455
+ var MINUTE_MS = 6e4;
12456
+ var HOUR_MS = 60 * MINUTE_MS;
12457
+ var DAY_MS = 24 * HOUR_MS;
12458
+ var ROLLING_SPANS = [
12459
+ "1h",
12460
+ "24h",
12461
+ "2d",
12462
+ "7d",
12463
+ "30d",
12464
+ "custom"
12465
+ ];
12466
+ var CALENDAR_PERIODS = [
12467
+ "today",
12468
+ "yesterday",
12469
+ "this-week",
12470
+ "this-month"
12471
+ ];
12472
+ var RESET_PERIODS = [
12473
+ "never",
12474
+ "daily",
12475
+ "weekly",
12476
+ "monthly"
12477
+ ];
12478
+ 5 * MINUTE_MS;
12479
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
12480
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
12481
+ var rollingWindowObject = object({
12482
+ kind: literal("rolling"),
12483
+ span: _enum(ROLLING_SPANS),
12484
+ customMinutes: number().int().min(5).optional()
12485
+ }).check((payload) => {
12486
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
12487
+ code: "custom",
12488
+ message: "a custom range needs a duration in minutes",
12489
+ path: ["customMinutes"],
12490
+ input: payload.value
12491
+ });
12492
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
12493
+ code: "custom",
12494
+ message: "only a custom range carries a duration",
12495
+ path: ["customMinutes"],
12496
+ input: payload.value
12497
+ });
12498
+ });
12499
+ var RollingWindowSchema = rollingWindowObject;
12500
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
12501
+ kind: literal("calendar"),
12502
+ period: _enum(CALENDAR_PERIODS)
12503
+ })]);
12504
+ /**
12505
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
12506
+ * id. Split out so helper schemas can name it without a cycle through
12507
+ * `composition.ts` (D659).
12508
+ */
12371
12509
  var CompositionSourceRefSchema = object({
12372
12510
  /** The owning addon; `stableId` is unique only within it. */
12373
12511
  addonId: string().min(1),
12374
12512
  stableId: string().min(1)
12375
12513
  });
12514
+ var GROUP_TEMPLATE_IDS = [
12515
+ "temperature",
12516
+ "humidity",
12517
+ "pressure",
12518
+ "illuminance",
12519
+ "power",
12520
+ "energy",
12521
+ "battery",
12522
+ "numeric",
12523
+ "motion",
12524
+ "contact",
12525
+ "leak",
12526
+ "smoke",
12527
+ "occupancy",
12528
+ "binary",
12529
+ "switch",
12530
+ "light",
12531
+ "fan",
12532
+ "cover",
12533
+ "valve",
12534
+ "lock",
12535
+ "media-player",
12536
+ "climate"
12537
+ ];
12538
+ var NUMERIC_AGGREGATES = [
12539
+ "mean",
12540
+ "median",
12541
+ "min",
12542
+ "max",
12543
+ "sum"
12544
+ ];
12545
+ var BINARY_AGGREGATES = ["any", "all"];
12546
+ /** How an actuator group decides it is on. Unknown members never count as off. */
12547
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
12548
+ var AGGREGATE_REDUCERS = [
12549
+ "mean",
12550
+ "median",
12551
+ "min",
12552
+ "max",
12553
+ "sum",
12554
+ "any",
12555
+ "all"
12556
+ ];
12557
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
12558
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
12559
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
12560
+ /** One member. The field is stored so occupancy can name motion or binary. */
12561
+ var HelperMemberSchema = object({
12562
+ source: CompositionSourceRefSchema,
12563
+ cap: string().min(1),
12564
+ fieldPath: string().min(1)
12565
+ });
12566
+ var HelperSpecSchema = discriminatedUnion("kind", [
12567
+ object({
12568
+ kind: literal("group"),
12569
+ templateId: GroupTemplateIdSchema,
12570
+ aggregate: AggregateReducerSchema,
12571
+ members: array(HelperMemberSchema).min(1).max(64),
12572
+ /** Actuator groups only. Readings leave this unset. */
12573
+ active: ActiveCriterionSchema.optional()
12574
+ }),
12575
+ object({
12576
+ kind: literal("appliance"),
12577
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
12578
+ appliance: _enum(APPLIANCE_KINDS),
12579
+ thresholdWatts: number().positive(),
12580
+ startDwellMs: number().int().nonnegative(),
12581
+ stopDwellMs: number().int().nonnegative(),
12582
+ member: HelperMemberSchema
12583
+ }),
12584
+ object({
12585
+ kind: literal("statistic"),
12586
+ stat: _enum(STATISTIC_STATS),
12587
+ window: StatisticWindowSchema,
12588
+ source: HelperMemberSchema
12589
+ }),
12590
+ object({
12591
+ kind: literal("integral"),
12592
+ reset: _enum(RESET_PERIODS),
12593
+ output: _enum(["energy", "meter"]),
12594
+ source: HelperMemberSchema
12595
+ }),
12596
+ object({
12597
+ kind: literal("threshold"),
12598
+ source: HelperMemberSchema,
12599
+ high: number(),
12600
+ low: number(),
12601
+ onForMs: number().int().nonnegative(),
12602
+ offForMs: number().int().nonnegative()
12603
+ }),
12604
+ object({
12605
+ kind: literal("arithmetic"),
12606
+ op: _enum([
12607
+ "sum",
12608
+ "difference",
12609
+ "ratio"
12610
+ ]),
12611
+ inputs: array(HelperMemberSchema).min(2).max(64)
12612
+ }),
12613
+ object({
12614
+ kind: literal("expression"),
12615
+ expr: string().min(1),
12616
+ inputs: array(object({
12617
+ name: string().min(1),
12618
+ member: HelperMemberSchema
12619
+ })).min(1).max(64),
12620
+ output: _enum(["number", "boolean"])
12621
+ }),
12622
+ object({
12623
+ kind: literal("derivative"),
12624
+ window: RollingWindowSchema,
12625
+ per: _enum([
12626
+ "second",
12627
+ "minute",
12628
+ "hour"
12629
+ ]),
12630
+ source: HelperMemberSchema
12631
+ })
12632
+ ]);
12633
+ /** A composed device's stableId is this prefix plus the block id. */
12634
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12635
+ /** Longest snippet a `code` source may carry (slice 4). */
12636
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
12376
12637
  /** One field of one capability of one source device. */
12377
12638
  var CompositionFieldReadSchema = object({
12378
12639
  source: CompositionSourceRefSchema,
@@ -12416,7 +12677,59 @@ var CompositionCodeSourceSchema = object({
12416
12677
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12417
12678
  CompositionFromSourceSchema,
12418
12679
  CompositionExpressionSourceSchema,
12419
- CompositionCodeSourceSchema
12680
+ CompositionCodeSourceSchema,
12681
+ object({
12682
+ kind: literal("aggregate"),
12683
+ reducer: _enum([
12684
+ "mean",
12685
+ "median",
12686
+ "min",
12687
+ "max",
12688
+ "sum",
12689
+ "any",
12690
+ "all"
12691
+ ]),
12692
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12693
+ /**
12694
+ * Set when the reads are an enum state, not a yes/no: the listed states
12695
+ * count as active (a cover is `open`). A state outside the list is inactive;
12696
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
12697
+ */
12698
+ activeWhen: array(string().min(1)).min(1).optional(),
12699
+ /** `value` is the reading; the others are the member counts. */
12700
+ publish: _enum([
12701
+ "value",
12702
+ "included",
12703
+ "excluded",
12704
+ "total"
12705
+ ]),
12706
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12707
+ empty: _enum([
12708
+ "null",
12709
+ "omit",
12710
+ "offline"
12711
+ ])
12712
+ }),
12713
+ object({
12714
+ kind: literal("series"),
12715
+ read: CompositionFieldReadSchema,
12716
+ /** `value` is the statistic or the integral. The others are the gap counts. */
12717
+ publish: _enum([
12718
+ "value",
12719
+ "included",
12720
+ "excluded",
12721
+ "total"
12722
+ ]),
12723
+ /**
12724
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
12725
+ * device unseeded until history answers, so a restart cannot publish 0.
12726
+ */
12727
+ empty: _enum([
12728
+ "null",
12729
+ "omit",
12730
+ "offline"
12731
+ ])
12732
+ })
12420
12733
  ]);
12421
12734
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12422
12735
  kind: literal("forward"),
@@ -12462,7 +12775,13 @@ var CompositionSchema = object({
12462
12775
  kind: literal("existing"),
12463
12776
  device: CompositionSourceRefSchema
12464
12777
  })]),
12465
- features: array(CompositionFeatureSchema).min(1).max(16)
12778
+ features: array(CompositionFeatureSchema).min(1).max(16),
12779
+ /**
12780
+ * Present when the block is a helper template. Absent on a free
12781
+ * composition (the room example, device 4603). The editor reads this,
12782
+ * never the compiled features.
12783
+ */
12784
+ helper: HelperSpecSchema.optional()
12466
12785
  }).superRefine((composition, ctx) => {
12467
12786
  const seen = /* @__PURE__ */ new Set();
12468
12787
  composition.features.forEach((feature, index) => {
@@ -17569,7 +17888,22 @@ var BrokerProviderInfoSchema = object({
17569
17888
  */
17570
17889
  interactiveConnectPath: string().min(1).optional(),
17571
17890
  /** Relative hub path to disconnect the linked account (same popup pattern). */
17572
- interactiveDisconnectPath: string().min(1).optional()
17891
+ interactiveDisconnectPath: string().min(1).optional(),
17892
+ /**
17893
+ * The kind signs in through a server-held interactive session instead of
17894
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17895
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17896
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17897
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17898
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17899
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17900
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17901
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17902
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17903
+ * offers it as the default path; the fields the session fills are
17904
+ * marked `filledBySignIn` in the kind's settings schema.
17905
+ */
17906
+ signIn: object({ basePath: string().min(1) }).optional()
17573
17907
  }))
17574
17908
  });
17575
17909
  var ListInputSchema = object({
@@ -18670,7 +19004,12 @@ var CompositionProblemSchema = object({
18670
19004
  "item-field-reserved",
18671
19005
  "target-missing",
18672
19006
  "target-unreadable",
18673
- "not-replaceable"
19007
+ "not-replaceable",
19008
+ "native-capability-replace",
19009
+ "mixed-quantity",
19010
+ "helper-mismatch",
19011
+ "member-constraint",
19012
+ "retention"
18674
19013
  ]),
18675
19014
  path: string(),
18676
19015
  message: string()
@@ -18691,7 +19030,8 @@ var CompositionUnavailableModeSchema = _enum([
18691
19030
  "null",
18692
19031
  "offline",
18693
19032
  "unknown",
18694
- "release"
19033
+ "release",
19034
+ "omit"
18695
19035
  ]);
18696
19036
  /**
18697
19037
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18728,9 +19068,11 @@ var CompositionFieldPlanSchema = object({
18728
19068
  reads: array(CompositionFieldReadSchema)
18729
19069
  });
18730
19070
  /**
18731
- * What a feature does to its target (D663): `new` builds a capability on a new
18732
- * device; on an existing device it `add`s a capability the device lacks, or
18733
- * `replace`s named fields of one it has natively.
19071
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
19072
+ * new device; on an existing device it `add`s a capability the device lacks.
19073
+ * Replacing a capability the device already serves is refused
19074
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
19075
+ * wire for a claim the hub already holds.
18734
19076
  */
18735
19077
  var CompositionFeatureModeSchema = _enum([
18736
19078
  "new",
@@ -18901,8 +19243,392 @@ Object.freeze({
18901
19243
  latchedAt: {
18902
19244
  kind: "fixed",
18903
19245
  result: ["number", "null"]
19246
+ },
19247
+ schmitt: {
19248
+ kind: "fixed",
19249
+ result: ["boolean", "null"]
18904
19250
  }
18905
19251
  });
19252
+ /**
19253
+ * One catalog row per base reading a group helper can build (D944 §3).
19254
+ * Data, not a form default written into the block.
19255
+ */
19256
+ var NOW$2 = {
19257
+ fieldPath: "lastFetchedAt",
19258
+ expr: "now"
19259
+ };
19260
+ var CHANGED = {
19261
+ fieldPath: "lastChangedAt",
19262
+ expr: "now"
19263
+ };
19264
+ function numeric(input) {
19265
+ return {
19266
+ ...input,
19267
+ family: "numeric",
19268
+ deviceType: DeviceType.Sensor,
19269
+ memberDeviceType: DeviceType.Sensor,
19270
+ memberDeviceTypes: [DeviceType.Sensor],
19271
+ activeWhen: void 0,
19272
+ commandCaps: [],
19273
+ baseUnit: baseUnitOf(input.quantity),
19274
+ aggregates: NUMERIC_AGGREGATES,
19275
+ accept: [{
19276
+ cap: input.outputCap,
19277
+ fieldPath: input.outputField
19278
+ }]
19279
+ };
19280
+ }
19281
+ function binary(input) {
19282
+ return {
19283
+ ...input,
19284
+ family: "binary",
19285
+ deviceType: DeviceType.Sensor,
19286
+ memberDeviceType: DeviceType.Sensor,
19287
+ memberDeviceTypes: [DeviceType.Sensor],
19288
+ activeWhen: void 0,
19289
+ commandCaps: [],
19290
+ quantity: "none",
19291
+ baseUnit: void 0,
19292
+ aggregates: BINARY_AGGREGATES,
19293
+ empty: "offline"
19294
+ };
19295
+ }
19296
+ var GROUP_TEMPLATES = [
19297
+ numeric({
19298
+ id: "temperature",
19299
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
19300
+ role: DeviceRole.TemperatureSensor,
19301
+ outputCap: "temperature-sensor",
19302
+ outputField: "celsius",
19303
+ quantity: "temperature",
19304
+ defaultAggregate: "mean",
19305
+ empty: "null",
19306
+ companions: [NOW$2]
19307
+ }),
19308
+ numeric({
19309
+ id: "humidity",
19310
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
19311
+ role: DeviceRole.HumiditySensor,
19312
+ outputCap: "humidity-sensor",
19313
+ outputField: "percent",
19314
+ quantity: "humidity",
19315
+ defaultAggregate: "mean",
19316
+ empty: "null",
19317
+ companions: [NOW$2]
19318
+ }),
19319
+ numeric({
19320
+ id: "pressure",
19321
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
19322
+ role: DeviceRole.PressureSensor,
19323
+ outputCap: "pressure-sensor",
19324
+ outputField: "hpa",
19325
+ quantity: "pressure",
19326
+ defaultAggregate: "mean",
19327
+ empty: "null",
19328
+ companions: [NOW$2]
19329
+ }),
19330
+ numeric({
19331
+ id: "illuminance",
19332
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
19333
+ role: DeviceRole.AmbientLightSensor,
19334
+ outputCap: "ambient-light-sensor",
19335
+ outputField: "lux",
19336
+ quantity: "illuminance",
19337
+ defaultAggregate: "mean",
19338
+ empty: "null",
19339
+ companions: [NOW$2]
19340
+ }),
19341
+ numeric({
19342
+ id: "power",
19343
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
19344
+ role: DeviceRole.PowerSensor,
19345
+ outputCap: "power-meter",
19346
+ outputField: "watts",
19347
+ quantity: "power",
19348
+ defaultAggregate: "sum",
19349
+ empty: "omit",
19350
+ companions: [NOW$2]
19351
+ }),
19352
+ numeric({
19353
+ id: "energy",
19354
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
19355
+ role: DeviceRole.EnergySensor,
19356
+ outputCap: "power-meter",
19357
+ outputField: "kwhTotal",
19358
+ quantity: "energy",
19359
+ defaultAggregate: "sum",
19360
+ empty: "omit",
19361
+ companions: [NOW$2]
19362
+ }),
19363
+ numeric({
19364
+ id: "battery",
19365
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
19366
+ role: DeviceRole.BatterySensor,
19367
+ outputCap: "battery",
19368
+ outputField: "percentage",
19369
+ quantity: "percentage",
19370
+ defaultAggregate: "mean",
19371
+ empty: "null",
19372
+ companions: [
19373
+ {
19374
+ fieldPath: "charging",
19375
+ expr: "\"unknown\""
19376
+ },
19377
+ {
19378
+ fieldPath: "sleeping",
19379
+ expr: "false"
19380
+ },
19381
+ {
19382
+ fieldPath: "lastUpdated",
19383
+ expr: "now"
19384
+ }
19385
+ ]
19386
+ }),
19387
+ numeric({
19388
+ id: "numeric",
19389
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
19390
+ role: DeviceRole.NumericSensor,
19391
+ outputCap: "numeric-sensor",
19392
+ outputField: "value",
19393
+ quantity: "device",
19394
+ defaultAggregate: "mean",
19395
+ empty: "null",
19396
+ companions: [NOW$2]
19397
+ }),
19398
+ binary({
19399
+ id: "motion",
19400
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
19401
+ role: DeviceRole.MotionSensor,
19402
+ outputCap: "motion",
19403
+ outputField: "detected",
19404
+ defaultAggregate: "any",
19405
+ accept: [{
19406
+ cap: "motion",
19407
+ fieldPath: "detected"
19408
+ }],
19409
+ companions: [{
19410
+ fieldPath: "lastDetectedAt",
19411
+ expr: "null"
19412
+ }, {
19413
+ fieldPath: "autoClearAfterMs",
19414
+ expr: "null"
19415
+ }]
19416
+ }),
19417
+ binary({
19418
+ id: "contact",
19419
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
19420
+ role: DeviceRole.ContactSensor,
19421
+ outputCap: "contact",
19422
+ outputField: "entryOpen",
19423
+ defaultAggregate: "any",
19424
+ accept: [{
19425
+ cap: "contact",
19426
+ fieldPath: "entryOpen"
19427
+ }],
19428
+ companions: [CHANGED]
19429
+ }),
19430
+ binary({
19431
+ id: "leak",
19432
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
19433
+ role: DeviceRole.LeakSensor,
19434
+ outputCap: "flood",
19435
+ outputField: "flooded",
19436
+ defaultAggregate: "any",
19437
+ accept: [{
19438
+ cap: "flood",
19439
+ fieldPath: "flooded"
19440
+ }],
19441
+ companions: [CHANGED]
19442
+ }),
19443
+ binary({
19444
+ id: "smoke",
19445
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
19446
+ role: DeviceRole.SmokeSensor,
19447
+ outputCap: "smoke",
19448
+ outputField: "detected",
19449
+ defaultAggregate: "any",
19450
+ accept: [{
19451
+ cap: "smoke",
19452
+ fieldPath: "detected"
19453
+ }],
19454
+ companions: [CHANGED]
19455
+ }),
19456
+ binary({
19457
+ id: "occupancy",
19458
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
19459
+ role: DeviceRole.BinarySensor,
19460
+ outputCap: "binary",
19461
+ outputField: "on",
19462
+ defaultAggregate: "any",
19463
+ accept: [{
19464
+ cap: "motion",
19465
+ fieldPath: "detected"
19466
+ }, {
19467
+ cap: "binary",
19468
+ fieldPath: "on"
19469
+ }],
19470
+ companions: [CHANGED]
19471
+ }),
19472
+ binary({
19473
+ id: "binary",
19474
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
19475
+ role: DeviceRole.BinarySensor,
19476
+ outputCap: "binary",
19477
+ outputField: "on",
19478
+ defaultAggregate: "any",
19479
+ accept: [{
19480
+ cap: "binary",
19481
+ fieldPath: "on"
19482
+ }],
19483
+ companions: [CHANGED]
19484
+ }),
19485
+ actuator({
19486
+ id: "switch",
19487
+ summary: "On when at least one switch is on, or when all of them are. Offline and unknown members are left out of that decision, never read as off, and still receive the command.",
19488
+ deviceType: DeviceType.Switch,
19489
+ outputCap: "switch",
19490
+ outputField: "on",
19491
+ member: {
19492
+ cap: "switch",
19493
+ fieldPath: "on"
19494
+ },
19495
+ commandCaps: ["switch"]
19496
+ }),
19497
+ actuator({
19498
+ id: "light",
19499
+ summary: "On when at least one light is on, or when all of them are. Brightness and colour are offered only when every member has them. A command is not sent to a member that lacks it.",
19500
+ deviceType: DeviceType.Light,
19501
+ outputCap: "switch",
19502
+ outputField: "on",
19503
+ member: {
19504
+ cap: "switch",
19505
+ fieldPath: "on"
19506
+ },
19507
+ commandCaps: [
19508
+ "switch",
19509
+ "brightness",
19510
+ "color"
19511
+ ]
19512
+ }),
19513
+ actuator({
19514
+ id: "fan",
19515
+ summary: "On when at least one fan is on, or when all of them are. Speed, preset, direction and oscillation are offered only when every fan has them. A command is not sent to a fan that lacks it.",
19516
+ deviceType: DeviceType.Fan,
19517
+ outputCap: "switch",
19518
+ outputField: "on",
19519
+ member: {
19520
+ cap: "switch",
19521
+ fieldPath: "on"
19522
+ },
19523
+ commandCaps: ["switch", "fan-control"]
19524
+ }),
19525
+ actuator({
19526
+ id: "cover",
19527
+ summary: "Open when at least one cover is open or part-open, or only when all of them are. Position and tilt are offered only when every cover has them. A cover that reports nothing is left out, never read as closed.",
19528
+ deviceType: DeviceType.Cover,
19529
+ outputCap: "group-active",
19530
+ outputField: "active",
19531
+ member: {
19532
+ cap: "cover",
19533
+ fieldPath: "state"
19534
+ },
19535
+ activeWhen: [
19536
+ "open",
19537
+ "opening",
19538
+ "stopped"
19539
+ ],
19540
+ commandCaps: ["cover"]
19541
+ }),
19542
+ actuator({
19543
+ id: "valve",
19544
+ summary: "Open when at least one valve is open or part-open, or only when all of them are. Position is offered only when every valve has it. A valve that reports nothing is left out, never read as closed.",
19545
+ deviceType: DeviceType.Valve,
19546
+ outputCap: "group-active",
19547
+ outputField: "active",
19548
+ member: {
19549
+ cap: "valve",
19550
+ fieldPath: "state"
19551
+ },
19552
+ activeWhen: [
19553
+ "open",
19554
+ "opening",
19555
+ "stopped"
19556
+ ],
19557
+ commandCaps: ["valve"]
19558
+ }),
19559
+ actuator({
19560
+ id: "lock",
19561
+ summary: "Locked when at least one lock is locked, or only when all of them are. A jammed or moving lock is not locked. Locking is one tap; unlocking the group needs a confirmation every time, and nothing is sent without it.",
19562
+ deviceType: DeviceType.Lock,
19563
+ outputCap: "group-active",
19564
+ outputField: "active",
19565
+ member: {
19566
+ cap: "lock-control",
19567
+ fieldPath: "state"
19568
+ },
19569
+ activeWhen: ["locked"],
19570
+ commandCaps: ["lock-control"]
19571
+ }),
19572
+ actuator({
19573
+ id: "media-player",
19574
+ summary: "Playing when at least one player is playing, or only when all of them are. Volume is offered only when every player has it. A player that reports nothing is left out, never read as stopped.",
19575
+ deviceType: DeviceType.MediaPlayer,
19576
+ outputCap: "group-active",
19577
+ outputField: "active",
19578
+ member: {
19579
+ cap: "media-player",
19580
+ fieldPath: "state"
19581
+ },
19582
+ activeWhen: ["playing", "buffering"],
19583
+ commandCaps: ["media-player"]
19584
+ }),
19585
+ actuator({
19586
+ id: "climate",
19587
+ summary: "Running when at least one thermostat or air conditioner is not off, or only when all of them are. Target temperature and fan are offered only when every device has them. A device that reports nothing is left out, never read as off.",
19588
+ deviceType: DeviceType.Thermostat,
19589
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
19590
+ outputCap: "group-active",
19591
+ outputField: "active",
19592
+ member: {
19593
+ cap: "climate-control",
19594
+ fieldPath: "mode"
19595
+ },
19596
+ activeWhen: [
19597
+ "heat",
19598
+ "cool",
19599
+ "auto",
19600
+ "heat_cool",
19601
+ "fan_only",
19602
+ "dry"
19603
+ ],
19604
+ commandCaps: ["climate-control"]
19605
+ })
19606
+ ];
19607
+ function actuator(input) {
19608
+ return {
19609
+ id: input.id,
19610
+ summary: input.summary,
19611
+ deviceType: input.deviceType,
19612
+ outputCap: input.outputCap,
19613
+ outputField: input.outputField,
19614
+ commandCaps: input.commandCaps,
19615
+ family: "actuator",
19616
+ role: void 0,
19617
+ memberDeviceType: input.deviceType,
19618
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
19619
+ activeWhen: input.activeWhen,
19620
+ quantity: "none",
19621
+ baseUnit: void 0,
19622
+ aggregates: BINARY_AGGREGATES,
19623
+ defaultAggregate: "any",
19624
+ empty: "offline",
19625
+ accept: [input.member],
19626
+ companions: [CHANGED]
19627
+ };
19628
+ }
19629
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19630
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19631
+ DeviceType.Camera;
18906
19632
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18907
19633
  /**
18908
19634
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20616,12 +21342,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20616
21342
  * providers detect renames by matching `uniqueId` and patch `id`
20617
21343
  * in-place so dispatch keeps working.
20618
21344
  *
20619
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20620
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20621
- * state push and are the single source of truth. The `device_class` tag is
20622
- * an HA-specific input that is consumed at the discovery boundary
20623
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20624
- * stored after that point.
21345
+ * **Display unit and precision are NOT stored here.** They are a read-time
21346
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
21347
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
21348
+ * neither is repeated on each sample. The `device_class` tag is an
21349
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
21350
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20625
21351
  *
20626
21352
  * The `raw` escape hatch carries provider-specific attributes that don't
20627
21353
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20635,6 +21361,14 @@ var SourceInfoSchema = object({
20635
21361
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20636
21362
  * Used to detect rename when `id` changes. */
20637
21363
  uniqueId: string().optional(),
21364
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
21365
+ quantity: ReadingQuantitySchema.optional(),
21366
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
21367
+ dateTimeShape: _enum([
21368
+ "date",
21369
+ "time",
21370
+ "datetime"
21371
+ ]).optional(),
20638
21372
  /** Provider-specific extras that don't fit the structured slots.
20639
21373
  * Whitelist what goes here per provider — do NOT dump entire HA
20640
21374
  * attribute blobs (would explode the meta JSON column). */
@@ -41756,7 +42490,8 @@ method(object({
41756
42490
  "enum"
41757
42491
  ]),
41758
42492
  enumValues: array(string()).optional(),
41759
- item: boolean().optional()
42493
+ item: boolean().optional(),
42494
+ quantity: ReadingQuantitySchema.optional()
41760
42495
  })).readonly(),
41761
42496
  itemArray: object({
41762
42497
  path: string(),
@@ -44117,7 +44852,7 @@ var NetworkAccessStatusSchema = object({
44117
44852
  });
44118
44853
  /**
44119
44854
  * Optional, richer endpoint shape returned by providers that expose
44120
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44855
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
44121
44856
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
44122
44857
  * the originating provider config (mode + sourcePort) so the
44123
44858
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -45600,8 +46335,8 @@ var MaskPolygonShapeSchema = object({
45600
46335
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45601
46336
  var MaskGridShapeSchema = object({
45602
46337
  kind: literal("grid"),
45603
- gridWidth: number(),
45604
- gridHeight: number(),
46338
+ gridWidth: number().meta(readingMeta("none")),
46339
+ gridHeight: number().meta(readingMeta("none")),
45605
46340
  cells: array(boolean())
45606
46341
  });
45607
46342
  discriminatedUnion("kind", [
@@ -45713,6 +46448,8 @@ var NcSystemEventKindSchema = _enum([
45713
46448
  "detection-blind",
45714
46449
  "motion-noisy",
45715
46450
  "motion-calm",
46451
+ "package-llm-silent",
46452
+ "package-llm-answered",
45716
46453
  "recording-stalled",
45717
46454
  "recording-healthy",
45718
46455
  "recording-degraded",
@@ -46802,7 +47539,20 @@ var NcRuleInputSchema = object({
46802
47539
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46803
47540
  * path does not run Zod defaults.
46804
47541
  */
46805
- groupingSec: number().int().min(5).max(60).optional()
47542
+ groupingSec: number().int().min(5).max(60).optional(),
47543
+ /**
47544
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
47545
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
47546
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
47547
+ * same mark the Library writes, and records whose notification it was and
47548
+ * when. A channel with no buttons gets the same two choices as links.
47549
+ *
47550
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
47551
+ * cap path): absent and `false` both mean off, which is what every rule
47552
+ * authored before this field meant. An editor turns it off by writing
47553
+ * `false`, so it needs no `clear` key.
47554
+ */
47555
+ askVerdict: boolean().optional()
46806
47556
  });
46807
47557
  /**
46808
47558
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -56804,6 +57554,28 @@ var PackageVersionInfoSchema = object({
56804
57554
  deprecated: string().optional(),
56805
57555
  distTags: array(string()).readonly()
56806
57556
  });
57557
+ /**
57558
+ * One published document. `ok` is markdown from the registry. `missing` means
57559
+ * the registry answered and the file was not there. `error` is a registry or
57560
+ * tarball failure — never an empty string standing in for either.
57561
+ */
57562
+ var PackageDocBodySchema = discriminatedUnion("status", [
57563
+ object({
57564
+ status: literal("ok"),
57565
+ markdown: string()
57566
+ }),
57567
+ object({ status: literal("missing") }),
57568
+ object({
57569
+ status: literal("error"),
57570
+ message: string()
57571
+ })
57572
+ ]);
57573
+ var PackageDocsSchema = object({
57574
+ name: string(),
57575
+ version: string(),
57576
+ readme: PackageDocBodySchema,
57577
+ changelog: PackageDocBodySchema
57578
+ });
56807
57579
  var SearchResultSchema = object({
56808
57580
  name: string(),
56809
57581
  version: string(),
@@ -57022,7 +57794,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57022
57794
  }), object({ success: literal(true) }), {
57023
57795
  kind: "mutation",
57024
57796
  auth: "admin"
57025
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57797
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57798
+ name: string().min(1),
57799
+ version: string().min(1).optional()
57800
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
57026
57801
  kind: "mutation",
57027
57802
  auth: "admin"
57028
57803
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -57081,27 +57856,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57081
57856
  */
57082
57857
  var AirQualitySensorStatusSchema = object({
57083
57858
  /** Carbon dioxide concentration in ppm. */
57084
- co2Ppm: number().min(0).optional(),
57859
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
57085
57860
  /** Total volatile organic compounds in ppb. */
57086
- vocPpb: number().min(0).optional(),
57861
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
57087
57862
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
57088
- pm25: number().min(0).optional(),
57863
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
57089
57864
  /** Particulate matter ≤ 10 μm in µg/m³. */
57090
- pm10: number().min(0).optional(),
57865
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
57091
57866
  /** Composite AQI value (typically 0..500). */
57092
- aqi: number().optional(),
57867
+ aqi: number().optional().meta(readingMeta("index")),
57093
57868
  /** Ms epoch when the slice was last updated. */
57094
- lastFetchedAt: number(),
57095
- /** Live display unit of the single metric this slice carries (e.g. HA
57096
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
57097
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
57098
- * per slice is unambiguous. */
57099
- unit: string().optional(),
57100
- /** Suggested decimal places for numeric display.
57101
- * Populated live from the upstream source when provided (e.g. HA
57102
- * `attributes.suggested_display_precision`). Falls back to
57103
- * auto-formatting when absent. */
57104
- precision: number().int().min(0).max(10).optional()
57869
+ lastFetchedAt: number()
57105
57870
  });
57106
57871
  var airQualitySensorCapability = {
57107
57872
  name: "air-quality-sensor",
@@ -57138,12 +57903,12 @@ var airQualitySensorCapability = {
57138
57903
  },
57139
57904
  pm25: {
57140
57905
  kind: "gauge",
57141
- unit: "ug/m3",
57906
+ unit: "µg/m³",
57142
57907
  deadband: 1
57143
57908
  },
57144
57909
  pm10: {
57145
57910
  kind: "gauge",
57146
- unit: "ug/m3",
57911
+ unit: "µg/m³",
57147
57912
  deadband: 1
57148
57913
  },
57149
57914
  aqi: {
@@ -57161,22 +57926,13 @@ var airQualitySensorCapability = {
57161
57926
  * entries with `device_class: illuminance`.
57162
57927
  */
57163
57928
  var AmbientLightSensorStatusSchema = object({
57164
- /** Current illuminance in lux (lx). */
57165
- lux: number().min(0).meta({
57166
- unit: "lx",
57929
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57930
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57931
+ precision: 0,
57167
57932
  label: "Illuminance"
57168
- }),
57933
+ })),
57169
57934
  /** Ms epoch when the slice was last updated. */
57170
- lastFetchedAt: number(),
57171
- /** Live display unit from the upstream source (e.g. HA
57172
- * `attributes.unit_of_measurement`). The UI prefers this over the
57173
- * role's canonical unit. Absent → fall back to the canonical unit. */
57174
- unit: string().optional(),
57175
- /** Suggested decimal places for numeric display.
57176
- * Populated live from the upstream source when provided (e.g. HA
57177
- * `attributes.suggested_display_precision`). Falls back to
57178
- * auto-formatting when absent. */
57179
- precision: number().int().min(0).max(10).optional()
57935
+ lastFetchedAt: number()
57180
57936
  });
57181
57937
  var ambientLightSensorCapability = {
57182
57938
  name: "ambient-light-sensor",
@@ -57208,12 +57964,7 @@ var ambientLightSensorCapability = {
57208
57964
  } } },
57209
57965
  /** Clock fields: written, but excluded from the compare that decides
57210
57966
  * whether persisting is worth a SQLite commit. */
57211
- volatileStateFields: ["lastFetchedAt"],
57212
- customize: {
57213
- version: 1,
57214
- advanced: "derive",
57215
- targets: []
57216
- }
57967
+ volatileStateFields: ["lastFetchedAt"]
57217
57968
  };
57218
57969
  /**
57219
57970
  * Per-class audio metrics aggregated over a sliding window.
@@ -57440,10 +58191,7 @@ var BatteryStatusSchema = object({
57440
58191
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57441
58192
  * declines to band a non-finite reading, which is the correct shape.
57442
58193
  */
57443
- percentage: number().min(0).max(100).nullable().meta({
57444
- unit: "%",
57445
- label: "Charge"
57446
- }),
58194
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57447
58195
  /**
57448
58196
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57449
58197
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57470,7 +58218,14 @@ var BatteryStatusSchema = object({
57470
58218
  */
57471
58219
  sleeping: boolean().meta({ label: "Sleeping" }),
57472
58220
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57473
- lastUpdated: number(),
58221
+ lastUpdated: number().meta(readingMeta("none")),
58222
+ /**
58223
+ * Battery voltage in volts, for a cell the firmware reports as voltage
58224
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
58225
+ * that only report charge. A second reading on this cap, not a second
58226
+ * `numeric-sensor` on the same device.
58227
+ */
58228
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57474
58229
  /**
57475
58230
  * Ms epoch of the last time the device PROVED it was reachable — a
57476
58231
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57517,12 +58272,12 @@ var BATTERY_DEVICE_TYPES = [
57517
58272
  DeviceType.Switch,
57518
58273
  DeviceType.PetFeeder,
57519
58274
  DeviceType.Container,
57520
- DeviceType.Thermostat
58275
+ DeviceType.Thermostat,
58276
+ DeviceType.EventEmitter
57521
58277
  ];
57522
58278
  /** Generic Customize target for the percentage reading (D936). */
57523
58279
  var batteryCustomize = {
57524
58280
  version: 1,
57525
- advanced: "derive",
57526
58281
  targets: [{
57527
58282
  id: "percentage",
57528
58283
  cardinality: "scalar",
@@ -57531,7 +58286,7 @@ var batteryCustomize = {
57531
58286
  stateField: "percentage"
57532
58287
  },
57533
58288
  deviceTypes: BATTERY_DEVICE_TYPES,
57534
- apply: "add-or-replace",
58289
+ apply: "add",
57535
58290
  path: "percentage",
57536
58291
  sources: [{
57537
58292
  kind: "field",
@@ -57652,10 +58407,16 @@ onStatusChanged: { data: object({
57652
58407
  /**
57653
58408
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57654
58409
  */
57655
- history: { fields: { percentage: {
57656
- kind: "gauge",
57657
- unit: "%"
57658
- } } },
58410
+ history: { fields: {
58411
+ percentage: {
58412
+ kind: "gauge",
58413
+ unit: "%"
58414
+ },
58415
+ volts: {
58416
+ kind: "gauge",
58417
+ unit: "V"
58418
+ }
58419
+ } },
57659
58420
  customize: batteryCustomize,
57660
58421
  /** Clock fields: written, but excluded from the compare that decides
57661
58422
  * whether persisting is worth a SQLite commit. */
@@ -57702,12 +58463,7 @@ var binaryCapability = {
57702
58463
  history: { fields: { on: { kind: "boolean" } } },
57703
58464
  /** Clock fields: written, but excluded from the compare that decides
57704
58465
  * whether persisting is worth a SQLite commit. */
57705
- volatileStateFields: ["lastChangedAt"],
57706
- customize: {
57707
- version: 1,
57708
- advanced: "derive",
57709
- targets: []
57710
- }
58466
+ volatileStateFields: ["lastChangedAt"]
57711
58467
  };
57712
58468
  /**
57713
58469
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57723,7 +58479,7 @@ var binaryCapability = {
57723
58479
  */
57724
58480
  var BrightnessStatusSchema = object({
57725
58481
  /** Current level as 0..100 inclusive. Firmware-reported. */
57726
- percentage: number().min(0).max(100),
58482
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57727
58483
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57728
58484
  lastChangedAt: number()
57729
58485
  });
@@ -58177,21 +58933,21 @@ var ClimateControlStatusSchema = object({
58177
58933
  /** Single setpoint in Celsius. Used by single-target modes
58178
58934
  * (heat / cool / dry). Null when the device is in a range mode
58179
58935
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58180
- target: number().nullable(),
58936
+ target: number().nullable().meta(readingMeta("temperature")),
58181
58937
  /** Upper bound of the dual setpoint in Celsius. Populated only
58182
58938
  * in `heat_cool` mode. */
58183
- targetHigh: number().nullable(),
58939
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58184
58940
  /** Lower bound of the dual setpoint in Celsius. Populated only
58185
58941
  * in `heat_cool` mode. */
58186
- targetLow: number().nullable(),
58942
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58187
58943
  /** Target relative humidity (0..100). Null when the device has no
58188
58944
  * humidity control. */
58189
- targetHumidity: number().min(0).max(100).nullable(),
58945
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58190
58946
  /** Current measured temperature in Celsius. Read-only — pushed by
58191
58947
  * the device's internal sensor. */
58192
- currentTemp: number().nullable(),
58948
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58193
58949
  /** Current measured relative humidity (0..100). Read-only. */
58194
- currentHumidity: number().min(0).max(100).nullable(),
58950
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58195
58951
  /** Vertical louver swing on/off. Null when the device has no vertical
58196
58952
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58197
58953
  swingVertical: boolean().nullable(),
@@ -58199,11 +58955,11 @@ var ClimateControlStatusSchema = object({
58199
58955
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58200
58956
  swingHorizontal: boolean().nullable(),
58201
58957
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58202
- minTemp: number().nullable().optional(),
58958
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58203
58959
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58204
- maxTemp: number().nullable().optional(),
58960
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58205
58961
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58206
- tempStep: number().positive().nullable().optional(),
58962
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58207
58963
  /** Ms epoch when the slice was last updated (push or command). */
58208
58964
  lastFetchedAt: number()
58209
58965
  });
@@ -58345,17 +59101,17 @@ var climateControlCapability = {
58345
59101
  * modes to expose in the picker without a slice fetch.
58346
59102
  */
58347
59103
  var RgbTripletSchema = object({
58348
- r: number().int().min(0).max(255),
58349
- g: number().int().min(0).max(255),
58350
- b: number().int().min(0).max(255)
59104
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
59105
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
59106
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58351
59107
  });
58352
59108
  var HsvTripletSchema = object({
58353
59109
  /** Hue in degrees, 0..360. */
58354
- h: number().min(0).max(360),
59110
+ h: number().min(0).max(360).meta(readingMeta("none")),
58355
59111
  /** Saturation as 0..100 inclusive. */
58356
- s: number().min(0).max(100),
59112
+ s: number().min(0).max(100).meta(readingMeta("none")),
58357
59113
  /** Value/brightness as 0..100 inclusive. */
58358
- v: number().min(0).max(100)
59114
+ v: number().min(0).max(100).meta(readingMeta("none"))
58359
59115
  });
58360
59116
  var ColorInputSchema = discriminatedUnion("mode", [
58361
59117
  object({
@@ -58384,7 +59140,7 @@ var ColorStatusSchema = object({
58384
59140
  /** Populated when `mode === 'hsv'`. */
58385
59141
  hsv: HsvTripletSchema.optional(),
58386
59142
  /** Populated when `mode === 'mired'`. */
58387
- mireds: number().int().optional(),
59143
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58388
59144
  /** Ms epoch of the last operator-driven change. */
58389
59145
  lastChangedAt: number()
58390
59146
  });
@@ -58587,18 +59343,12 @@ _enum([
58587
59343
  "empty",
58588
59344
  "full"
58589
59345
  ]);
58590
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58591
- * each. */
58592
- var ConsumableRemainingUnitSchema = _enum([
58593
- "days",
58594
- "hours",
58595
- "cycles"
58596
- ]);
58597
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58598
- * days). `value` null = unknown (D393). */
59346
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
59347
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
59348
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58599
59349
  var ConsumableRemainingSchema = object({
58600
- value: number().min(0).nullable(),
58601
- unit: ConsumableRemainingUnitSchema
59350
+ value: number().min(0).nullable().meta(readingMeta("device")),
59351
+ kind: ConsumableRemainingKindSchema
58602
59352
  });
58603
59353
  /** A single consumable item. Either a continuous `level` (remaining
58604
59354
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58611,7 +59361,7 @@ var ConsumableItemSchema = object({
58611
59361
  /** Display name. */
58612
59362
  label: string().min(1),
58613
59363
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58614
- level: number().min(0).max(100).nullable(),
59364
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58615
59365
  /** The provider's discrete verdict when known (binary mode). */
58616
59366
  status: ConsumableStatusSchema.nullable(),
58617
59367
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58693,8 +59443,8 @@ var consumablesCustomize = {
58693
59443
  transforms: [{ kind: "identity" }]
58694
59444
  }],
58695
59445
  literals: [{
58696
- path: "remaining.unit",
58697
- values: [...ConsumableRemainingUnitSchema.options]
59446
+ path: "remaining.kind",
59447
+ values: [...ConsumableRemainingKindSchema.options]
58698
59448
  }]
58699
59449
  },
58700
59450
  {
@@ -58823,7 +59573,6 @@ reset: method(object({
58823
59573
  },
58824
59574
  "remaining.value": {
58825
59575
  kind: "gauge",
58826
- unitFrom: "remaining.unit",
58827
59576
  deadband: 1,
58828
59577
  displayName: "Remaining"
58829
59578
  }
@@ -58893,10 +59642,9 @@ var contactCapability = {
58893
59642
  * (empty array for the other kinds). It lives in the slice because
58894
59643
  * HA `select.*` entities may update their options at runtime.
58895
59644
  *
58896
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58897
- * `kind === 'numeric'` and are absent (undefined) for all other
58898
- * kinds. They are populated live from HA attributes on each state
58899
- * push — consistent with `options` and the sensor-unit approach.
59645
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59646
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59647
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58900
59648
  */
58901
59649
  var ControlStatusSchema = object({
58902
59650
  kind: _enum([
@@ -58908,39 +59656,17 @@ var ControlStatusSchema = object({
58908
59656
  /** Current value. Numeric kind → number; select/text/datetime → string.
58909
59657
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58910
59658
  * or time-only — HA accepts all three forms). */
58911
- value: union([number(), string()]),
59659
+ value: union([number().meta(readingMeta("device")), string()]),
58912
59660
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58913
59661
  options: array(string()),
58914
59662
  /** Ms epoch when the slice was last updated. */
58915
59663
  lastChangedAt: number(),
58916
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58917
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58918
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58919
- unit: string().optional(),
58920
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58921
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58922
- min: number().optional(),
58923
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58924
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58925
- max: number().optional(),
58926
- /** Step increment. Populated live from HA `attributes.step`.
58927
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58928
- step: number().optional(),
58929
- /** Suggested decimal places for numeric display. Populated live from HA
58930
- * `attributes.suggested_display_precision`, or derived from `step` when the
58931
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58932
- * other kinds. Falls back to auto-formatting when absent. */
58933
- precision: number().int().min(0).max(10).optional(),
58934
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58935
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58936
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58937
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58938
- * `has_date`/`has_time` attributes on each push. */
58939
- format: _enum([
58940
- "date",
58941
- "time",
58942
- "datetime"
58943
- ]).optional()
59664
+ /** Minimum allowed value, in the device quantity's base unit. */
59665
+ min: number().optional().meta(readingMeta("device")),
59666
+ /** Maximum allowed value, in the device quantity's base unit. */
59667
+ max: number().optional().meta(readingMeta("device")),
59668
+ /** Step increment, in the device quantity's base unit. */
59669
+ step: number().optional().meta(readingMeta("device"))
58944
59670
  });
58945
59671
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58946
59672
  object({
@@ -59006,9 +59732,9 @@ var CoverStatusSchema = object({
59006
59732
  ]),
59007
59733
  /** 0 = fully closed, 100 = fully open. Null when the device has
59008
59734
  * no intermediate position surface. */
59009
- position: number().min(0).max(100).nullable(),
59735
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59010
59736
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
59011
- tiltPosition: number().min(0).max(100).nullable(),
59737
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59012
59738
  /** Ms epoch when the slice was last updated. */
59013
59739
  lastChangedAt: number()
59014
59740
  });
@@ -59111,9 +59837,9 @@ var NormalizedRangeSchema$1 = object({
59111
59837
  var DayNightStatusSchema = object({
59112
59838
  mode: DayNightModeSchema,
59113
59839
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
59114
- sensitivity: number().optional(),
59840
+ sensitivity: number().optional().meta(readingMeta("none")),
59115
59841
  /** Delay before the IR-cut filter flips, in seconds. */
59116
- switchDelaySec: number().optional(),
59842
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
59117
59843
  lastFetchedAt: number()
59118
59844
  });
59119
59845
  /**
@@ -59331,11 +60057,11 @@ var DishwasherStatusSchema = object({
59331
60057
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59332
60058
  availablePrograms: array(string()),
59333
60059
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59334
- remainingSeconds: number().int().nonnegative().nullable(),
60060
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59335
60061
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59336
- delayedStartSeconds: number().int().nonnegative().nullable(),
60062
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59337
60063
  /** Cycle completion 0..100, or null when not applicable. */
59338
- progressPercent: number().min(0).max(100).nullable(),
60064
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59339
60065
  /** True when the door is reported open; null when the door state is not known. */
59340
60066
  doorOpen: boolean().nullable(),
59341
60067
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59359,7 +60085,7 @@ var DishwasherStatusSchema = object({
59359
60085
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59360
60086
  machineCareDue: boolean().nullable(),
59361
60087
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59362
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
60088
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59363
60089
  /** Option states: null when the appliance does not expose the option or has not said. */
59364
60090
  halfLoad: boolean().nullable(),
59365
60091
  hygienePlus: boolean().nullable(),
@@ -59374,7 +60100,7 @@ var DishwasherStatusSchema = object({
59374
60100
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59375
60101
  activeEvents: array(string()),
59376
60102
  /** Numeric error code when the appliance reports one; null otherwise. */
59377
- errorCode: number().nullable(),
60103
+ errorCode: number().nullable().meta(readingMeta("none")),
59378
60104
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59379
60105
  errorLabel: string().nullable(),
59380
60106
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59605,7 +60331,7 @@ var DoorbellStatusSchema = object({
59605
60331
  /** Ms epoch of the last press. null = never observed since this provider started. */
59606
60332
  lastPressedAt: number().nullable(),
59607
60333
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59608
- pressCountSinceStart: number()
60334
+ pressCountSinceStart: number().meta(readingMeta("count"))
59609
60335
  });
59610
60336
  var DoorbellPressEventSchema = object({
59611
60337
  deviceId: number(),
@@ -59670,17 +60396,7 @@ onPressed: {
59670
60396
  history: "none",
59671
60397
  customize: doorbellCustomize
59672
60398
  };
59673
- /**
59674
- * Enum-state sensor — a string value picked from a finite option set.
59675
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59676
- * states, weather conditions, contact-source channels, mode strings).
59677
- * The option set is stable across observations; it is persisted in the
59678
- * device config blob (via the control slice at adoption) and is NOT
59679
- * stored in `sourceInfo`.
59680
- */
59681
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59682
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59683
- var EnumSensorDateTimeFormatSchema = _enum([
60399
+ _enum([
59684
60400
  "date",
59685
60401
  "time",
59686
60402
  "datetime"
@@ -59688,12 +60404,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59688
60404
  var EnumSensorStatusSchema = object({
59689
60405
  value: string(),
59690
60406
  /**
59691
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59692
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59693
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59694
- * a time-only sensor → `'time'`.
60407
+ * Set when `value` is a timestamp rather than an enum state, so history
60408
+ * does not learn ISO strings as states. The display shape (date / time /
60409
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59695
60410
  */
59696
- format: EnumSensorDateTimeFormatSchema.optional(),
60411
+ timestamp: boolean().optional(),
59697
60412
  /** Ms epoch when the slice was last updated. */
59698
60413
  lastFetchedAt: number()
59699
60414
  });
@@ -59720,11 +60435,11 @@ var enumSensorCapability = {
59720
60435
  * History (D850): the value as an `enum` series (an appliance's program or
59721
60436
  * operation state, an HVAC action). The option set is per device, so the
59722
60437
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59723
- * `format` carries a timestamp, not a state, and is not recorded.
60438
+ * `timestamp` carries a clock, not a state, and is not recorded.
59724
60439
  */
59725
60440
  history: { fields: { value: {
59726
60441
  kind: "enum",
59727
- skipWhenSet: "format"
60442
+ skipWhenSet: "timestamp"
59728
60443
  } } },
59729
60444
  /** Clock fields: written, but excluded from the compare that decides
59730
60445
  * whether persisting is worth a SQLite commit. */
@@ -59744,13 +60459,13 @@ var EventFireSchema = object({
59744
60459
  deviceId: number(),
59745
60460
  eventType: string(),
59746
60461
  data: record(string(), unknown()).nullable(),
59747
- timestamp: number(),
59748
- seq: number()
60462
+ timestamp: number().meta(readingMeta("none")),
60463
+ seq: number().meta(readingMeta("count"))
59749
60464
  });
59750
60465
  var EventEmitterStatusSchema = object({
59751
60466
  eventTypes: array(string()),
59752
60467
  lastEvent: EventFireSchema.nullable(),
59753
- eventCountSinceStart: number()
60468
+ eventCountSinceStart: number().meta(readingMeta("count"))
59754
60469
  });
59755
60470
  var eventEmitterCapability = {
59756
60471
  name: "event-emitter",
@@ -60163,10 +60878,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60163
60878
  var FanControlStatusSchema = object({
60164
60879
  /** Active speed as 0..100 inclusive. Null when the device has no
60165
60880
  * speed surface (single-speed fan). */
60166
- percentage: number().min(0).max(100).nullable(),
60881
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60167
60882
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60168
60883
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60169
- percentageStep: number().positive().optional(),
60884
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60170
60885
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60171
60886
  * — empty string when no preset is active or supported. */
60172
60887
  preset: string(),
@@ -60287,7 +61002,7 @@ var FeatureProbeStatusSchema = object({
60287
61002
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60288
61003
  model: string().nullable(),
60289
61004
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60290
- channelCount: number().nullable(),
61005
+ channelCount: number().nullable().meta(readingMeta("count")),
60291
61006
  /**
60292
61007
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60293
61008
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60410,9 +61125,151 @@ var gasCapability = {
60410
61125
  * whether persisting is worth a SQLite commit. */
60411
61126
  volatileStateFields: ["lastChangedAt"]
60412
61127
  };
61128
+ var GroupCommandsSchema = object({
61129
+ /** One row per member that has been commanded. A later command replaces that member's row. */
61130
+ results: array(object({
61131
+ deviceId: number().int().nonnegative(),
61132
+ cap: string().min(1),
61133
+ method: string().min(1),
61134
+ ok: boolean(),
61135
+ error: string().optional(),
61136
+ /** Ms epoch of the attempt. */
61137
+ at: number()
61138
+ })) });
61139
+ var groupCommandsCapability = {
61140
+ name: "group-commands",
61141
+ scope: "device",
61142
+ deviceNative: true,
61143
+ mode: "singleton",
61144
+ deviceTypes: [
61145
+ DeviceType.Switch,
61146
+ DeviceType.Light,
61147
+ DeviceType.Fan,
61148
+ DeviceType.Cover,
61149
+ DeviceType.Valve,
61150
+ DeviceType.Lock,
61151
+ DeviceType.MediaPlayer,
61152
+ DeviceType.Thermostat,
61153
+ DeviceType.Climate
61154
+ ],
61155
+ methods: {},
61156
+ status: {
61157
+ schema: GroupCommandsSchema,
61158
+ kind: "push"
61159
+ },
61160
+ runtimeState: GroupCommandsSchema,
61161
+ /**
61162
+ * Runtime-state durability: **session** — the last attempt. A restored
61163
+ * failure would outlive the command that caused it.
61164
+ *
61165
+ * See `RuntimeStateDurability`. Enforced by
61166
+ * `scripts/check-runtime-state-durability.ts`.
61167
+ */
61168
+ durability: "session",
61169
+ history: "none"
61170
+ };
61171
+ /**
61172
+ * Whether a helper actuator group counts as active (D944): at least one
61173
+ * included member is, or every included member is. What "active" means is the
61174
+ * type's: a cover is open, a lock is locked, a media player is playing, a
61175
+ * climate device is not off. A group of lights, switches and fans reports
61176
+ * `switch.on` instead; this cap is for the types whose own state is not a
61177
+ * yes/no. Unknown members are left out (`group-status` counts them) and an
61178
+ * empty included set takes the device offline, never `active: false`.
61179
+ */
61180
+ var GroupActiveSchema = object({
61181
+ active: boolean().meta({ label: "Active" }),
61182
+ /** Ms epoch when the slice was last updated. */
61183
+ lastChangedAt: number()
61184
+ });
61185
+ var groupActiveCapability = {
61186
+ name: "group-active",
61187
+ scope: "device",
61188
+ deviceNative: true,
61189
+ mode: "singleton",
61190
+ deviceTypes: [
61191
+ DeviceType.Cover,
61192
+ DeviceType.Valve,
61193
+ DeviceType.Lock,
61194
+ DeviceType.MediaPlayer,
61195
+ DeviceType.Climate,
61196
+ DeviceType.Thermostat
61197
+ ],
61198
+ methods: {},
61199
+ status: {
61200
+ schema: GroupActiveSchema,
61201
+ kind: "push"
61202
+ },
61203
+ runtimeState: GroupActiveSchema,
61204
+ /**
61205
+ * Runtime-state durability: **session** — recomputed from the members on the
61206
+ * next source event; a restored value would outlive the members it named.
61207
+ *
61208
+ * See `RuntimeStateDurability`. Enforced by
61209
+ * `scripts/check-runtime-state-durability.ts`.
61210
+ */
61211
+ durability: "session",
61212
+ history: "none"
61213
+ };
61214
+ /**
61215
+ * How many members of a helper group answered (D944). Counts are integers,
61216
+ * not measurements: zero included is a real count, never a stand-in reading.
61217
+ * The reading itself stays on the output capability. History is none — the
61218
+ * series that later statistics read is the output cap's own history block.
61219
+ */
61220
+ var GroupStatusSchema = object({
61221
+ /** Members whose current value entered the aggregate. */
61222
+ included: number().int().nonnegative().meta(readingMeta("count", {
61223
+ precision: 0,
61224
+ label: "Reporting"
61225
+ })),
61226
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
61227
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
61228
+ precision: 0,
61229
+ label: "Excluded"
61230
+ })),
61231
+ /** `included` + `excluded`. */
61232
+ total: number().int().nonnegative().meta(readingMeta("count", {
61233
+ precision: 0,
61234
+ label: "Members"
61235
+ }))
61236
+ });
61237
+ var groupStatusCapability = {
61238
+ name: "group-status",
61239
+ scope: "device",
61240
+ deviceNative: true,
61241
+ mode: "singleton",
61242
+ deviceTypes: [
61243
+ DeviceType.Sensor,
61244
+ DeviceType.Switch,
61245
+ DeviceType.Light,
61246
+ DeviceType.Fan,
61247
+ DeviceType.Cover,
61248
+ DeviceType.Valve,
61249
+ DeviceType.Lock,
61250
+ DeviceType.MediaPlayer,
61251
+ DeviceType.Thermostat,
61252
+ DeviceType.Climate
61253
+ ],
61254
+ methods: {},
61255
+ status: {
61256
+ schema: GroupStatusSchema,
61257
+ kind: "push"
61258
+ },
61259
+ runtimeState: GroupStatusSchema,
61260
+ /**
61261
+ * Runtime-state durability: **session** — recomputed from members on the
61262
+ * next source event; a restored count would outlive the readings it named.
61263
+ *
61264
+ * See `RuntimeStateDurability`. Enforced by
61265
+ * `scripts/check-runtime-state-durability.ts`.
61266
+ */
61267
+ durability: "session",
61268
+ history: "none"
61269
+ };
60413
61270
  var HeatingValveStatusSchema = object({
60414
61271
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60415
- position: number().min(0).max(100).nullable(),
61272
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60416
61273
  /** Ms epoch when the slice was last updated (push or poll). */
60417
61274
  lastFetchedAt: number()
60418
61275
  });
@@ -60455,9 +61312,9 @@ var HumidifierStatusSchema = object({
60455
61312
  /** Whether the humidifier is currently on. */
60456
61313
  on: boolean(),
60457
61314
  /** Current measured relative humidity (0..100). Null when not reported. */
60458
- currentHumidity: number().min(0).max(100).nullable(),
61315
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60459
61316
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60460
- targetHumidity: number().min(0).max(100).nullable(),
61317
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60461
61318
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60462
61319
  * device has no mode surface. */
60463
61320
  mode: string().nullable(),
@@ -60467,9 +61324,9 @@ var HumidifierStatusSchema = object({
60467
61324
  * `idle` / `off`). Null when the device doesn't report it. */
60468
61325
  action: string().nullable(),
60469
61326
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60470
- minHumidity: number().nullable(),
61327
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60471
61328
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60472
- maxHumidity: number().nullable(),
61329
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60473
61330
  /** Ms epoch when the slice was last updated. */
60474
61331
  lastChangedAt: number()
60475
61332
  });
@@ -60539,22 +61396,13 @@ var humidifierCapability = {
60539
61396
  * `sourceInfo`.
60540
61397
  */
60541
61398
  var HumiditySensorStatusSchema = object({
60542
- /** Current relative humidity, 0..100. */
60543
- percent: number().min(0).max(100).meta({
60544
- unit: "%",
61399
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
61400
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
61401
+ precision: 0,
60545
61402
  label: "Humidity"
60546
- }),
61403
+ })),
60547
61404
  /** Ms epoch when the slice was last updated. */
60548
- lastFetchedAt: number(),
60549
- /** Live display unit from the upstream source (e.g. HA
60550
- * `attributes.unit_of_measurement`). The UI prefers this over the
60551
- * role's canonical unit. Absent → fall back to the canonical unit. */
60552
- unit: string().optional(),
60553
- /** Suggested decimal places for numeric display.
60554
- * Populated live from the upstream source when provided (e.g. HA
60555
- * `attributes.suggested_display_precision`). Falls back to
60556
- * auto-formatting when absent. */
60557
- precision: number().int().min(0).max(10).optional()
61405
+ lastFetchedAt: number()
60558
61406
  });
60559
61407
  var humiditySensorCapability = {
60560
61408
  name: "humidity-sensor",
@@ -60585,12 +61433,7 @@ var humiditySensorCapability = {
60585
61433
  } } },
60586
61434
  /** Clock fields: written, but excluded from the compare that decides
60587
61435
  * whether persisting is worth a SQLite commit. */
60588
- volatileStateFields: ["lastFetchedAt"],
60589
- customize: {
60590
- version: 1,
60591
- advanced: "derive",
60592
- targets: []
60593
- }
61436
+ volatileStateFields: ["lastFetchedAt"]
60594
61437
  };
60595
61438
  /**
60596
61439
  * Image display cap. Models a single still image exposed by an integration —
@@ -60611,7 +61454,7 @@ var ImageStatusSchema = object({
60611
61454
  * entity exposes no `entity_picture` (yet). */
60612
61455
  url: string().nullable(),
60613
61456
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60614
- lastUpdated: number().nullable()
61457
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60615
61458
  });
60616
61459
  var imageCapability = {
60617
61460
  name: "image",
@@ -60699,19 +61542,19 @@ var NormalizedRangeSchema = object({
60699
61542
  */
60700
61543
  var ImageSettingsStatusSchema = object({
60701
61544
  /** Normalized 0–100. */
60702
- brightness: number().optional(),
61545
+ brightness: number().optional().meta(readingMeta("none")),
60703
61546
  /** Normalized 0–100. */
60704
- contrast: number().optional(),
61547
+ contrast: number().optional().meta(readingMeta("none")),
60705
61548
  /** Normalized 0–100. */
60706
- saturation: number().optional(),
61549
+ saturation: number().optional().meta(readingMeta("none")),
60707
61550
  /** Normalized 0–100. */
60708
- sharpness: number().optional(),
61551
+ sharpness: number().optional().meta(readingMeta("none")),
60709
61552
  mirror: boolean().optional(),
60710
61553
  flip: boolean().optional(),
60711
61554
  rotate: ImageRotateSchema.optional(),
60712
61555
  whiteBalance: WhiteBalanceModeSchema.optional(),
60713
61556
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60714
- warmth: number().optional(),
61557
+ warmth: number().optional().meta(readingMeta("none")),
60715
61558
  exposureMode: ExposureModeSchema.optional(),
60716
61559
  backlightMode: BacklightModeSchema.optional(),
60717
61560
  lastFetchedAt: number()
@@ -60990,10 +61833,10 @@ var IntercomAbilitySchema = object({
60990
61833
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60991
61834
  codecs: array(string()),
60992
61835
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60993
- sampleRate: number(),
61836
+ sampleRate: number().meta(readingMeta("none")),
60994
61837
  duplex: _enum(["half", "full"]),
60995
61838
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60996
- maxBacklogMs: number()
61839
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60997
61840
  });
60998
61841
  var IntercomStatusSchema = object({
60999
61842
  /** True while a talk session is open. */
@@ -61220,13 +62063,13 @@ var LawnMowerControlStatusSchema = object({
61220
62063
  activity: LawnMowerActivitySchema,
61221
62064
  /** 0..100 battery percentage. Null when the device has no battery
61222
62065
  * reading. */
61223
- batteryLevel: number().min(0).max(100).nullable(),
62066
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61224
62067
  /** 0..100 mowing-completion percentage of the current task, or null when no
61225
62068
  * task is active / progress is unavailable. */
61226
- progressPercent: number().min(0).max(100).nullable(),
62069
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61227
62070
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61228
62071
  * or null when unknown. */
61229
- currentCode: number().nullable(),
62072
+ currentCode: number().nullable().meta(readingMeta("none")),
61230
62073
  /** Human label for {@link currentCode}, or null when undecodable. */
61231
62074
  currentCodeLabel: string().nullable(),
61232
62075
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61679,7 +62522,14 @@ var lockControlCapability = {
61679
62522
  }),
61680
62523
  unlock: method(object({
61681
62524
  deviceId: number().int().nonnegative(),
61682
- code: string().min(1).optional()
62525
+ code: string().min(1).optional(),
62526
+ /**
62527
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
62528
+ * with one call is refused unless the caller says it was confirmed. A
62529
+ * single lock ignores it. The refusal lives in the callee, so no
62530
+ * automation or API caller reaches the doors by skipping a dialog.
62531
+ */
62532
+ confirm: boolean().optional()
61683
62533
  }), _void(), {
61684
62534
  kind: "mutation",
61685
62535
  auth: "protected"
@@ -61716,6 +62566,44 @@ var lockControlCapability = {
61716
62566
  volatileStateFields: ["lastChangedAt"]
61717
62567
  };
61718
62568
  /**
62569
+ * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
62570
+ * so dashboards keep one series; every other instantaneous reading lands here.
62571
+ * `value` is the time integral in the source quantity's accumulated base
62572
+ * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
62573
+ */
62574
+ var MeterStatusSchema = object({
62575
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
62576
+ lastFetchedAt: number()
62577
+ });
62578
+ var meterCapability = {
62579
+ name: "meter",
62580
+ scope: "device",
62581
+ deviceNative: true,
62582
+ mode: "singleton",
62583
+ deviceTypes: [DeviceType.Sensor],
62584
+ methods: {},
62585
+ status: {
62586
+ schema: MeterStatusSchema,
62587
+ kind: "push"
62588
+ },
62589
+ runtimeState: MeterStatusSchema,
62590
+ /**
62591
+ * Runtime-state durability: **restored** — the total is a cache of the
62592
+ * history integral. A restart shows it only until the history pass
62593
+ * recomputes the open period; it is not an in-memory buffer.
62594
+ *
62595
+ * See `RuntimeStateDurability`. Enforced by
62596
+ * `scripts/check-runtime-state-durability.ts`.
62597
+ */
62598
+ durability: "restored",
62599
+ history: { fields: { value: {
62600
+ kind: "counter",
62601
+ deadband: .001,
62602
+ minIntervalMs: 6e4
62603
+ } } },
62604
+ volatileStateFields: ["lastFetchedAt"]
62605
+ };
62606
+ /**
61719
62607
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
61720
62608
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
61721
62609
  * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
@@ -61759,7 +62647,7 @@ var MediaPlayerStatusSchema = object({
61759
62647
  state: MediaPlayerStateSchema,
61760
62648
  /** Volume as 0..100 inclusive. Null when the device has no volume
61761
62649
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61762
- volumeLevel: number().min(0).max(100).nullable(),
62650
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61763
62651
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61764
62652
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61765
62653
  isMuted: boolean().nullable(),
@@ -61772,10 +62660,10 @@ var MediaPlayerStatusSchema = object({
61772
62660
  currentMedia: MediaInfoSchema.nullable(),
61773
62661
  /** Current playback position in ms. Null when not seekable or
61774
62662
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61775
- positionMs: number().int().nonnegative().nullable(),
62663
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61776
62664
  /** Total duration of the current media in ms. Null when unknown
61777
62665
  * (live stream) or nothing is playing. */
61778
- durationMs: number().int().nonnegative().nullable(),
62666
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61779
62667
  /** Shuffle toggle. Null when no shuffle surface.
61780
62668
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61781
62669
  shuffle: boolean().nullable(),
@@ -62084,7 +62972,7 @@ var motionCustomize = {
62084
62972
  eventName: "onMotionChanged"
62085
62973
  },
62086
62974
  deviceTypes: [DeviceType.Camera],
62087
- apply: "replace",
62975
+ apply: "add",
62088
62976
  applyAuthority: {
62089
62977
  kind: "camera-sensors",
62090
62978
  timeoutField: "motionCooldownMs"
@@ -62156,7 +63044,7 @@ var MotionStatusSchema = object({
62156
63044
  * it, every provider seeded it at boot, and every session left it
62157
63045
  * `detected: true` after its detach.
62158
63046
  */
62159
- autoClearAfterMs: number().nullable()
63047
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62160
63048
  });
62161
63049
  /**
62162
63050
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62193,12 +63081,18 @@ var motionCapability = {
62193
63081
  methods: {
62194
63082
  /**
62195
63083
  * Pull the current motion state synchronously. Convenience shortcut
62196
- * for consumers that don't need the full status object; equivalent
62197
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62198
- * `getStatus` once the auto-injected status surface lands in every
62199
- * consumer.
63084
+ * for consumers that don't need the full status object. A composed
63085
+ * group answers from its slice (D944): unknown throws, it is not off.
62200
63086
  */
62201
63087
  isDetected: method(object({ deviceId: number() }), boolean()) },
63088
+ /**
63089
+ * A composed motion group serves `isDetected` from `detected` (D944).
63090
+ * Without this the cap stays uncomposable (`has-commands`).
63091
+ */
63092
+ composedMethods: { isDetected: {
63093
+ kind: "slice",
63094
+ field: "detected"
63095
+ } },
62202
63096
  events: {
62203
63097
  /**
62204
63098
  * Fires every time the runner transitions a camera between
@@ -62338,7 +63232,7 @@ var MotionZoneRegionSchema = object({
62338
63232
  * the grid region(s). */
62339
63233
  var MotionZoneStatusSchema = object({
62340
63234
  enabled: boolean(),
62341
- sensitivity: number(),
63235
+ sensitivity: number().meta(readingMeta("none")),
62342
63236
  /** Grid region(s). Today exactly one `grid` shape. */
62343
63237
  regions: array(MotionZoneRegionSchema),
62344
63238
  lastFetchedAt: number()
@@ -62708,7 +63602,7 @@ var NavigationStatusSchema = object({
62708
63602
  following: boolean(),
62709
63603
  flash: boolean(),
62710
63604
  lightMode: NavigationLightModeSchema,
62711
- lightLevel: number().min(40).max(100),
63605
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62712
63606
  /** Ms epoch when the slice was last updated. */
62713
63607
  lastChangedAt: number()
62714
63608
  });
@@ -62842,13 +63736,13 @@ var NetworkLinkStatusSchema = object({
62842
63736
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62843
63737
  * SKIP a null rather than coerce it.
62844
63738
  */
62845
- signalPercent: number().min(0).max(100).nullable(),
63739
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62846
63740
  /** Raw received signal strength in dBm, when the firmware reports one. */
62847
- rssiDbm: number().optional(),
63741
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62848
63742
  /** Network name of a wireless link, when the firmware reports it. */
62849
63743
  ssid: string().optional(),
62850
63744
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62851
- lastUpdated: number()
63745
+ lastUpdated: number().meta(readingMeta("none"))
62852
63746
  });
62853
63747
  var networkLinkCapability = {
62854
63748
  name: "network-link",
@@ -63233,7 +64127,7 @@ var NotifierStatusSchema = object({
63233
64127
  lastError: string().nullable(),
63234
64128
  /** Number of deliveries currently buffered server-side (rate-limited
63235
64129
  * notifiers may queue). 0 when the provider sends synchronously. */
63236
- queueDepth: number().int().nonnegative(),
64130
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63237
64131
  /** Per-feature capability matrix — authoritative source for the
63238
64132
  * compose-form field gating. */
63239
64133
  supports: NotifierSupportsSchema
@@ -63325,11 +64219,9 @@ onSent: { data: object({
63325
64219
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63326
64220
  * for: water flow, distance, weight, frequency, signal strength, …).
63327
64221
  *
63328
- * The `unit` and `precision` fields carry the live display metadata that
63329
- * the upstream source provides with each state push (HA
63330
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63331
- * The UI reads them directly from the slice — they are the single source
63332
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
64222
+ * The quantity is not on this slice. It is chosen when the device is
64223
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
64224
+ * that quantity's base unit. `null` is a refused sample (D393).
63333
64225
  *
63334
64226
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63335
64227
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63338,15 +64230,8 @@ onSent: { data: object({
63338
64230
  * numeric values land here.
63339
64231
  */
63340
64232
  var NumericSensorStatusSchema = object({
63341
- value: number(),
63342
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63343
- * Populated live from the upstream source on each state push.
63344
- * Absent for unitless sensors. */
63345
- unit: string().optional(),
63346
- /** Suggested decimal places for numeric display.
63347
- * Populated live from the upstream source when provided.
63348
- * Falls back to auto-formatting when absent. */
63349
- precision: number().int().min(0).max(10).optional(),
64233
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
64234
+ value: number().nullable().meta(readingMeta("device")),
63350
64235
  /** Ms epoch when the slice was last updated. */
63351
64236
  lastFetchedAt: number()
63352
64237
  });
@@ -63370,11 +64255,11 @@ var numericSensorCapability = {
63370
64255
  */
63371
64256
  durability: "restored",
63372
64257
  /**
63373
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
64258
+ * History (D850): recorded series. The unit is the device quantity's base,
64259
+ * resolved when the series is read — the cap has no single unit.
63374
64260
  */
63375
64261
  history: { fields: { value: {
63376
64262
  kind: "gauge",
63377
- unitFrom: "unit",
63378
64263
  minIntervalMs: 3e4
63379
64264
  } } },
63380
64265
  /** Clock fields: written, but excluded from the compare that decides
@@ -63666,23 +64551,26 @@ var PetFeederStatusSchema = object({
63666
64551
  /** Food currently in the bowl (grams). Null when the device has not
63667
64552
  * reported a reading yet. On dual-hopper models this is the combined
63668
64553
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63669
- foodLevel: number().nullable(),
64554
+ foodLevel: number().nullable().meta(readingMeta("none")),
63670
64555
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63671
64556
  * single-hopper models. */
63672
- food1: number().nullable(),
64557
+ food1: number().nullable().meta(readingMeta("none")),
63673
64558
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63674
64559
  * single-hopper models. */
63675
- food2: number().nullable(),
64560
+ food2: number().nullable().meta(readingMeta("none")),
63676
64561
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63677
64562
  * (`device_class: problem`, on = low). True when the bowl is empty /
63678
64563
  * below the feeder's low threshold. */
63679
64564
  lowFood: boolean(),
63680
64565
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63681
64566
  * device has no battery reading. */
63682
- batteryPower: number().min(0).max(100).nullable(),
63683
- /** Days of desiccant life remaining. Null when the model has no
63684
- * desiccant sensor. */
63685
- desiccantLeftDays: number().nullable(),
64567
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64568
+ /**
64569
+ * Desiccant life remaining, stored in seconds (duration base, D944).
64570
+ * Providers convert from the days the feeder reports. Null when the
64571
+ * model has no desiccant sensor.
64572
+ */
64573
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63686
64574
  /** True while a feed is in progress. */
63687
64575
  feeding: boolean(),
63688
64576
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63693,7 +64581,7 @@ var PetFeederStatusSchema = object({
63693
64581
  * with `errorCode` for consumers that want the raw integer. */
63694
64582
  error: string().nullable(),
63695
64583
  /** Raw device error code (0 / null = no error). */
63696
- errorCode: number().nullable(),
64584
+ errorCode: number().nullable().meta(readingMeta("none")),
63697
64585
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63698
64586
  isDualHopper: boolean(),
63699
64587
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63703,7 +64591,7 @@ var PetFeederStatusSchema = object({
63703
64591
  /** Play a chime when dispensing. */
63704
64592
  feedSound: boolean(),
63705
64593
  /** Speaker / prompt volume level (device-scaled integer). */
63706
- volume: number(),
64594
+ volume: number().meta(readingMeta("none")),
63707
64595
  /** Ms epoch when the slice was last refreshed from the cloud. */
63708
64596
  lastFetchedAt: number()
63709
64597
  });
@@ -63822,7 +64710,7 @@ var petFeederCapability = {
63822
64710
  },
63823
64711
  desiccantLeftDays: {
63824
64712
  kind: "gauge",
63825
- unit: "days"
64713
+ unit: "s"
63826
64714
  }
63827
64715
  } }
63828
64716
  };
@@ -64223,43 +65111,88 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64223
65111
  */
64224
65112
  var PowerMeterStatusSchema = object({
64225
65113
  /** Instantaneous power draw in watts. */
64226
- watts: number().optional().meta({
64227
- unit: "W",
64228
- label: "Power"
64229
- }),
65114
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64230
65115
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64231
- kwhTotal: number().optional().meta({
64232
- unit: "kWh",
64233
- label: "Energy"
64234
- }),
65116
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64235
65117
  /** Voltage in volts. */
64236
- volts: number().optional(),
65118
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
64237
65119
  /** Current in amperes. */
64238
- amps: number().optional(),
64239
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64240
- va: number().optional(),
65120
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
65121
+ /** Apparent power in volt-amperes. */
65122
+ va: number().optional().meta(readingMeta("apparent-power")),
64241
65123
  /** Power factor (0..1) if reported. */
64242
- powerFactor: number().min(0).max(1).optional(),
65124
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64243
65125
  /** Ms epoch when the slice was last updated. */
64244
- lastFetchedAt: number(),
64245
- /** Live display unit of the single metric this slice carries (e.g. HA
64246
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64247
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64248
- * unit per slice is unambiguous. The UI prefers this over the role's
64249
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64250
- unit: string().optional(),
64251
- /** Suggested decimal places for numeric display.
64252
- * Populated live from the upstream source when provided (e.g. HA
64253
- * `attributes.suggested_display_precision`). Falls back to
64254
- * auto-formatting when absent. */
64255
- precision: number().int().min(0).max(10).optional()
65126
+ lastFetchedAt: number()
64256
65127
  });
65128
+ /**
65129
+ * Loads that draw metered mains power (D951). The reading is a feature of the
65130
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
65131
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
65132
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
65133
+ * camera and the like are left out: their draw is not what anyone meters.
65134
+ */
65135
+ var POWER_METER_DEVICE_TYPES = [
65136
+ DeviceType.Sensor,
65137
+ DeviceType.Switch,
65138
+ DeviceType.Light,
65139
+ DeviceType.Fan,
65140
+ DeviceType.Climate,
65141
+ DeviceType.Thermostat,
65142
+ DeviceType.Humidifier,
65143
+ DeviceType.WaterHeater,
65144
+ DeviceType.MediaPlayer,
65145
+ DeviceType.Washer,
65146
+ DeviceType.Dryer,
65147
+ DeviceType.Dishwasher,
65148
+ DeviceType.Vacuum,
65149
+ DeviceType.LawnMower,
65150
+ DeviceType.PetFeeder
65151
+ ];
65152
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
65153
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
65154
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
65155
+ function powerTarget(id, unit) {
65156
+ return {
65157
+ id,
65158
+ cardinality: "scalar",
65159
+ vocabulary: {
65160
+ cap: "power-meter",
65161
+ stateField: id
65162
+ },
65163
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
65164
+ apply: "add",
65165
+ path: id,
65166
+ sources: [{
65167
+ kind: "numeric",
65168
+ requiredUnit: unit,
65169
+ transforms: [{ kind: "identity" }]
65170
+ }],
65171
+ companions: [{
65172
+ kind: "clock",
65173
+ path: "lastFetchedAt",
65174
+ when: "always"
65175
+ }],
65176
+ sourceReport: { status: "assignment-status" },
65177
+ advancedExpression: true
65178
+ };
65179
+ }
65180
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
65181
+ var powerMeterCustomize = {
65182
+ version: 1,
65183
+ targets: [
65184
+ powerTarget("watts", "W"),
65185
+ powerTarget("kwhTotal", "kWh"),
65186
+ powerTarget("volts", "V"),
65187
+ powerTarget("amps", "A")
65188
+ ]
65189
+ };
64257
65190
  var powerMeterCapability = {
64258
65191
  name: "power-meter",
64259
65192
  scope: "device",
64260
65193
  deviceNative: true,
64261
65194
  mode: "singleton",
64262
- deviceTypes: [DeviceType.Sensor],
65195
+ deviceTypes: POWER_METER_DEVICE_TYPES,
64263
65196
  methods: {},
64264
65197
  status: {
64265
65198
  schema: PowerMeterStatusSchema,
@@ -64317,11 +65250,7 @@ var powerMeterCapability = {
64317
65250
  /** Clock fields: written, but excluded from the compare that decides
64318
65251
  * whether persisting is worth a SQLite commit. */
64319
65252
  volatileStateFields: ["lastFetchedAt"],
64320
- customize: {
64321
- version: 1,
64322
- advanced: "derive",
64323
- targets: []
64324
- }
65253
+ customize: powerMeterCustomize
64325
65254
  };
64326
65255
  /**
64327
65256
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64341,11 +65270,11 @@ var powerMeterCapability = {
64341
65270
  */
64342
65271
  var GpsLocationSchema = object({
64343
65272
  /** Latitude in decimal degrees, -90..90. */
64344
- latitude: number().min(-90).max(90),
65273
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64345
65274
  /** Longitude in decimal degrees, -180..180. */
64346
- longitude: number().min(-180).max(180),
65275
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64347
65276
  /** Reported accuracy in meters (lower = better). */
64348
- accuracyMeters: number().nonnegative()
65277
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64349
65278
  });
64350
65279
  var PresenceStatusSchema = object({
64351
65280
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64357,7 +65286,7 @@ var PresenceStatusSchema = object({
64357
65286
  gps: GpsLocationSchema.nullable(),
64358
65287
  /** Optional battery level of the tracking device (0..100). Null
64359
65288
  * when not reported. */
64360
- batteryPercent: number().min(0).max(100).nullable(),
65289
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64361
65290
  /** Ms epoch when the slice was last updated. */
64362
65291
  lastChangedAt: number()
64363
65292
  });
@@ -64407,19 +65336,10 @@ var presenceCapability = {
64407
65336
  * `sourceInfo`.
64408
65337
  */
64409
65338
  var PressureSensorStatusSchema = object({
64410
- /** Current pressure in hPa. */
64411
- hpa: number(),
65339
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
65340
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64412
65341
  /** Ms epoch when the slice was last updated. */
64413
- lastFetchedAt: number(),
64414
- /** Live display unit from the upstream source (e.g. HA
64415
- * `attributes.unit_of_measurement`). The UI prefers this over the
64416
- * role's canonical unit. Absent → fall back to the canonical unit. */
64417
- unit: string().optional(),
64418
- /** Suggested decimal places for numeric display.
64419
- * Populated live from the upstream source when provided (e.g. HA
64420
- * `attributes.suggested_display_precision`). Falls back to
64421
- * auto-formatting when absent. */
64422
- precision: number().int().min(0).max(10).optional()
65342
+ lastFetchedAt: number()
64423
65343
  });
64424
65344
  var pressureSensorCapability = {
64425
65345
  name: "pressure-sensor",
@@ -64833,8 +65753,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64833
65753
  }), _void(), { kind: "mutation" });
64834
65754
  var PtzAutotrackSettingsSchema = object({
64835
65755
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64836
- stopDelaySeconds: number().int().min(0).max(300),
64837
- disappearDelaySeconds: number().int().min(0).max(300)
65756
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65757
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64838
65758
  });
64839
65759
  /**
64840
65760
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64988,7 +65908,7 @@ var RunningStateStatusSchema = object({
64988
65908
  /** null = unknown — never coerced to false (D393). */
64989
65909
  running: boolean().nullable(),
64990
65910
  /** Epoch ms the current boolean began; null while `running` is null. */
64991
- since: number().nullable(),
65911
+ since: number().nullable().meta(readingMeta("none")),
64992
65912
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64993
65913
  phase: string().nullable(),
64994
65914
  /** null until a reading has been published (D393). */
@@ -65003,7 +65923,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65003
65923
  /** Generic Customize target for the running boolean (D936). */
65004
65924
  var runningCustomize = {
65005
65925
  version: 1,
65006
- advanced: "derive",
65007
65926
  targets: [{
65008
65927
  id: "running",
65009
65928
  cardinality: "scalar",
@@ -65012,6 +65931,8 @@ var runningCustomize = {
65012
65931
  stateField: "running"
65013
65932
  },
65014
65933
  deviceTypes: [
65934
+ DeviceType.Washer,
65935
+ DeviceType.Dryer,
65015
65936
  DeviceType.Dishwasher,
65016
65937
  DeviceType.Vacuum,
65017
65938
  DeviceType.LawnMower,
@@ -65023,7 +65944,7 @@ var runningCustomize = {
65023
65944
  DeviceType.Thermostat,
65024
65945
  DeviceType.PetFeeder
65025
65946
  ],
65026
- apply: "add-or-replace",
65947
+ apply: "add",
65027
65948
  path: "running",
65028
65949
  sources: [
65029
65950
  {
@@ -66287,9 +67208,9 @@ var RecordTriggerSchema = _enum([
66287
67208
  */
66288
67209
  var RecordWindowSchema = object({
66289
67210
  trigger: RecordTriggerSchema,
66290
- day: number().int().min(0).max(6),
66291
- startMinute: number().int().min(0).max(1439),
66292
- endMinute: number().int().min(1).max(1440)
67211
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
67212
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
67213
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
66293
67214
  });
66294
67215
  /** Status of one physical volume, as the camera itself describes it. */
66295
67216
  var OnboardStorageVolumeSchema = object({
@@ -66365,10 +67286,10 @@ var RecordingOnboardStatusSchema = object({
66365
67286
  /** Master "record to the card at all" switch. */
66366
67287
  enabled: boolean().nullable(),
66367
67288
  overwriteWhenFull: boolean().nullable(),
66368
- preRecordSec: number().nullable(),
66369
- postRecordSec: number().nullable(),
67289
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
67290
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66370
67291
  /** Length of one recorded file, in minutes. */
66371
- segmentMinutes: number().nullable(),
67292
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66372
67293
  /** The primary track's weekly windows, flattened. */
66373
67294
  windows: array(RecordWindowSchema),
66374
67295
  /**
@@ -66384,7 +67305,7 @@ var RecordingOnboardStatusSchema = object({
66384
67305
  * provider reporting a non-zero count also reports the schedule as not
66385
67306
  * writable.
66386
67307
  */
66387
- unreadableWindows: number(),
67308
+ unreadableWindows: number().meta(readingMeta("count")),
66388
67309
  /**
66389
67310
  * The camera is scheduled to record and has NO usable storage.
66390
67311
  *
@@ -67229,7 +68150,7 @@ var CamStreamDescriptorSchema = object({
67229
68150
  url: string().optional(),
67230
68151
  codec: string().optional(),
67231
68152
  resolution: CamStreamResolutionSchema.optional(),
67232
- fps: number().positive().optional(),
68153
+ fps: number().positive().optional().meta(readingMeta("rate")),
67233
68154
  label: string().optional(),
67234
68155
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
67235
68156
  deviceFeatures: array(string()).optional(),
@@ -67316,18 +68237,18 @@ var StreamProfileSchema = _enum([
67316
68237
  ]);
67317
68238
  /** The current encoding config of one stream profile. */
67318
68239
  var StreamProfileConfigSchema = object({
67319
- width: number(),
67320
- height: number(),
68240
+ width: number().meta(readingMeta("none")),
68241
+ height: number().meta(readingMeta("none")),
67321
68242
  codec: _enum(["h264", "h265"]),
67322
- framerate: number(),
67323
- bitrate: number(),
68243
+ framerate: number().meta(readingMeta("rate")),
68244
+ bitrate: number().meta(readingMeta("bitrate")),
67324
68245
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
67325
68246
  encoderProfile: _enum([
67326
68247
  "high",
67327
68248
  "main",
67328
68249
  "baseline"
67329
68250
  ]).optional(),
67330
- gop: number().optional(),
68251
+ gop: number().optional().meta(readingMeta("none")),
67331
68252
  /**
67332
68253
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
67333
68254
  *
@@ -67501,7 +68422,8 @@ var switchCapability = {
67501
68422
  deviceTypes: [
67502
68423
  DeviceType.Switch,
67503
68424
  DeviceType.Siren,
67504
- DeviceType.Light
68425
+ DeviceType.Light,
68426
+ DeviceType.Fan
67505
68427
  ],
67506
68428
  methods: { setState: method(object({
67507
68429
  deviceId: number(),
@@ -67548,12 +68470,7 @@ var switchCapability = {
67548
68470
  label: "On",
67549
68471
  kind: "boolean"
67550
68472
  }
67551
- }] },
67552
- customize: {
67553
- version: 1,
67554
- advanced: "derive",
67555
- targets: []
67556
- }
68473
+ }] }
67557
68474
  };
67558
68475
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67559
68476
  function hasControlCharacter(value) {
@@ -68136,35 +69053,21 @@ var tamperCapability = {
68136
69053
  * entries with `device_class: temperature` and any future native
68137
69054
  * thermometer.
68138
69055
  *
68139
- * Unit normalisation: providers convert to Celsius before storing.
68140
- * The slice value is always Celsius so cross-cap aggregators
68141
- * (climate-control's `currentTemp`, energy analytics) can compose
68142
- * without per-source unit fixups. The canonical display unit (`°C`) is
68143
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68144
- * `sourceInfo`.
69056
+ * The number is Celsius (D944). Providers convert on ingest. A failed
69057
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
68145
69058
  *
68146
69059
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
68147
69060
  * frozen feed (provider hung) distinct from a "temperature hasn't
68148
69061
  * changed" steady state.
68149
69062
  */
68150
69063
  var TemperatureSensorStatusSchema = object({
68151
- /** Current temperature in Celsius. */
68152
- celsius: number().meta({
68153
- unit: "°C",
69064
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
69065
+ celsius: number().nullable().meta(readingMeta("temperature", {
69066
+ precision: 1,
68154
69067
  label: "Temperature"
68155
- }),
69068
+ })),
68156
69069
  /** Ms epoch when the slice was last updated (push or poll). */
68157
- lastFetchedAt: number(),
68158
- /** Live display unit from the upstream source (e.g. HA
68159
- * `attributes.unit_of_measurement`). The UI prefers this over the
68160
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
68161
- * Absent → the UI falls back to the role's canonical unit. */
68162
- unit: string().optional(),
68163
- /** Suggested decimal places for numeric display.
68164
- * Populated live from the upstream source when provided (e.g. HA
68165
- * `attributes.suggested_display_precision`). Falls back to
68166
- * auto-formatting when absent. */
68167
- precision: number().int().min(0).max(10).optional()
69070
+ lastFetchedAt: number()
68168
69071
  });
68169
69072
  var temperatureSensorCapability = {
68170
69073
  name: "temperature-sensor",
@@ -68195,12 +69098,7 @@ var temperatureSensorCapability = {
68195
69098
  } } },
68196
69099
  /** Clock fields: written, but excluded from the compare that decides
68197
69100
  * whether persisting is worth a SQLite commit. */
68198
- volatileStateFields: ["lastFetchedAt"],
68199
- customize: {
68200
- version: 1,
68201
- advanced: "derive",
68202
- targets: []
68203
- }
69101
+ volatileStateFields: ["lastFetchedAt"]
68204
69102
  };
68205
69103
  /**
68206
69104
  * toast — system-scoped singleton capability that streams toast
@@ -68483,16 +69381,16 @@ var VacuumControlStatusSchema = object({
68483
69381
  ]),
68484
69382
  /** 0..100 battery percentage. Null when the device has no battery
68485
69383
  * reading. */
68486
- batteryLevel: number().min(0).max(100).nullable(),
69384
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68487
69385
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68488
69386
  * the vacuum has no speed control. */
68489
69387
  fanSpeed: string().nullable(),
68490
69388
  /** Speed tokens the hardware accepts — drives the UI selector. */
68491
69389
  availableFanSpeeds: array(string()),
68492
69390
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68493
- progressPercent: number().min(0).max(100).nullable(),
69391
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68494
69392
  /** Current error code (0 / null = no error). */
68495
- errorCode: number().nullable(),
69393
+ errorCode: number().nullable().meta(readingMeta("none")),
68496
69394
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68497
69395
  errorLabel: string().nullable(),
68498
69396
  /** Ms epoch when the slice was last updated. */
@@ -68570,7 +69468,7 @@ var ValveStatusSchema = object({
68570
69468
  ]),
68571
69469
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68572
69470
  * intermediate position surface. */
68573
- position: number().min(0).max(100).nullable(),
69471
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68574
69472
  /** Ms epoch when the slice was last updated. */
68575
69473
  lastChangedAt: number()
68576
69474
  });
@@ -68678,9 +69576,9 @@ var vibrationCapability = {
68678
69576
  */
68679
69577
  var WaterHeaterStatusSchema = object({
68680
69578
  /** Current measured temperature. Null when not reported. */
68681
- currentTemp: number().nullable(),
69579
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68682
69580
  /** Target temperature setpoint. Null when no setpoint surface. */
68683
- targetTemp: number().nullable(),
69581
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68684
69582
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68685
69583
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68686
69584
  * device reports an unknown state. */
@@ -68691,9 +69589,9 @@ var WaterHeaterStatusSchema = object({
68691
69589
  * has no away surface. */
68692
69590
  away: boolean().nullable(),
68693
69591
  /** HA `min_temp` attribute. Null when not reported. */
68694
- minTemp: number().nullable(),
69592
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68695
69593
  /** HA `max_temp` attribute. Null when not reported. */
68696
- maxTemp: number().nullable(),
69594
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68697
69595
  /** Ms epoch when the slice was last updated. */
68698
69596
  lastChangedAt: number()
68699
69597
  });
@@ -68778,25 +69676,18 @@ var waterHeaterCapability = {
68778
69676
  * Forecast deferred — current conditions only for v1.
68779
69677
  */
68780
69678
  var WeatherStatusSchema = object({
68781
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68782
- * when no condition has been reported yet. */
69679
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68783
69680
  condition: string().nullable(),
68784
- /** Current temperature in the reported unit. Null when not provided. */
68785
- temperature: number().nullable(),
68786
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68787
- temperatureUnit: string().nullable(),
69681
+ /** Current temperature in Celsius. Null when not provided or refused. */
69682
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68788
69683
  /** Relative humidity (0..100). Null when not provided. */
68789
- humidity: number().min(0).max(100).nullable(),
68790
- /** Barometric pressure in the reported unit. Null when not provided. */
68791
- pressure: number().nullable(),
68792
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68793
- pressureUnit: string().nullable(),
68794
- /** Wind speed in the reported unit. Null when not provided. */
68795
- windSpeed: number().nullable(),
68796
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68797
- windSpeedUnit: string().nullable(),
69684
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
69685
+ /** Barometric pressure in hPa. Null when not provided or refused. */
69686
+ pressure: number().nullable().meta(readingMeta("pressure")),
69687
+ /** Wind speed in metres per second. Null when not provided or refused. */
69688
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68798
69689
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68799
- windBearing: number().nullable(),
69690
+ windBearing: number().nullable().meta(readingMeta("angle")),
68800
69691
  /** Ms epoch when the slice was last updated. */
68801
69692
  lastFetchedAt: number()
68802
69693
  });
@@ -68824,12 +69715,12 @@ var weatherCapability = {
68824
69715
  */
68825
69716
  durability: "session",
68826
69717
  /**
68827
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69718
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68828
69719
  */
68829
69720
  history: { fields: {
68830
69721
  temperature: {
68831
69722
  kind: "gauge",
68832
- unitFrom: "temperatureUnit",
69723
+ unit: "°C",
68833
69724
  deadband: .2
68834
69725
  },
68835
69726
  humidity: {
@@ -68839,12 +69730,12 @@ var weatherCapability = {
68839
69730
  },
68840
69731
  pressure: {
68841
69732
  kind: "gauge",
68842
- unitFrom: "pressureUnit",
69733
+ unit: "hPa",
68843
69734
  deadband: .5
68844
69735
  },
68845
69736
  windSpeed: {
68846
69737
  kind: "gauge",
68847
- unitFrom: "windSpeedUnit",
69738
+ unit: "m/s",
68848
69739
  deadband: .5
68849
69740
  }
68850
69741
  } }
@@ -69745,8 +70636,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
69745
70636
  kind: "mutation",
69746
70637
  auth: "admin"
69747
70638
  });
70639
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
69748
70640
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69749
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Camera, DeviceType.Camera, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
70641
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter, DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer, DeviceType.Camera, DeviceType.Camera, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.MediaPlayer, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.PetFeeder, DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
69750
70642
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
69751
70643
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
69752
70644
  new Set(CAP_NAMES);
@@ -70075,6 +70967,9 @@ var DEVICE_LOCAL_STATE_CAPS = {
70075
70967
  featureProbe: featureProbeCapability,
70076
70968
  flood: floodCapability,
70077
70969
  gas: gasCapability,
70970
+ groupActive: groupActiveCapability,
70971
+ groupCommands: groupCommandsCapability,
70972
+ groupStatus: groupStatusCapability,
70078
70973
  heatingValve: heatingValveCapability,
70079
70974
  humidifier: humidifierCapability,
70080
70975
  humiditySensor: humiditySensorCapability,
@@ -70084,6 +70979,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
70084
70979
  lawnMowerControl: lawnMowerControlCapability,
70085
70980
  lockControl: lockControlCapability,
70086
70981
  mediaPlayer: mediaPlayerCapability,
70982
+ meter: meterCapability,
70087
70983
  motion: motionCapability,
70088
70984
  motionTrigger: motionTriggerCapability,
70089
70985
  motionZones: motionZonesCapability,
@@ -71343,7 +72239,7 @@ var BaseDeviceProvider = class extends BaseAddon {
71343
72239
  return toDeviceSummary(device, this.addonId);
71344
72240
  }
71345
72241
  };
71346
- DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
72242
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Camera, DeviceType.Hub, DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
71347
72243
  new Set(Object.values(DeviceType));
71348
72244
  Object.freeze({
71349
72245
  "accessories.getStatus": {
@@ -71451,6 +72347,13 @@ Object.freeze({
71451
72347
  access: "view",
71452
72348
  role: "admin"
71453
72349
  },
72350
+ "addons.getPackageDocs": {
72351
+ capName: "addons",
72352
+ capScope: "system",
72353
+ addonId: null,
72354
+ access: "view",
72355
+ role: "admin"
72356
+ },
71454
72357
  "addons.getVersions": {
71455
72358
  capName: "addons",
71456
72359
  capScope: "system",
@@ -74559,6 +75462,27 @@ Object.freeze({
74559
75462
  access: "view",
74560
75463
  role: "viewer"
74561
75464
  },
75465
+ "groupActive.getStatus": {
75466
+ capName: "group-active",
75467
+ capScope: "device",
75468
+ addonId: null,
75469
+ access: "view",
75470
+ role: "viewer"
75471
+ },
75472
+ "groupCommands.getStatus": {
75473
+ capName: "group-commands",
75474
+ capScope: "device",
75475
+ addonId: null,
75476
+ access: "view",
75477
+ role: "viewer"
75478
+ },
75479
+ "groupStatus.getStatus": {
75480
+ capName: "group-status",
75481
+ capScope: "device",
75482
+ addonId: null,
75483
+ access: "view",
75484
+ role: "viewer"
75485
+ },
74562
75486
  "heatingValve.getStatus": {
74563
75487
  capName: "heating-valve",
74564
75488
  capScope: "device",
@@ -75511,6 +76435,13 @@ Object.freeze({
75511
76435
  access: "create",
75512
76436
  role: "admin"
75513
76437
  },
76438
+ "meter.getStatus": {
76439
+ capName: "meter",
76440
+ capScope: "device",
76441
+ addonId: null,
76442
+ access: "view",
76443
+ role: "viewer"
76444
+ },
75514
76445
  "metricsProvider.collectSnapshot": {
75515
76446
  capName: "metrics-provider",
75516
76447
  capScope: "system",
@@ -83609,7 +84540,39 @@ var APPLIANCE_CAP_VOCABULARY = {
83609
84540
  phrase: t("When {device} finishes", "Quando {device} finisce"),
83610
84541
  icon: "square"
83611
84542
  }
83612
- }
84543
+ },
84544
+ overrides: { byType: {
84545
+ [DeviceType.Washer]: {
84546
+ state: { running: {
84547
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
84548
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
84549
+ } },
84550
+ events: {
84551
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
84552
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
84553
+ }
84554
+ },
84555
+ [DeviceType.Dryer]: {
84556
+ state: { running: {
84557
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
84558
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
84559
+ } },
84560
+ events: {
84561
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
84562
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
84563
+ }
84564
+ },
84565
+ [DeviceType.Dishwasher]: {
84566
+ state: { running: {
84567
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
84568
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
84569
+ } },
84570
+ events: {
84571
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
84572
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
84573
+ }
84574
+ }
84575
+ } }
83613
84576
  },
83614
84577
  update: {
83615
84578
  scope: "device",
@@ -84381,6 +85344,41 @@ var SENSOR_CAP_VOCABULARY = {
84381
85344
  icon: "flame"
84382
85345
  }]
84383
85346
  },
85347
+ "group-active": {
85348
+ scope: "device",
85349
+ noun: t("Group state", "Stato del gruppo")
85350
+ },
85351
+ "group-commands": {
85352
+ scope: "device",
85353
+ noun: t("Group command", "Comando di gruppo")
85354
+ },
85355
+ "group-status": {
85356
+ scope: "device",
85357
+ noun: t("Group", "Gruppo"),
85358
+ state: [
85359
+ {
85360
+ kind: "numeric",
85361
+ field: "included",
85362
+ quantity: t("the members reporting", "i membri che rispondono"),
85363
+ label: t("Reporting", "Rispondono"),
85364
+ icon: "users"
85365
+ },
85366
+ {
85367
+ kind: "numeric",
85368
+ field: "excluded",
85369
+ quantity: t("the members left out", "i membri esclusi"),
85370
+ label: t("Excluded", "Esclusi"),
85371
+ icon: "users"
85372
+ },
85373
+ {
85374
+ kind: "numeric",
85375
+ field: "total",
85376
+ quantity: t("the member count", "il numero di membri"),
85377
+ label: t("Members", "Membri"),
85378
+ icon: "sigma"
85379
+ }
85380
+ ]
85381
+ },
84384
85382
  "humidity-sensor": {
84385
85383
  scope: "device",
84386
85384
  noun: t("Humidity", "Umidità"),
@@ -84406,6 +85404,17 @@ var SENSOR_CAP_VOCABULARY = {
84406
85404
  }],
84407
85405
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
84408
85406
  },
85407
+ meter: {
85408
+ scope: "device",
85409
+ noun: t("Meter", "Contatore"),
85410
+ state: [{
85411
+ kind: "numeric",
85412
+ field: "value",
85413
+ quantity: t("the total", "il totale"),
85414
+ label: t("Total", "Totale"),
85415
+ icon: "gauge"
85416
+ }]
85417
+ },
84409
85418
  "numeric-sensor": {
84410
85419
  scope: "device",
84411
85420
  noun: t("Sensor", "Sensore"),
@@ -84414,7 +85423,6 @@ var SENSOR_CAP_VOCABULARY = {
84414
85423
  field: "value",
84415
85424
  quantity: t("the reading", "il valore"),
84416
85425
  label: t("Reading", "Valore"),
84417
- unitField: "unit",
84418
85426
  icon: "gauge"
84419
85427
  }]
84420
85428
  },
@@ -84550,7 +85558,7 @@ var SENSOR_CAP_VOCABULARY = {
84550
85558
  field: "temperature",
84551
85559
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84552
85560
  label: t("Temperature", "Temperatura"),
84553
- unitField: "temperatureUnit",
85561
+ unit: "°C",
84554
85562
  icon: "thermometer"
84555
85563
  },
84556
85564
  {
@@ -84566,7 +85574,7 @@ var SENSOR_CAP_VOCABULARY = {
84566
85574
  field: "windSpeed",
84567
85575
  quantity: t("the wind speed", "la velocità del vento"),
84568
85576
  label: t("Wind", "Vento"),
84569
- unitField: "windSpeedUnit",
85577
+ unit: "m/s",
84570
85578
  icon: "wind"
84571
85579
  },
84572
85580
  {
@@ -84574,7 +85582,7 @@ var SENSOR_CAP_VOCABULARY = {
84574
85582
  field: "pressure",
84575
85583
  quantity: t("the air pressure", "la pressione atmosferica"),
84576
85584
  label: t("Pressure", "Pressione"),
84577
- unitField: "pressureUnit",
85585
+ unit: "hPa",
84578
85586
  icon: "gauge"
84579
85587
  }
84580
85588
  ]
@@ -84879,8 +85887,47 @@ function projectAction(cap, action) {
84879
85887
  };
84880
85888
  }
84881
85889
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
84882
- DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher;
85890
+ DeviceType.Camera, DeviceType.Hub, DeviceType.Light, DeviceType.Siren, DeviceType.Switch, DeviceType.Sensor, DeviceType.Thermostat, DeviceType.Climate, DeviceType.Button, DeviceType.EventEmitter, DeviceType.Update, DeviceType.Generic, DeviceType.Notifier, DeviceType.Script, DeviceType.Automation, DeviceType.Lock, DeviceType.Cover, DeviceType.Valve, DeviceType.Humidifier, DeviceType.WaterHeater, DeviceType.Fan, DeviceType.MediaPlayer, DeviceType.AlarmPanel, DeviceType.Control, DeviceType.Presence, DeviceType.Weather, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.Container, DeviceType.Image, DeviceType.PetFeeder, DeviceType.Dishwasher, DeviceType.Washer, DeviceType.Dryer;
84883
85891
  DeviceFeature.BatteryOperated;
85892
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
85893
+ var HA_AS = "a power sensor (device_class power). Home Assistant has no laundry domain. running-state stays unexported until a null reading can be published as unavailable.";
85894
+ var HOMEKIT_REASON = "HomeKit has no laundry-appliance service. The export tab would pair an accessory with nothing on it: running-state is withheld until a null reading can be unavailable, and power is not a HomeKit service this export honours.";
85895
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
85896
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
85897
+ var MATTER_REASON = "No Matter export is built. The closest device types are Laundry Washer (0x0073) and Laundry Dryer (0x0074); a power threshold is not the Laundry Washer Mode cluster.";
85898
+ function withheld(reason) {
85899
+ return {
85900
+ [DeviceType.Washer]: {
85901
+ exported: false,
85902
+ reason
85903
+ },
85904
+ [DeviceType.Dryer]: {
85905
+ exported: false,
85906
+ reason
85907
+ },
85908
+ [DeviceType.Dishwasher]: {
85909
+ exported: false,
85910
+ reason
85911
+ }
85912
+ };
85913
+ }
85914
+ function offered(received) {
85915
+ return {
85916
+ [DeviceType.Washer]: {
85917
+ exported: true,
85918
+ as: received
85919
+ },
85920
+ [DeviceType.Dryer]: {
85921
+ exported: true,
85922
+ as: received
85923
+ },
85924
+ [DeviceType.Dishwasher]: {
85925
+ exported: true,
85926
+ as: received
85927
+ }
85928
+ };
85929
+ }
85930
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
84884
85931
  Object.freeze({
84885
85932
  "accessories.getStatus": [{
84886
85933
  name: "deviceId",
@@ -85847,6 +86894,21 @@ Object.freeze({
85847
86894
  form: "single",
85848
86895
  optional: false
85849
86896
  }],
86897
+ "groupActive.getStatus": [{
86898
+ name: "deviceId",
86899
+ form: "single",
86900
+ optional: false
86901
+ }],
86902
+ "groupCommands.getStatus": [{
86903
+ name: "deviceId",
86904
+ form: "single",
86905
+ optional: false
86906
+ }],
86907
+ "groupStatus.getStatus": [{
86908
+ name: "deviceId",
86909
+ form: "single",
86910
+ optional: false
86911
+ }],
85850
86912
  "heatingValve.getStatus": [{
85851
86913
  name: "deviceId",
85852
86914
  form: "single",
@@ -86047,6 +87109,11 @@ Object.freeze({
86047
87109
  form: "single",
86048
87110
  optional: false
86049
87111
  }],
87112
+ "meter.getStatus": [{
87113
+ name: "deviceId",
87114
+ form: "single",
87115
+ optional: false
87116
+ }],
86050
87117
  "motion.getStatus": [{
86051
87118
  name: "deviceId",
86052
87119
  form: "single",
@@ -87654,7 +88721,6 @@ Object.freeze({
87654
88721
  "network-access": "ingress",
87655
88722
  "smtp-provider": "email"
87656
88723
  });
87657
- DeviceType.Camera;
87658
88724
  /**
87659
88725
  * A saved Customize assignment (D936). The schema is the type: callers use
87660
88726
  * `z.infer`, they do not restate the shape.
@@ -87831,6 +88897,8 @@ new Map(Object.entries({
87831
88897
  "detection-blind": "device-events",
87832
88898
  "motion-noisy": "device-events",
87833
88899
  "motion-calm": "device-events",
88900
+ "package-llm-silent": "device-events",
88901
+ "package-llm-answered": "device-events",
87834
88902
  "recording-stalled": "device-events",
87835
88903
  "recording-healthy": "device-events",
87836
88904
  "recording-degraded": "device-events",
@@ -87914,6 +88982,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87914
88982
  label: "Motion without a subject",
87915
88983
  kinds: ["motion-noisy", "motion-calm"]
87916
88984
  },
88985
+ {
88986
+ id: "package-llm",
88987
+ family: "device-events",
88988
+ label: "Package confirm",
88989
+ kinds: ["package-llm-silent", "package-llm-answered"]
88990
+ },
87917
88991
  {
87918
88992
  id: "recording",
87919
88993
  family: "device-events",
@@ -88095,6 +89169,8 @@ var DEVICE_EVENTS = [
88095
89169
  "detection-blind",
88096
89170
  "motion-noisy",
88097
89171
  "motion-calm",
89172
+ "package-llm-silent",
89173
+ "package-llm-answered",
88098
89174
  "recording-stalled",
88099
89175
  "recording-healthy",
88100
89176
  "recording-degraded",
@@ -88363,6 +89439,15 @@ new Map([
88363
89439
  snapshotLabel: "Cameras with constant motion now",
88364
89440
  snapshotSection: null
88365
89441
  },
89442
+ {
89443
+ id: "package-llm-confirm",
89444
+ family: "device-events",
89445
+ label: "Package confirm (not answering / answering again)",
89446
+ bad: ["package-llm-silent"],
89447
+ recovery: ["package-llm-answered"],
89448
+ snapshotLabel: "Cameras whose package confirm is not answering",
89449
+ snapshotSection: null
89450
+ },
88366
89451
  {
88367
89452
  id: "recording-health",
88368
89453
  family: "device-events",