@camstack/addon-provider-rademacher 0.2.146 → 0.2.148

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.mjs CHANGED
@@ -983,7 +983,7 @@ var Rademacher = class {
983
983
  }
984
984
  };
985
985
  //#endregion
986
- //#region ../types/dist/event-category-DHXxB8t9.mjs
986
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
987
987
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
988
988
  EventCategory["SystemBoot"] = "system.boot";
989
989
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -1309,6 +1309,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
1309
1309
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
1310
1310
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
1311
1311
  /**
1312
+ * A camera's package LLM confirm has failed closed three times in a row
1313
+ * (D931, 2026-10-26). One edge per camera until
1314
+ * {@link PipelinePackageLlmAnswered}.
1315
+ */
1316
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
1317
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
1318
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
1319
+ /**
1312
1320
  * Per-camera pipeline config was mutated by the orchestrator
1313
1321
  * (a settings change via `setAgentInferenceDevices` /
1314
1322
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -6418,7 +6426,7 @@ var ZodIssueCode = {
6418
6426
  var ZodFirstPartyTypeKind;
6419
6427
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6420
6428
  //#endregion
6421
- //#region ../types/dist/sleep-BErAxhul.mjs
6429
+ //#region ../types/dist/sleep--jXS9hWo.mjs
6422
6430
  /**
6423
6431
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6424
6432
  * window to float samples (D455).
@@ -8972,6 +8980,99 @@ var RecordKindDeclarationSchema = object({
8972
8980
  }).strict();
8973
8981
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8974
8982
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8983
+ var READING_QUANTITIES = [
8984
+ "temperature",
8985
+ "pressure",
8986
+ "speed",
8987
+ "precipitation",
8988
+ "precipitation-rate",
8989
+ "length",
8990
+ "area",
8991
+ "illuminance",
8992
+ "irradiance",
8993
+ "power",
8994
+ "energy",
8995
+ "voltage",
8996
+ "current",
8997
+ "apparent-power",
8998
+ "humidity",
8999
+ "percentage",
9000
+ "concentration-volume",
9001
+ "concentration-mass",
9002
+ "concentration-ppb",
9003
+ "volume",
9004
+ "duration",
9005
+ "duration-ms",
9006
+ "information",
9007
+ "sound-level",
9008
+ "signal-level",
9009
+ "colour-temperature",
9010
+ "angle",
9011
+ "bitrate",
9012
+ "rate",
9013
+ "mass",
9014
+ "index",
9015
+ "ratio",
9016
+ "count",
9017
+ "none",
9018
+ "device"
9019
+ ];
9020
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
9021
+ new Set(READING_QUANTITIES);
9022
+ /**
9023
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
9024
+ * (or an index / ratio / count). `device` means the cap has no single unit.
9025
+ */
9026
+ var QUANTITY_BASE = {
9027
+ temperature: "°C",
9028
+ pressure: "hPa",
9029
+ speed: "m/s",
9030
+ precipitation: "mm",
9031
+ "precipitation-rate": "mm/h",
9032
+ length: "m",
9033
+ area: "m²",
9034
+ illuminance: "lx",
9035
+ irradiance: "W/m²",
9036
+ power: "W",
9037
+ energy: "kWh",
9038
+ voltage: "V",
9039
+ current: "A",
9040
+ "apparent-power": "VA",
9041
+ humidity: "%",
9042
+ percentage: "%",
9043
+ "concentration-volume": "ppm",
9044
+ "concentration-mass": "µg/m³",
9045
+ "concentration-ppb": "ppb",
9046
+ volume: "L",
9047
+ duration: "s",
9048
+ "duration-ms": "ms",
9049
+ information: "B",
9050
+ "sound-level": "dBFS",
9051
+ "signal-level": "dBm",
9052
+ "colour-temperature": "mired",
9053
+ angle: "°",
9054
+ bitrate: "kbps",
9055
+ rate: "fps",
9056
+ mass: "kg",
9057
+ index: "none",
9058
+ ratio: "none",
9059
+ count: "none",
9060
+ none: "none",
9061
+ device: "device"
9062
+ };
9063
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
9064
+ function baseUnitOf(quantity) {
9065
+ const symbol = QUANTITY_BASE[quantity];
9066
+ if (symbol === "none" || symbol === "device") return void 0;
9067
+ return symbol;
9068
+ }
9069
+ function readingMeta(quantity, extras) {
9070
+ return {
9071
+ quantity,
9072
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
9073
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
9074
+ };
9075
+ }
8975
9076
  /** Shared Zod schemas used across streaming capabilities. */
8976
9077
  var CamProfileSchema = _enum([
8977
9078
  "high",
@@ -8999,8 +9100,8 @@ var CamStreamKindSchema = _enum([
8999
9100
  "derived"
9000
9101
  ]);
9001
9102
  var CamStreamResolutionSchema = object({
9002
- width: number().int().positive(),
9003
- height: number().int().positive()
9103
+ width: number().int().positive().meta(readingMeta("none")),
9104
+ height: number().int().positive().meta(readingMeta("none"))
9004
9105
  });
9005
9106
  var CameraStreamSchema = object({
9006
9107
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -9518,6 +9619,9 @@ var CAP_NAMES = Object.freeze([
9518
9619
  "filesystem-browse",
9519
9620
  "flood",
9520
9621
  "gas",
9622
+ "group-active",
9623
+ "group-commands",
9624
+ "group-status",
9521
9625
  "heating-valve",
9522
9626
  "humidifier",
9523
9627
  "humidity-sensor",
@@ -9538,6 +9642,7 @@ var CAP_NAMES = Object.freeze([
9538
9642
  "login-method",
9539
9643
  "media-player",
9540
9644
  "mesh-network",
9645
+ "meter",
9541
9646
  "metrics-provider",
9542
9647
  "model-convert",
9543
9648
  "model-distributor",
@@ -9647,14 +9752,6 @@ Object.freeze({
9647
9752
  {
9648
9753
  "path": "aqi",
9649
9754
  "valueType": "number"
9650
- },
9651
- {
9652
- "path": "unit",
9653
- "valueType": "string"
9654
- },
9655
- {
9656
- "path": "precision",
9657
- "valueType": "number"
9658
9755
  }
9659
9756
  ],
9660
9757
  "alarm-panel": [
@@ -9690,20 +9787,10 @@ Object.freeze({
9690
9787
  "valueType": "boolean"
9691
9788
  }
9692
9789
  ],
9693
- "ambient-light-sensor": [
9694
- {
9695
- "path": "lux",
9696
- "valueType": "number"
9697
- },
9698
- {
9699
- "path": "unit",
9700
- "valueType": "string"
9701
- },
9702
- {
9703
- "path": "precision",
9704
- "valueType": "number"
9705
- }
9706
- ],
9790
+ "ambient-light-sensor": [{
9791
+ "path": "lux",
9792
+ "valueType": "number"
9793
+ }],
9707
9794
  "audio-metrics": [
9708
9795
  {
9709
9796
  "path": "ts",
@@ -9783,6 +9870,10 @@ Object.freeze({
9783
9870
  "path": "lastUpdated",
9784
9871
  "valueType": "number"
9785
9872
  },
9873
+ {
9874
+ "path": "volts",
9875
+ "valueType": "number"
9876
+ },
9786
9877
  {
9787
9878
  "path": "lastContactAt",
9788
9879
  "valueType": "number"
@@ -10025,10 +10116,6 @@ Object.freeze({
10025
10116
  "datetime"
10026
10117
  ]
10027
10118
  },
10028
- {
10029
- "path": "unit",
10030
- "valueType": "string"
10031
- },
10032
10119
  {
10033
10120
  "path": "min",
10034
10121
  "valueType": "number"
@@ -10040,19 +10127,6 @@ Object.freeze({
10040
10127
  {
10041
10128
  "path": "step",
10042
10129
  "valueType": "number"
10043
- },
10044
- {
10045
- "path": "precision",
10046
- "valueType": "number"
10047
- },
10048
- {
10049
- "path": "format",
10050
- "valueType": "enum",
10051
- "enumValues": [
10052
- "date",
10053
- "time",
10054
- "datetime"
10055
- ]
10056
10130
  }
10057
10131
  ],
10058
10132
  "cover": [
@@ -10293,13 +10367,8 @@ Object.freeze({
10293
10367
  "path": "value",
10294
10368
  "valueType": "string"
10295
10369
  }, {
10296
- "path": "format",
10297
- "valueType": "enum",
10298
- "enumValues": [
10299
- "date",
10300
- "time",
10301
- "datetime"
10302
- ]
10370
+ "path": "timestamp",
10371
+ "valueType": "boolean"
10303
10372
  }],
10304
10373
  "event-emitter": [
10305
10374
  {
@@ -10372,6 +10441,24 @@ Object.freeze({
10372
10441
  "path": "detected",
10373
10442
  "valueType": "boolean"
10374
10443
  }],
10444
+ "group-active": [{
10445
+ "path": "active",
10446
+ "valueType": "boolean"
10447
+ }],
10448
+ "group-status": [
10449
+ {
10450
+ "path": "included",
10451
+ "valueType": "number"
10452
+ },
10453
+ {
10454
+ "path": "excluded",
10455
+ "valueType": "number"
10456
+ },
10457
+ {
10458
+ "path": "total",
10459
+ "valueType": "number"
10460
+ }
10461
+ ],
10375
10462
  "heating-valve": [{
10376
10463
  "path": "position",
10377
10464
  "valueType": "number"
@@ -10406,20 +10493,10 @@ Object.freeze({
10406
10493
  "valueType": "number"
10407
10494
  }
10408
10495
  ],
10409
- "humidity-sensor": [
10410
- {
10411
- "path": "percent",
10412
- "valueType": "number"
10413
- },
10414
- {
10415
- "path": "unit",
10416
- "valueType": "string"
10417
- },
10418
- {
10419
- "path": "precision",
10420
- "valueType": "number"
10421
- }
10422
- ],
10496
+ "humidity-sensor": [{
10497
+ "path": "percent",
10498
+ "valueType": "number"
10499
+ }],
10423
10500
  "image": [{
10424
10501
  "path": "url",
10425
10502
  "valueType": "string"
@@ -10627,6 +10704,10 @@ Object.freeze({
10627
10704
  ]
10628
10705
  }
10629
10706
  ],
10707
+ "meter": [{
10708
+ "path": "value",
10709
+ "valueType": "number"
10710
+ }],
10630
10711
  "motion": [
10631
10712
  {
10632
10713
  "path": "detected",
@@ -10758,20 +10839,10 @@ Object.freeze({
10758
10839
  "valueType": "boolean"
10759
10840
  }
10760
10841
  ],
10761
- "numeric-sensor": [
10762
- {
10763
- "path": "value",
10764
- "valueType": "number"
10765
- },
10766
- {
10767
- "path": "unit",
10768
- "valueType": "string"
10769
- },
10770
- {
10771
- "path": "precision",
10772
- "valueType": "number"
10773
- }
10774
- ],
10842
+ "numeric-sensor": [{
10843
+ "path": "value",
10844
+ "valueType": "number"
10845
+ }],
10775
10846
  "pet-feeder": [
10776
10847
  {
10777
10848
  "path": "foodLevel",
@@ -10863,14 +10934,6 @@ Object.freeze({
10863
10934
  {
10864
10935
  "path": "powerFactor",
10865
10936
  "valueType": "number"
10866
- },
10867
- {
10868
- "path": "unit",
10869
- "valueType": "string"
10870
- },
10871
- {
10872
- "path": "precision",
10873
- "valueType": "number"
10874
10937
  }
10875
10938
  ],
10876
10939
  "presence": [
@@ -10899,20 +10962,10 @@ Object.freeze({
10899
10962
  "valueType": "number"
10900
10963
  }
10901
10964
  ],
10902
- "pressure-sensor": [
10903
- {
10904
- "path": "hpa",
10905
- "valueType": "number"
10906
- },
10907
- {
10908
- "path": "unit",
10909
- "valueType": "string"
10910
- },
10911
- {
10912
- "path": "precision",
10913
- "valueType": "number"
10914
- }
10915
- ],
10965
+ "pressure-sensor": [{
10966
+ "path": "hpa",
10967
+ "valueType": "number"
10968
+ }],
10916
10969
  "privacy-mask": [{
10917
10970
  "path": "enabled",
10918
10971
  "valueType": "boolean"
@@ -11223,20 +11276,10 @@ Object.freeze({
11223
11276
  "path": "tampered",
11224
11277
  "valueType": "boolean"
11225
11278
  }],
11226
- "temperature-sensor": [
11227
- {
11228
- "path": "celsius",
11229
- "valueType": "number"
11230
- },
11231
- {
11232
- "path": "unit",
11233
- "valueType": "string"
11234
- },
11235
- {
11236
- "path": "precision",
11237
- "valueType": "number"
11238
- }
11239
- ],
11279
+ "temperature-sensor": [{
11280
+ "path": "celsius",
11281
+ "valueType": "number"
11282
+ }],
11240
11283
  "update": [
11241
11284
  {
11242
11285
  "path": "currentVersion",
@@ -11347,10 +11390,6 @@ Object.freeze({
11347
11390
  "path": "temperature",
11348
11391
  "valueType": "number"
11349
11392
  },
11350
- {
11351
- "path": "temperatureUnit",
11352
- "valueType": "string"
11353
- },
11354
11393
  {
11355
11394
  "path": "humidity",
11356
11395
  "valueType": "number"
@@ -11359,18 +11398,10 @@ Object.freeze({
11359
11398
  "path": "pressure",
11360
11399
  "valueType": "number"
11361
11400
  },
11362
- {
11363
- "path": "pressureUnit",
11364
- "valueType": "string"
11365
- },
11366
11401
  {
11367
11402
  "path": "windSpeed",
11368
11403
  "valueType": "number"
11369
11404
  },
11370
- {
11371
- "path": "windSpeedUnit",
11372
- "valueType": "string"
11373
- },
11374
11405
  {
11375
11406
  "path": "windBearing",
11376
11407
  "valueType": "number"
@@ -11420,7 +11451,7 @@ object({
11420
11451
  params: record(string(), union([string(), number()])).optional()
11421
11452
  });
11422
11453
  //#endregion
11423
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
11454
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
11424
11455
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
11425
11456
  DeviceType["Camera"] = "camera";
11426
11457
  DeviceType["Hub"] = "hub";
@@ -11514,8 +11545,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
11514
11545
  DeviceType["PetFeeder"] = "pet-feeder";
11515
11546
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
11516
11547
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
11517
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
11548
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
11549
+ * A plug-built dishwasher carries running-state and power instead. */
11518
11550
  DeviceType["Dishwasher"] = "dishwasher";
11551
+ /** Washing machine. A plug-built one is running while its power stays
11552
+ * above a threshold. No program cap — that surface is the dishwasher's. */
11553
+ DeviceType["Washer"] = "washer";
11554
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
11555
+ DeviceType["Dryer"] = "dryer";
11519
11556
  return DeviceType;
11520
11557
  }({});
11521
11558
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -11773,7 +11810,7 @@ function errMsg(err) {
11773
11810
  return String(err);
11774
11811
  }
11775
11812
  //#endregion
11776
- //#region ../types/dist/composition-kEl9mevu.mjs
11813
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
11777
11814
  /**
11778
11815
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11779
11816
  *
@@ -12088,6 +12125,13 @@ var table = {
12088
12125
  apply: () => {
12089
12126
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
12090
12127
  }
12128
+ },
12129
+ schmitt: {
12130
+ minArgs: 3,
12131
+ maxArgs: 5,
12132
+ apply: () => {
12133
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
12134
+ }
12091
12135
  }
12092
12136
  };
12093
12137
  Object.freeze(Object.assign(Object.create(null), table));
@@ -12528,15 +12572,232 @@ function validateExpressionSource(src) {
12528
12572
  }
12529
12573
  return null;
12530
12574
  }
12531
- /** A composed device's stableId is this prefix plus the block id. */
12532
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12533
- /** Longest snippet a `code` source may carry (slice 4). */
12534
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
12575
+ /**
12576
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
12577
+ *
12578
+ * Standby of the control board is under 5 W; a motor or a heater is far
12579
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
12580
+ * a cycle, so running starts only after 30 s above the line.
12581
+ *
12582
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
12583
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
12584
+ * report "finished" in the middle of the program, so the stop dwell is
12585
+ * 10 minutes on every type. The number is stored per type so one machine
12586
+ * can move later without moving the others.
12587
+ */
12588
+ var APPLIANCE_TEMPLATE_ID = "appliance";
12589
+ var APPLIANCE_KINDS = [
12590
+ "washer",
12591
+ "dryer",
12592
+ "dishwasher"
12593
+ ];
12594
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
12595
+ /**
12596
+ * Statistics and integrals over a history read (D944).
12597
+ *
12598
+ * Pure: the same points always give the same answer, so a restart recomputes
12599
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
12600
+ * never the mean of the samples. A hole with no held value is not integrated
12601
+ * as zero. A truncated read is not published.
12602
+ */
12603
+ var STATISTIC_STATS = [
12604
+ "min",
12605
+ "max",
12606
+ "mean",
12607
+ "sum",
12608
+ "count",
12609
+ "change"
12610
+ ];
12611
+ /**
12612
+ * The range a statistics or integral helper asks history for (D944).
12613
+ *
12614
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
12615
+ * or 25 hours across a DST change. A tier is eligible only when its retention
12616
+ * is strictly longer than the longest that range can be. Forever qualifies.
12617
+ * Equal to a tier's retention is a refusal, not a short answer.
12618
+ */
12619
+ var MINUTE_MS = 6e4;
12620
+ var HOUR_MS = 60 * MINUTE_MS;
12621
+ var DAY_MS = 24 * HOUR_MS;
12622
+ var ROLLING_SPANS = [
12623
+ "1h",
12624
+ "24h",
12625
+ "2d",
12626
+ "7d",
12627
+ "30d",
12628
+ "custom"
12629
+ ];
12630
+ var CALENDAR_PERIODS = [
12631
+ "today",
12632
+ "yesterday",
12633
+ "this-week",
12634
+ "this-month"
12635
+ ];
12636
+ var RESET_PERIODS = [
12637
+ "never",
12638
+ "daily",
12639
+ "weekly",
12640
+ "monthly"
12641
+ ];
12642
+ 5 * MINUTE_MS;
12643
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
12644
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
12645
+ var rollingWindowObject = object({
12646
+ kind: literal("rolling"),
12647
+ span: _enum(ROLLING_SPANS),
12648
+ customMinutes: number().int().min(5).optional()
12649
+ }).check((payload) => {
12650
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
12651
+ code: "custom",
12652
+ message: "a custom range needs a duration in minutes",
12653
+ path: ["customMinutes"],
12654
+ input: payload.value
12655
+ });
12656
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
12657
+ code: "custom",
12658
+ message: "only a custom range carries a duration",
12659
+ path: ["customMinutes"],
12660
+ input: payload.value
12661
+ });
12662
+ });
12663
+ var RollingWindowSchema = rollingWindowObject;
12664
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
12665
+ kind: literal("calendar"),
12666
+ period: _enum(CALENDAR_PERIODS)
12667
+ })]);
12668
+ /**
12669
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
12670
+ * id. Split out so helper schemas can name it without a cycle through
12671
+ * `composition.ts` (D659).
12672
+ */
12535
12673
  var CompositionSourceRefSchema = object({
12536
12674
  /** The owning addon; `stableId` is unique only within it. */
12537
12675
  addonId: string().min(1),
12538
12676
  stableId: string().min(1)
12539
12677
  });
12678
+ var GROUP_TEMPLATE_IDS = [
12679
+ "temperature",
12680
+ "humidity",
12681
+ "pressure",
12682
+ "illuminance",
12683
+ "power",
12684
+ "energy",
12685
+ "battery",
12686
+ "numeric",
12687
+ "motion",
12688
+ "contact",
12689
+ "leak",
12690
+ "smoke",
12691
+ "occupancy",
12692
+ "binary",
12693
+ "switch",
12694
+ "light",
12695
+ "fan",
12696
+ "cover",
12697
+ "valve",
12698
+ "lock",
12699
+ "media-player",
12700
+ "climate"
12701
+ ];
12702
+ var NUMERIC_AGGREGATES = [
12703
+ "mean",
12704
+ "median",
12705
+ "min",
12706
+ "max",
12707
+ "sum"
12708
+ ];
12709
+ var BINARY_AGGREGATES = ["any", "all"];
12710
+ /** How an actuator group decides it is on. Unknown members never count as off. */
12711
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
12712
+ var AGGREGATE_REDUCERS = [
12713
+ "mean",
12714
+ "median",
12715
+ "min",
12716
+ "max",
12717
+ "sum",
12718
+ "any",
12719
+ "all"
12720
+ ];
12721
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
12722
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
12723
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
12724
+ /** One member. The field is stored so occupancy can name motion or binary. */
12725
+ var HelperMemberSchema = object({
12726
+ source: CompositionSourceRefSchema,
12727
+ cap: string().min(1),
12728
+ fieldPath: string().min(1)
12729
+ });
12730
+ var HelperSpecSchema = discriminatedUnion("kind", [
12731
+ object({
12732
+ kind: literal("group"),
12733
+ templateId: GroupTemplateIdSchema,
12734
+ aggregate: AggregateReducerSchema,
12735
+ members: array(HelperMemberSchema).min(1).max(64),
12736
+ /** Actuator groups only. Readings leave this unset. */
12737
+ active: ActiveCriterionSchema.optional()
12738
+ }),
12739
+ object({
12740
+ kind: literal("appliance"),
12741
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
12742
+ appliance: _enum(APPLIANCE_KINDS),
12743
+ thresholdWatts: number().positive(),
12744
+ startDwellMs: number().int().nonnegative(),
12745
+ stopDwellMs: number().int().nonnegative(),
12746
+ member: HelperMemberSchema
12747
+ }),
12748
+ object({
12749
+ kind: literal("statistic"),
12750
+ stat: _enum(STATISTIC_STATS),
12751
+ window: StatisticWindowSchema,
12752
+ source: HelperMemberSchema
12753
+ }),
12754
+ object({
12755
+ kind: literal("integral"),
12756
+ reset: _enum(RESET_PERIODS),
12757
+ output: _enum(["energy", "meter"]),
12758
+ source: HelperMemberSchema
12759
+ }),
12760
+ object({
12761
+ kind: literal("threshold"),
12762
+ source: HelperMemberSchema,
12763
+ high: number(),
12764
+ low: number(),
12765
+ onForMs: number().int().nonnegative(),
12766
+ offForMs: number().int().nonnegative()
12767
+ }),
12768
+ object({
12769
+ kind: literal("arithmetic"),
12770
+ op: _enum([
12771
+ "sum",
12772
+ "difference",
12773
+ "ratio"
12774
+ ]),
12775
+ inputs: array(HelperMemberSchema).min(2).max(64)
12776
+ }),
12777
+ object({
12778
+ kind: literal("expression"),
12779
+ expr: string().min(1),
12780
+ inputs: array(object({
12781
+ name: string().min(1),
12782
+ member: HelperMemberSchema
12783
+ })).min(1).max(64),
12784
+ output: _enum(["number", "boolean"])
12785
+ }),
12786
+ object({
12787
+ kind: literal("derivative"),
12788
+ window: RollingWindowSchema,
12789
+ per: _enum([
12790
+ "second",
12791
+ "minute",
12792
+ "hour"
12793
+ ]),
12794
+ source: HelperMemberSchema
12795
+ })
12796
+ ]);
12797
+ /** A composed device's stableId is this prefix plus the block id. */
12798
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12799
+ /** Longest snippet a `code` source may carry (slice 4). */
12800
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
12540
12801
  /** One field of one capability of one source device. */
12541
12802
  var CompositionFieldReadSchema = object({
12542
12803
  source: CompositionSourceRefSchema,
@@ -12580,7 +12841,59 @@ var CompositionCodeSourceSchema = object({
12580
12841
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12581
12842
  CompositionFromSourceSchema,
12582
12843
  CompositionExpressionSourceSchema,
12583
- CompositionCodeSourceSchema
12844
+ CompositionCodeSourceSchema,
12845
+ object({
12846
+ kind: literal("aggregate"),
12847
+ reducer: _enum([
12848
+ "mean",
12849
+ "median",
12850
+ "min",
12851
+ "max",
12852
+ "sum",
12853
+ "any",
12854
+ "all"
12855
+ ]),
12856
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12857
+ /**
12858
+ * Set when the reads are an enum state, not a yes/no: the listed states
12859
+ * count as active (a cover is `open`). A state outside the list is inactive;
12860
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
12861
+ */
12862
+ activeWhen: array(string().min(1)).min(1).optional(),
12863
+ /** `value` is the reading; the others are the member counts. */
12864
+ publish: _enum([
12865
+ "value",
12866
+ "included",
12867
+ "excluded",
12868
+ "total"
12869
+ ]),
12870
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12871
+ empty: _enum([
12872
+ "null",
12873
+ "omit",
12874
+ "offline"
12875
+ ])
12876
+ }),
12877
+ object({
12878
+ kind: literal("series"),
12879
+ read: CompositionFieldReadSchema,
12880
+ /** `value` is the statistic or the integral. The others are the gap counts. */
12881
+ publish: _enum([
12882
+ "value",
12883
+ "included",
12884
+ "excluded",
12885
+ "total"
12886
+ ]),
12887
+ /**
12888
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
12889
+ * device unseeded until history answers, so a restart cannot publish 0.
12890
+ */
12891
+ empty: _enum([
12892
+ "null",
12893
+ "omit",
12894
+ "offline"
12895
+ ])
12896
+ })
12584
12897
  ]);
12585
12898
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12586
12899
  kind: literal("forward"),
@@ -12626,7 +12939,13 @@ var CompositionSchema = object({
12626
12939
  kind: literal("existing"),
12627
12940
  device: CompositionSourceRefSchema
12628
12941
  })]),
12629
- features: array(CompositionFeatureSchema).min(1).max(16)
12942
+ features: array(CompositionFeatureSchema).min(1).max(16),
12943
+ /**
12944
+ * Present when the block is a helper template. Absent on a free
12945
+ * composition (the room example, device 4603). The editor reads this,
12946
+ * never the compiled features.
12947
+ */
12948
+ helper: HelperSpecSchema.optional()
12630
12949
  }).superRefine((composition, ctx) => {
12631
12950
  const seen = /* @__PURE__ */ new Set();
12632
12951
  composition.features.forEach((feature, index) => {
@@ -17733,7 +18052,22 @@ var BrokerProviderInfoSchema = object({
17733
18052
  */
17734
18053
  interactiveConnectPath: string().min(1).optional(),
17735
18054
  /** Relative hub path to disconnect the linked account (same popup pattern). */
17736
- interactiveDisconnectPath: string().min(1).optional()
18055
+ interactiveDisconnectPath: string().min(1).optional(),
18056
+ /**
18057
+ * The kind signs in through a server-held interactive session instead of
18058
+ * (or before) typed credentials. `basePath` is a hub-relative route set
18059
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
18060
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
18061
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
18062
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
18063
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
18064
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
18065
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
18066
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
18067
+ * offers it as the default path; the fields the session fills are
18068
+ * marked `filledBySignIn` in the kind's settings schema.
18069
+ */
18070
+ signIn: object({ basePath: string().min(1) }).optional()
17737
18071
  }))
17738
18072
  });
17739
18073
  var ListInputSchema = object({
@@ -18834,7 +19168,12 @@ var CompositionProblemSchema = object({
18834
19168
  "item-field-reserved",
18835
19169
  "target-missing",
18836
19170
  "target-unreadable",
18837
- "not-replaceable"
19171
+ "not-replaceable",
19172
+ "native-capability-replace",
19173
+ "mixed-quantity",
19174
+ "helper-mismatch",
19175
+ "member-constraint",
19176
+ "retention"
18838
19177
  ]),
18839
19178
  path: string(),
18840
19179
  message: string()
@@ -18855,7 +19194,8 @@ var CompositionUnavailableModeSchema = _enum([
18855
19194
  "null",
18856
19195
  "offline",
18857
19196
  "unknown",
18858
- "release"
19197
+ "release",
19198
+ "omit"
18859
19199
  ]);
18860
19200
  /**
18861
19201
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18892,9 +19232,11 @@ var CompositionFieldPlanSchema = object({
18892
19232
  reads: array(CompositionFieldReadSchema)
18893
19233
  });
18894
19234
  /**
18895
- * What a feature does to its target (D663): `new` builds a capability on a new
18896
- * device; on an existing device it `add`s a capability the device lacks, or
18897
- * `replace`s named fields of one it has natively.
19235
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
19236
+ * new device; on an existing device it `add`s a capability the device lacks.
19237
+ * Replacing a capability the device already serves is refused
19238
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
19239
+ * wire for a claim the hub already holds.
18898
19240
  */
18899
19241
  var CompositionFeatureModeSchema = _enum([
18900
19242
  "new",
@@ -19065,8 +19407,392 @@ Object.freeze({
19065
19407
  latchedAt: {
19066
19408
  kind: "fixed",
19067
19409
  result: ["number", "null"]
19410
+ },
19411
+ schmitt: {
19412
+ kind: "fixed",
19413
+ result: ["boolean", "null"]
19068
19414
  }
19069
19415
  });
19416
+ /**
19417
+ * One catalog row per base reading a group helper can build (D944 §3).
19418
+ * Data, not a form default written into the block.
19419
+ */
19420
+ var NOW$2 = {
19421
+ fieldPath: "lastFetchedAt",
19422
+ expr: "now"
19423
+ };
19424
+ var CHANGED = {
19425
+ fieldPath: "lastChangedAt",
19426
+ expr: "now"
19427
+ };
19428
+ function numeric(input) {
19429
+ return {
19430
+ ...input,
19431
+ family: "numeric",
19432
+ deviceType: DeviceType.Sensor,
19433
+ memberDeviceType: DeviceType.Sensor,
19434
+ memberDeviceTypes: [DeviceType.Sensor],
19435
+ activeWhen: void 0,
19436
+ commandCaps: [],
19437
+ baseUnit: baseUnitOf(input.quantity),
19438
+ aggregates: NUMERIC_AGGREGATES,
19439
+ accept: [{
19440
+ cap: input.outputCap,
19441
+ fieldPath: input.outputField
19442
+ }]
19443
+ };
19444
+ }
19445
+ function binary(input) {
19446
+ return {
19447
+ ...input,
19448
+ family: "binary",
19449
+ deviceType: DeviceType.Sensor,
19450
+ memberDeviceType: DeviceType.Sensor,
19451
+ memberDeviceTypes: [DeviceType.Sensor],
19452
+ activeWhen: void 0,
19453
+ commandCaps: [],
19454
+ quantity: "none",
19455
+ baseUnit: void 0,
19456
+ aggregates: BINARY_AGGREGATES,
19457
+ empty: "offline"
19458
+ };
19459
+ }
19460
+ var GROUP_TEMPLATES = [
19461
+ numeric({
19462
+ id: "temperature",
19463
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
19464
+ role: DeviceRole.TemperatureSensor,
19465
+ outputCap: "temperature-sensor",
19466
+ outputField: "celsius",
19467
+ quantity: "temperature",
19468
+ defaultAggregate: "mean",
19469
+ empty: "null",
19470
+ companions: [NOW$2]
19471
+ }),
19472
+ numeric({
19473
+ id: "humidity",
19474
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
19475
+ role: DeviceRole.HumiditySensor,
19476
+ outputCap: "humidity-sensor",
19477
+ outputField: "percent",
19478
+ quantity: "humidity",
19479
+ defaultAggregate: "mean",
19480
+ empty: "null",
19481
+ companions: [NOW$2]
19482
+ }),
19483
+ numeric({
19484
+ id: "pressure",
19485
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
19486
+ role: DeviceRole.PressureSensor,
19487
+ outputCap: "pressure-sensor",
19488
+ outputField: "hpa",
19489
+ quantity: "pressure",
19490
+ defaultAggregate: "mean",
19491
+ empty: "null",
19492
+ companions: [NOW$2]
19493
+ }),
19494
+ numeric({
19495
+ id: "illuminance",
19496
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
19497
+ role: DeviceRole.AmbientLightSensor,
19498
+ outputCap: "ambient-light-sensor",
19499
+ outputField: "lux",
19500
+ quantity: "illuminance",
19501
+ defaultAggregate: "mean",
19502
+ empty: "null",
19503
+ companions: [NOW$2]
19504
+ }),
19505
+ numeric({
19506
+ id: "power",
19507
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
19508
+ role: DeviceRole.PowerSensor,
19509
+ outputCap: "power-meter",
19510
+ outputField: "watts",
19511
+ quantity: "power",
19512
+ defaultAggregate: "sum",
19513
+ empty: "omit",
19514
+ companions: [NOW$2]
19515
+ }),
19516
+ numeric({
19517
+ id: "energy",
19518
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
19519
+ role: DeviceRole.EnergySensor,
19520
+ outputCap: "power-meter",
19521
+ outputField: "kwhTotal",
19522
+ quantity: "energy",
19523
+ defaultAggregate: "sum",
19524
+ empty: "omit",
19525
+ companions: [NOW$2]
19526
+ }),
19527
+ numeric({
19528
+ id: "battery",
19529
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
19530
+ role: DeviceRole.BatterySensor,
19531
+ outputCap: "battery",
19532
+ outputField: "percentage",
19533
+ quantity: "percentage",
19534
+ defaultAggregate: "mean",
19535
+ empty: "null",
19536
+ companions: [
19537
+ {
19538
+ fieldPath: "charging",
19539
+ expr: "\"unknown\""
19540
+ },
19541
+ {
19542
+ fieldPath: "sleeping",
19543
+ expr: "false"
19544
+ },
19545
+ {
19546
+ fieldPath: "lastUpdated",
19547
+ expr: "now"
19548
+ }
19549
+ ]
19550
+ }),
19551
+ numeric({
19552
+ id: "numeric",
19553
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
19554
+ role: DeviceRole.NumericSensor,
19555
+ outputCap: "numeric-sensor",
19556
+ outputField: "value",
19557
+ quantity: "device",
19558
+ defaultAggregate: "mean",
19559
+ empty: "null",
19560
+ companions: [NOW$2]
19561
+ }),
19562
+ binary({
19563
+ id: "motion",
19564
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
19565
+ role: DeviceRole.MotionSensor,
19566
+ outputCap: "motion",
19567
+ outputField: "detected",
19568
+ defaultAggregate: "any",
19569
+ accept: [{
19570
+ cap: "motion",
19571
+ fieldPath: "detected"
19572
+ }],
19573
+ companions: [{
19574
+ fieldPath: "lastDetectedAt",
19575
+ expr: "null"
19576
+ }, {
19577
+ fieldPath: "autoClearAfterMs",
19578
+ expr: "null"
19579
+ }]
19580
+ }),
19581
+ binary({
19582
+ id: "contact",
19583
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
19584
+ role: DeviceRole.ContactSensor,
19585
+ outputCap: "contact",
19586
+ outputField: "entryOpen",
19587
+ defaultAggregate: "any",
19588
+ accept: [{
19589
+ cap: "contact",
19590
+ fieldPath: "entryOpen"
19591
+ }],
19592
+ companions: [CHANGED]
19593
+ }),
19594
+ binary({
19595
+ id: "leak",
19596
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
19597
+ role: DeviceRole.LeakSensor,
19598
+ outputCap: "flood",
19599
+ outputField: "flooded",
19600
+ defaultAggregate: "any",
19601
+ accept: [{
19602
+ cap: "flood",
19603
+ fieldPath: "flooded"
19604
+ }],
19605
+ companions: [CHANGED]
19606
+ }),
19607
+ binary({
19608
+ id: "smoke",
19609
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
19610
+ role: DeviceRole.SmokeSensor,
19611
+ outputCap: "smoke",
19612
+ outputField: "detected",
19613
+ defaultAggregate: "any",
19614
+ accept: [{
19615
+ cap: "smoke",
19616
+ fieldPath: "detected"
19617
+ }],
19618
+ companions: [CHANGED]
19619
+ }),
19620
+ binary({
19621
+ id: "occupancy",
19622
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
19623
+ role: DeviceRole.BinarySensor,
19624
+ outputCap: "binary",
19625
+ outputField: "on",
19626
+ defaultAggregate: "any",
19627
+ accept: [{
19628
+ cap: "motion",
19629
+ fieldPath: "detected"
19630
+ }, {
19631
+ cap: "binary",
19632
+ fieldPath: "on"
19633
+ }],
19634
+ companions: [CHANGED]
19635
+ }),
19636
+ binary({
19637
+ id: "binary",
19638
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
19639
+ role: DeviceRole.BinarySensor,
19640
+ outputCap: "binary",
19641
+ outputField: "on",
19642
+ defaultAggregate: "any",
19643
+ accept: [{
19644
+ cap: "binary",
19645
+ fieldPath: "on"
19646
+ }],
19647
+ companions: [CHANGED]
19648
+ }),
19649
+ actuator({
19650
+ id: "switch",
19651
+ 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.",
19652
+ deviceType: DeviceType.Switch,
19653
+ outputCap: "switch",
19654
+ outputField: "on",
19655
+ member: {
19656
+ cap: "switch",
19657
+ fieldPath: "on"
19658
+ },
19659
+ commandCaps: ["switch"]
19660
+ }),
19661
+ actuator({
19662
+ id: "light",
19663
+ 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.",
19664
+ deviceType: DeviceType.Light,
19665
+ outputCap: "switch",
19666
+ outputField: "on",
19667
+ member: {
19668
+ cap: "switch",
19669
+ fieldPath: "on"
19670
+ },
19671
+ commandCaps: [
19672
+ "switch",
19673
+ "brightness",
19674
+ "color"
19675
+ ]
19676
+ }),
19677
+ actuator({
19678
+ id: "fan",
19679
+ 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.",
19680
+ deviceType: DeviceType.Fan,
19681
+ outputCap: "switch",
19682
+ outputField: "on",
19683
+ member: {
19684
+ cap: "switch",
19685
+ fieldPath: "on"
19686
+ },
19687
+ commandCaps: ["switch", "fan-control"]
19688
+ }),
19689
+ actuator({
19690
+ id: "cover",
19691
+ 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.",
19692
+ deviceType: DeviceType.Cover,
19693
+ outputCap: "group-active",
19694
+ outputField: "active",
19695
+ member: {
19696
+ cap: "cover",
19697
+ fieldPath: "state"
19698
+ },
19699
+ activeWhen: [
19700
+ "open",
19701
+ "opening",
19702
+ "stopped"
19703
+ ],
19704
+ commandCaps: ["cover"]
19705
+ }),
19706
+ actuator({
19707
+ id: "valve",
19708
+ 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.",
19709
+ deviceType: DeviceType.Valve,
19710
+ outputCap: "group-active",
19711
+ outputField: "active",
19712
+ member: {
19713
+ cap: "valve",
19714
+ fieldPath: "state"
19715
+ },
19716
+ activeWhen: [
19717
+ "open",
19718
+ "opening",
19719
+ "stopped"
19720
+ ],
19721
+ commandCaps: ["valve"]
19722
+ }),
19723
+ actuator({
19724
+ id: "lock",
19725
+ 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.",
19726
+ deviceType: DeviceType.Lock,
19727
+ outputCap: "group-active",
19728
+ outputField: "active",
19729
+ member: {
19730
+ cap: "lock-control",
19731
+ fieldPath: "state"
19732
+ },
19733
+ activeWhen: ["locked"],
19734
+ commandCaps: ["lock-control"]
19735
+ }),
19736
+ actuator({
19737
+ id: "media-player",
19738
+ 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.",
19739
+ deviceType: DeviceType.MediaPlayer,
19740
+ outputCap: "group-active",
19741
+ outputField: "active",
19742
+ member: {
19743
+ cap: "media-player",
19744
+ fieldPath: "state"
19745
+ },
19746
+ activeWhen: ["playing", "buffering"],
19747
+ commandCaps: ["media-player"]
19748
+ }),
19749
+ actuator({
19750
+ id: "climate",
19751
+ 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.",
19752
+ deviceType: DeviceType.Thermostat,
19753
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
19754
+ outputCap: "group-active",
19755
+ outputField: "active",
19756
+ member: {
19757
+ cap: "climate-control",
19758
+ fieldPath: "mode"
19759
+ },
19760
+ activeWhen: [
19761
+ "heat",
19762
+ "cool",
19763
+ "auto",
19764
+ "heat_cool",
19765
+ "fan_only",
19766
+ "dry"
19767
+ ],
19768
+ commandCaps: ["climate-control"]
19769
+ })
19770
+ ];
19771
+ function actuator(input) {
19772
+ return {
19773
+ id: input.id,
19774
+ summary: input.summary,
19775
+ deviceType: input.deviceType,
19776
+ outputCap: input.outputCap,
19777
+ outputField: input.outputField,
19778
+ commandCaps: input.commandCaps,
19779
+ family: "actuator",
19780
+ role: void 0,
19781
+ memberDeviceType: input.deviceType,
19782
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
19783
+ activeWhen: input.activeWhen,
19784
+ quantity: "none",
19785
+ baseUnit: void 0,
19786
+ aggregates: BINARY_AGGREGATES,
19787
+ defaultAggregate: "any",
19788
+ empty: "offline",
19789
+ accept: [input.member],
19790
+ companions: [CHANGED]
19791
+ };
19792
+ }
19793
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19794
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19795
+ DeviceType.Camera;
19070
19796
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
19071
19797
  /**
19072
19798
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20893,12 +21619,12 @@ function buildDeviceAdminUrl(parts) {
20893
21619
  * providers detect renames by matching `uniqueId` and patch `id`
20894
21620
  * in-place so dispatch keeps working.
20895
21621
  *
20896
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20897
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20898
- * state push and are the single source of truth. The `device_class` tag is
20899
- * an HA-specific input that is consumed at the discovery boundary
20900
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20901
- * stored after that point.
21622
+ * **Display unit and precision are NOT stored here.** They are a read-time
21623
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
21624
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
21625
+ * neither is repeated on each sample. The `device_class` tag is an
21626
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
21627
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20902
21628
  *
20903
21629
  * The `raw` escape hatch carries provider-specific attributes that don't
20904
21630
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20912,6 +21638,14 @@ var SourceInfoSchema = object({
20912
21638
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20913
21639
  * Used to detect rename when `id` changes. */
20914
21640
  uniqueId: string().optional(),
21641
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
21642
+ quantity: ReadingQuantitySchema.optional(),
21643
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
21644
+ dateTimeShape: _enum([
21645
+ "date",
21646
+ "time",
21647
+ "datetime"
21648
+ ]).optional(),
20915
21649
  /** Provider-specific extras that don't fit the structured slots.
20916
21650
  * Whitelist what goes here per provider — do NOT dump entire HA
20917
21651
  * attribute blobs (would explode the meta JSON column). */
@@ -42016,7 +42750,8 @@ method(object({
42016
42750
  "enum"
42017
42751
  ]),
42018
42752
  enumValues: array(string()).optional(),
42019
- item: boolean().optional()
42753
+ item: boolean().optional(),
42754
+ quantity: ReadingQuantitySchema.optional()
42020
42755
  })).readonly(),
42021
42756
  itemArray: object({
42022
42757
  path: string(),
@@ -44377,7 +45112,7 @@ var NetworkAccessStatusSchema = object({
44377
45112
  });
44378
45113
  /**
44379
45114
  * Optional, richer endpoint shape returned by providers that expose
44380
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
45115
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
44381
45116
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
44382
45117
  * the originating provider config (mode + sourcePort) so the
44383
45118
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -45860,8 +46595,8 @@ var MaskPolygonShapeSchema = object({
45860
46595
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45861
46596
  var MaskGridShapeSchema = object({
45862
46597
  kind: literal("grid"),
45863
- gridWidth: number(),
45864
- gridHeight: number(),
46598
+ gridWidth: number().meta(readingMeta("none")),
46599
+ gridHeight: number().meta(readingMeta("none")),
45865
46600
  cells: array(boolean())
45866
46601
  });
45867
46602
  discriminatedUnion("kind", [
@@ -45973,6 +46708,8 @@ var NcSystemEventKindSchema = _enum([
45973
46708
  "detection-blind",
45974
46709
  "motion-noisy",
45975
46710
  "motion-calm",
46711
+ "package-llm-silent",
46712
+ "package-llm-answered",
45976
46713
  "recording-stalled",
45977
46714
  "recording-healthy",
45978
46715
  "recording-degraded",
@@ -47062,7 +47799,20 @@ var NcRuleInputSchema = object({
47062
47799
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
47063
47800
  * path does not run Zod defaults.
47064
47801
  */
47065
- groupingSec: number().int().min(5).max(60).optional()
47802
+ groupingSec: number().int().min(5).max(60).optional(),
47803
+ /**
47804
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
47805
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
47806
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
47807
+ * same mark the Library writes, and records whose notification it was and
47808
+ * when. A channel with no buttons gets the same two choices as links.
47809
+ *
47810
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
47811
+ * cap path): absent and `false` both mean off, which is what every rule
47812
+ * authored before this field meant. An editor turns it off by writing
47813
+ * `false`, so it needs no `clear` key.
47814
+ */
47815
+ askVerdict: boolean().optional()
47066
47816
  });
47067
47817
  /**
47068
47818
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -57064,6 +57814,28 @@ var PackageVersionInfoSchema = object({
57064
57814
  deprecated: string().optional(),
57065
57815
  distTags: array(string()).readonly()
57066
57816
  });
57817
+ /**
57818
+ * One published document. `ok` is markdown from the registry. `missing` means
57819
+ * the registry answered and the file was not there. `error` is a registry or
57820
+ * tarball failure — never an empty string standing in for either.
57821
+ */
57822
+ var PackageDocBodySchema = discriminatedUnion("status", [
57823
+ object({
57824
+ status: literal("ok"),
57825
+ markdown: string()
57826
+ }),
57827
+ object({ status: literal("missing") }),
57828
+ object({
57829
+ status: literal("error"),
57830
+ message: string()
57831
+ })
57832
+ ]);
57833
+ var PackageDocsSchema = object({
57834
+ name: string(),
57835
+ version: string(),
57836
+ readme: PackageDocBodySchema,
57837
+ changelog: PackageDocBodySchema
57838
+ });
57067
57839
  var SearchResultSchema = object({
57068
57840
  name: string(),
57069
57841
  version: string(),
@@ -57282,7 +58054,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57282
58054
  }), object({ success: literal(true) }), {
57283
58055
  kind: "mutation",
57284
58056
  auth: "admin"
57285
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
58057
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
58058
+ name: string().min(1),
58059
+ version: string().min(1).optional()
58060
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
57286
58061
  kind: "mutation",
57287
58062
  auth: "admin"
57288
58063
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -57341,27 +58116,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57341
58116
  */
57342
58117
  var AirQualitySensorStatusSchema = object({
57343
58118
  /** Carbon dioxide concentration in ppm. */
57344
- co2Ppm: number().min(0).optional(),
58119
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
57345
58120
  /** Total volatile organic compounds in ppb. */
57346
- vocPpb: number().min(0).optional(),
58121
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
57347
58122
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
57348
- pm25: number().min(0).optional(),
58123
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
57349
58124
  /** Particulate matter ≤ 10 μm in µg/m³. */
57350
- pm10: number().min(0).optional(),
58125
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
57351
58126
  /** Composite AQI value (typically 0..500). */
57352
- aqi: number().optional(),
58127
+ aqi: number().optional().meta(readingMeta("index")),
57353
58128
  /** Ms epoch when the slice was last updated. */
57354
- lastFetchedAt: number(),
57355
- /** Live display unit of the single metric this slice carries (e.g. HA
57356
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
57357
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
57358
- * per slice is unambiguous. */
57359
- unit: string().optional(),
57360
- /** Suggested decimal places for numeric display.
57361
- * Populated live from the upstream source when provided (e.g. HA
57362
- * `attributes.suggested_display_precision`). Falls back to
57363
- * auto-formatting when absent. */
57364
- precision: number().int().min(0).max(10).optional()
58129
+ lastFetchedAt: number()
57365
58130
  });
57366
58131
  var airQualitySensorCapability = {
57367
58132
  name: "air-quality-sensor",
@@ -57398,12 +58163,12 @@ var airQualitySensorCapability = {
57398
58163
  },
57399
58164
  pm25: {
57400
58165
  kind: "gauge",
57401
- unit: "ug/m3",
58166
+ unit: "µg/m³",
57402
58167
  deadband: 1
57403
58168
  },
57404
58169
  pm10: {
57405
58170
  kind: "gauge",
57406
- unit: "ug/m3",
58171
+ unit: "µg/m³",
57407
58172
  deadband: 1
57408
58173
  },
57409
58174
  aqi: {
@@ -57421,22 +58186,13 @@ var airQualitySensorCapability = {
57421
58186
  * entries with `device_class: illuminance`.
57422
58187
  */
57423
58188
  var AmbientLightSensorStatusSchema = object({
57424
- /** Current illuminance in lux (lx). */
57425
- lux: number().min(0).meta({
57426
- unit: "lx",
58189
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
58190
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
58191
+ precision: 0,
57427
58192
  label: "Illuminance"
57428
- }),
58193
+ })),
57429
58194
  /** Ms epoch when the slice was last updated. */
57430
- lastFetchedAt: number(),
57431
- /** Live display unit from the upstream source (e.g. HA
57432
- * `attributes.unit_of_measurement`). The UI prefers this over the
57433
- * role's canonical unit. Absent → fall back to the canonical unit. */
57434
- unit: string().optional(),
57435
- /** Suggested decimal places for numeric display.
57436
- * Populated live from the upstream source when provided (e.g. HA
57437
- * `attributes.suggested_display_precision`). Falls back to
57438
- * auto-formatting when absent. */
57439
- precision: number().int().min(0).max(10).optional()
58195
+ lastFetchedAt: number()
57440
58196
  });
57441
58197
  var ambientLightSensorCapability = {
57442
58198
  name: "ambient-light-sensor",
@@ -57468,12 +58224,7 @@ var ambientLightSensorCapability = {
57468
58224
  } } },
57469
58225
  /** Clock fields: written, but excluded from the compare that decides
57470
58226
  * whether persisting is worth a SQLite commit. */
57471
- volatileStateFields: ["lastFetchedAt"],
57472
- customize: {
57473
- version: 1,
57474
- advanced: "derive",
57475
- targets: []
57476
- }
58227
+ volatileStateFields: ["lastFetchedAt"]
57477
58228
  };
57478
58229
  /**
57479
58230
  * Per-class audio metrics aggregated over a sliding window.
@@ -57700,10 +58451,7 @@ var BatteryStatusSchema = object({
57700
58451
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57701
58452
  * declines to band a non-finite reading, which is the correct shape.
57702
58453
  */
57703
- percentage: number().min(0).max(100).nullable().meta({
57704
- unit: "%",
57705
- label: "Charge"
57706
- }),
58454
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57707
58455
  /**
57708
58456
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57709
58457
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57730,7 +58478,14 @@ var BatteryStatusSchema = object({
57730
58478
  */
57731
58479
  sleeping: boolean().meta({ label: "Sleeping" }),
57732
58480
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57733
- lastUpdated: number(),
58481
+ lastUpdated: number().meta(readingMeta("none")),
58482
+ /**
58483
+ * Battery voltage in volts, for a cell the firmware reports as voltage
58484
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
58485
+ * that only report charge. A second reading on this cap, not a second
58486
+ * `numeric-sensor` on the same device.
58487
+ */
58488
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57734
58489
  /**
57735
58490
  * Ms epoch of the last time the device PROVED it was reachable — a
57736
58491
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57777,12 +58532,12 @@ var BATTERY_DEVICE_TYPES = [
57777
58532
  DeviceType.Switch,
57778
58533
  DeviceType.PetFeeder,
57779
58534
  DeviceType.Container,
57780
- DeviceType.Thermostat
58535
+ DeviceType.Thermostat,
58536
+ DeviceType.EventEmitter
57781
58537
  ];
57782
58538
  /** Generic Customize target for the percentage reading (D936). */
57783
58539
  var batteryCustomize = {
57784
58540
  version: 1,
57785
- advanced: "derive",
57786
58541
  targets: [{
57787
58542
  id: "percentage",
57788
58543
  cardinality: "scalar",
@@ -57791,7 +58546,7 @@ var batteryCustomize = {
57791
58546
  stateField: "percentage"
57792
58547
  },
57793
58548
  deviceTypes: BATTERY_DEVICE_TYPES,
57794
- apply: "add-or-replace",
58549
+ apply: "add",
57795
58550
  path: "percentage",
57796
58551
  sources: [{
57797
58552
  kind: "field",
@@ -57912,10 +58667,16 @@ onStatusChanged: { data: object({
57912
58667
  /**
57913
58668
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57914
58669
  */
57915
- history: { fields: { percentage: {
57916
- kind: "gauge",
57917
- unit: "%"
57918
- } } },
58670
+ history: { fields: {
58671
+ percentage: {
58672
+ kind: "gauge",
58673
+ unit: "%"
58674
+ },
58675
+ volts: {
58676
+ kind: "gauge",
58677
+ unit: "V"
58678
+ }
58679
+ } },
57919
58680
  customize: batteryCustomize,
57920
58681
  /** Clock fields: written, but excluded from the compare that decides
57921
58682
  * whether persisting is worth a SQLite commit. */
@@ -57962,12 +58723,7 @@ var binaryCapability = {
57962
58723
  history: { fields: { on: { kind: "boolean" } } },
57963
58724
  /** Clock fields: written, but excluded from the compare that decides
57964
58725
  * whether persisting is worth a SQLite commit. */
57965
- volatileStateFields: ["lastChangedAt"],
57966
- customize: {
57967
- version: 1,
57968
- advanced: "derive",
57969
- targets: []
57970
- }
58726
+ volatileStateFields: ["lastChangedAt"]
57971
58727
  };
57972
58728
  /**
57973
58729
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57983,7 +58739,7 @@ var binaryCapability = {
57983
58739
  */
57984
58740
  var BrightnessStatusSchema = object({
57985
58741
  /** Current level as 0..100 inclusive. Firmware-reported. */
57986
- percentage: number().min(0).max(100),
58742
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57987
58743
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57988
58744
  lastChangedAt: number()
57989
58745
  });
@@ -58437,21 +59193,21 @@ var ClimateControlStatusSchema = object({
58437
59193
  /** Single setpoint in Celsius. Used by single-target modes
58438
59194
  * (heat / cool / dry). Null when the device is in a range mode
58439
59195
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58440
- target: number().nullable(),
59196
+ target: number().nullable().meta(readingMeta("temperature")),
58441
59197
  /** Upper bound of the dual setpoint in Celsius. Populated only
58442
59198
  * in `heat_cool` mode. */
58443
- targetHigh: number().nullable(),
59199
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58444
59200
  /** Lower bound of the dual setpoint in Celsius. Populated only
58445
59201
  * in `heat_cool` mode. */
58446
- targetLow: number().nullable(),
59202
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58447
59203
  /** Target relative humidity (0..100). Null when the device has no
58448
59204
  * humidity control. */
58449
- targetHumidity: number().min(0).max(100).nullable(),
59205
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58450
59206
  /** Current measured temperature in Celsius. Read-only — pushed by
58451
59207
  * the device's internal sensor. */
58452
- currentTemp: number().nullable(),
59208
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58453
59209
  /** Current measured relative humidity (0..100). Read-only. */
58454
- currentHumidity: number().min(0).max(100).nullable(),
59210
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58455
59211
  /** Vertical louver swing on/off. Null when the device has no vertical
58456
59212
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58457
59213
  swingVertical: boolean().nullable(),
@@ -58459,11 +59215,11 @@ var ClimateControlStatusSchema = object({
58459
59215
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58460
59216
  swingHorizontal: boolean().nullable(),
58461
59217
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58462
- minTemp: number().nullable().optional(),
59218
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58463
59219
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58464
- maxTemp: number().nullable().optional(),
59220
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58465
59221
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58466
- tempStep: number().positive().nullable().optional(),
59222
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58467
59223
  /** Ms epoch when the slice was last updated (push or command). */
58468
59224
  lastFetchedAt: number()
58469
59225
  });
@@ -58605,17 +59361,17 @@ var climateControlCapability = {
58605
59361
  * modes to expose in the picker without a slice fetch.
58606
59362
  */
58607
59363
  var RgbTripletSchema = object({
58608
- r: number().int().min(0).max(255),
58609
- g: number().int().min(0).max(255),
58610
- b: number().int().min(0).max(255)
59364
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
59365
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
59366
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58611
59367
  });
58612
59368
  var HsvTripletSchema = object({
58613
59369
  /** Hue in degrees, 0..360. */
58614
- h: number().min(0).max(360),
59370
+ h: number().min(0).max(360).meta(readingMeta("none")),
58615
59371
  /** Saturation as 0..100 inclusive. */
58616
- s: number().min(0).max(100),
59372
+ s: number().min(0).max(100).meta(readingMeta("none")),
58617
59373
  /** Value/brightness as 0..100 inclusive. */
58618
- v: number().min(0).max(100)
59374
+ v: number().min(0).max(100).meta(readingMeta("none"))
58619
59375
  });
58620
59376
  var ColorInputSchema = discriminatedUnion("mode", [
58621
59377
  object({
@@ -58644,7 +59400,7 @@ var ColorStatusSchema = object({
58644
59400
  /** Populated when `mode === 'hsv'`. */
58645
59401
  hsv: HsvTripletSchema.optional(),
58646
59402
  /** Populated when `mode === 'mired'`. */
58647
- mireds: number().int().optional(),
59403
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58648
59404
  /** Ms epoch of the last operator-driven change. */
58649
59405
  lastChangedAt: number()
58650
59406
  });
@@ -58839,18 +59595,12 @@ _enum([
58839
59595
  "empty",
58840
59596
  "full"
58841
59597
  ]);
58842
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58843
- * each. */
58844
- var ConsumableRemainingUnitSchema = _enum([
58845
- "days",
58846
- "hours",
58847
- "cycles"
58848
- ]);
58849
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58850
- * days). `value` null = unknown (D393). */
59598
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
59599
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
59600
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58851
59601
  var ConsumableRemainingSchema = object({
58852
- value: number().min(0).nullable(),
58853
- unit: ConsumableRemainingUnitSchema
59602
+ value: number().min(0).nullable().meta(readingMeta("device")),
59603
+ kind: ConsumableRemainingKindSchema
58854
59604
  });
58855
59605
  /** A single consumable item. Either a continuous `level` (remaining
58856
59606
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58863,7 +59613,7 @@ var ConsumableItemSchema = object({
58863
59613
  /** Display name. */
58864
59614
  label: string().min(1),
58865
59615
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58866
- level: number().min(0).max(100).nullable(),
59616
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58867
59617
  /** The provider's discrete verdict when known (binary mode). */
58868
59618
  status: ConsumableStatusSchema.nullable(),
58869
59619
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58945,8 +59695,8 @@ var consumablesCustomize = {
58945
59695
  transforms: [{ kind: "identity" }]
58946
59696
  }],
58947
59697
  literals: [{
58948
- path: "remaining.unit",
58949
- values: [...ConsumableRemainingUnitSchema.options]
59698
+ path: "remaining.kind",
59699
+ values: [...ConsumableRemainingKindSchema.options]
58950
59700
  }]
58951
59701
  },
58952
59702
  {
@@ -59075,7 +59825,6 @@ reset: method(object({
59075
59825
  },
59076
59826
  "remaining.value": {
59077
59827
  kind: "gauge",
59078
- unitFrom: "remaining.unit",
59079
59828
  deadband: 1,
59080
59829
  displayName: "Remaining"
59081
59830
  }
@@ -59145,10 +59894,9 @@ var contactCapability = {
59145
59894
  * (empty array for the other kinds). It lives in the slice because
59146
59895
  * HA `select.*` entities may update their options at runtime.
59147
59896
  *
59148
- * The `unit`/`min`/`max`/`step` fields are meaningful for
59149
- * `kind === 'numeric'` and are absent (undefined) for all other
59150
- * kinds. They are populated live from HA attributes on each state
59151
- * push — consistent with `options` and the sensor-unit approach.
59897
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59898
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59899
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
59152
59900
  */
59153
59901
  var ControlStatusSchema = object({
59154
59902
  kind: _enum([
@@ -59160,39 +59908,17 @@ var ControlStatusSchema = object({
59160
59908
  /** Current value. Numeric kind → number; select/text/datetime → string.
59161
59909
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
59162
59910
  * or time-only — HA accepts all three forms). */
59163
- value: union([number(), string()]),
59911
+ value: union([number().meta(readingMeta("device")), string()]),
59164
59912
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
59165
59913
  options: array(string()),
59166
59914
  /** Ms epoch when the slice was last updated. */
59167
59915
  lastChangedAt: number(),
59168
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
59169
- * Populated live from HA `attributes.unit_of_measurement` on each push.
59170
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59171
- unit: string().optional(),
59172
- /** Minimum allowed value. Populated live from HA `attributes.min`.
59173
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59174
- min: number().optional(),
59175
- /** Maximum allowed value. Populated live from HA `attributes.max`.
59176
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59177
- max: number().optional(),
59178
- /** Step increment. Populated live from HA `attributes.step`.
59179
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59180
- step: number().optional(),
59181
- /** Suggested decimal places for numeric display. Populated live from HA
59182
- * `attributes.suggested_display_precision`, or derived from `step` when the
59183
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
59184
- * other kinds. Falls back to auto-formatting when absent. */
59185
- precision: number().int().min(0).max(10).optional(),
59186
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
59187
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
59188
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
59189
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
59190
- * `has_date`/`has_time` attributes on each push. */
59191
- format: _enum([
59192
- "date",
59193
- "time",
59194
- "datetime"
59195
- ]).optional()
59916
+ /** Minimum allowed value, in the device quantity's base unit. */
59917
+ min: number().optional().meta(readingMeta("device")),
59918
+ /** Maximum allowed value, in the device quantity's base unit. */
59919
+ max: number().optional().meta(readingMeta("device")),
59920
+ /** Step increment, in the device quantity's base unit. */
59921
+ step: number().optional().meta(readingMeta("device"))
59196
59922
  });
59197
59923
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
59198
59924
  object({
@@ -59258,9 +59984,9 @@ var CoverStatusSchema = object({
59258
59984
  ]),
59259
59985
  /** 0 = fully closed, 100 = fully open. Null when the device has
59260
59986
  * no intermediate position surface. */
59261
- position: number().min(0).max(100).nullable(),
59987
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59262
59988
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
59263
- tiltPosition: number().min(0).max(100).nullable(),
59989
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59264
59990
  /** Ms epoch when the slice was last updated. */
59265
59991
  lastChangedAt: number()
59266
59992
  });
@@ -59363,9 +60089,9 @@ var NormalizedRangeSchema$1 = object({
59363
60089
  var DayNightStatusSchema = object({
59364
60090
  mode: DayNightModeSchema,
59365
60091
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
59366
- sensitivity: number().optional(),
60092
+ sensitivity: number().optional().meta(readingMeta("none")),
59367
60093
  /** Delay before the IR-cut filter flips, in seconds. */
59368
- switchDelaySec: number().optional(),
60094
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
59369
60095
  lastFetchedAt: number()
59370
60096
  });
59371
60097
  /**
@@ -59583,11 +60309,11 @@ var DishwasherStatusSchema = object({
59583
60309
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59584
60310
  availablePrograms: array(string()),
59585
60311
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59586
- remainingSeconds: number().int().nonnegative().nullable(),
60312
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59587
60313
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59588
- delayedStartSeconds: number().int().nonnegative().nullable(),
60314
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59589
60315
  /** Cycle completion 0..100, or null when not applicable. */
59590
- progressPercent: number().min(0).max(100).nullable(),
60316
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59591
60317
  /** True when the door is reported open; null when the door state is not known. */
59592
60318
  doorOpen: boolean().nullable(),
59593
60319
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59611,7 +60337,7 @@ var DishwasherStatusSchema = object({
59611
60337
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59612
60338
  machineCareDue: boolean().nullable(),
59613
60339
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59614
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
60340
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59615
60341
  /** Option states: null when the appliance does not expose the option or has not said. */
59616
60342
  halfLoad: boolean().nullable(),
59617
60343
  hygienePlus: boolean().nullable(),
@@ -59626,7 +60352,7 @@ var DishwasherStatusSchema = object({
59626
60352
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59627
60353
  activeEvents: array(string()),
59628
60354
  /** Numeric error code when the appliance reports one; null otherwise. */
59629
- errorCode: number().nullable(),
60355
+ errorCode: number().nullable().meta(readingMeta("none")),
59630
60356
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59631
60357
  errorLabel: string().nullable(),
59632
60358
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59857,7 +60583,7 @@ var DoorbellStatusSchema = object({
59857
60583
  /** Ms epoch of the last press. null = never observed since this provider started. */
59858
60584
  lastPressedAt: number().nullable(),
59859
60585
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59860
- pressCountSinceStart: number()
60586
+ pressCountSinceStart: number().meta(readingMeta("count"))
59861
60587
  });
59862
60588
  var DoorbellPressEventSchema = object({
59863
60589
  deviceId: number(),
@@ -59922,17 +60648,7 @@ onPressed: {
59922
60648
  history: "none",
59923
60649
  customize: doorbellCustomize
59924
60650
  };
59925
- /**
59926
- * Enum-state sensor — a string value picked from a finite option set.
59927
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59928
- * states, weather conditions, contact-source channels, mode strings).
59929
- * The option set is stable across observations; it is persisted in the
59930
- * device config blob (via the control slice at adoption) and is NOT
59931
- * stored in `sourceInfo`.
59932
- */
59933
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59934
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59935
- var EnumSensorDateTimeFormatSchema = _enum([
60651
+ _enum([
59936
60652
  "date",
59937
60653
  "time",
59938
60654
  "datetime"
@@ -59940,12 +60656,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59940
60656
  var EnumSensorStatusSchema = object({
59941
60657
  value: string(),
59942
60658
  /**
59943
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59944
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59945
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59946
- * a time-only sensor → `'time'`.
60659
+ * Set when `value` is a timestamp rather than an enum state, so history
60660
+ * does not learn ISO strings as states. The display shape (date / time /
60661
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59947
60662
  */
59948
- format: EnumSensorDateTimeFormatSchema.optional(),
60663
+ timestamp: boolean().optional(),
59949
60664
  /** Ms epoch when the slice was last updated. */
59950
60665
  lastFetchedAt: number()
59951
60666
  });
@@ -59972,11 +60687,11 @@ var enumSensorCapability = {
59972
60687
  * History (D850): the value as an `enum` series (an appliance's program or
59973
60688
  * operation state, an HVAC action). The option set is per device, so the
59974
60689
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59975
- * `format` carries a timestamp, not a state, and is not recorded.
60690
+ * `timestamp` carries a clock, not a state, and is not recorded.
59976
60691
  */
59977
60692
  history: { fields: { value: {
59978
60693
  kind: "enum",
59979
- skipWhenSet: "format"
60694
+ skipWhenSet: "timestamp"
59980
60695
  } } },
59981
60696
  /** Clock fields: written, but excluded from the compare that decides
59982
60697
  * whether persisting is worth a SQLite commit. */
@@ -59996,13 +60711,13 @@ var EventFireSchema = object({
59996
60711
  deviceId: number(),
59997
60712
  eventType: string(),
59998
60713
  data: record(string(), unknown()).nullable(),
59999
- timestamp: number(),
60000
- seq: number()
60714
+ timestamp: number().meta(readingMeta("none")),
60715
+ seq: number().meta(readingMeta("count"))
60001
60716
  });
60002
60717
  var EventEmitterStatusSchema = object({
60003
60718
  eventTypes: array(string()),
60004
60719
  lastEvent: EventFireSchema.nullable(),
60005
- eventCountSinceStart: number()
60720
+ eventCountSinceStart: number().meta(readingMeta("count"))
60006
60721
  });
60007
60722
  var eventEmitterCapability = {
60008
60723
  name: "event-emitter",
@@ -60415,10 +61130,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60415
61130
  var FanControlStatusSchema = object({
60416
61131
  /** Active speed as 0..100 inclusive. Null when the device has no
60417
61132
  * speed surface (single-speed fan). */
60418
- percentage: number().min(0).max(100).nullable(),
61133
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60419
61134
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60420
61135
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60421
- percentageStep: number().positive().optional(),
61136
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60422
61137
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60423
61138
  * — empty string when no preset is active or supported. */
60424
61139
  preset: string(),
@@ -60539,7 +61254,7 @@ var FeatureProbeStatusSchema = object({
60539
61254
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60540
61255
  model: string().nullable(),
60541
61256
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60542
- channelCount: number().nullable(),
61257
+ channelCount: number().nullable().meta(readingMeta("count")),
60543
61258
  /**
60544
61259
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60545
61260
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60662,9 +61377,151 @@ var gasCapability = {
60662
61377
  * whether persisting is worth a SQLite commit. */
60663
61378
  volatileStateFields: ["lastChangedAt"]
60664
61379
  };
61380
+ var GroupCommandsSchema = object({
61381
+ /** One row per member that has been commanded. A later command replaces that member's row. */
61382
+ results: array(object({
61383
+ deviceId: number().int().nonnegative(),
61384
+ cap: string().min(1),
61385
+ method: string().min(1),
61386
+ ok: boolean(),
61387
+ error: string().optional(),
61388
+ /** Ms epoch of the attempt. */
61389
+ at: number()
61390
+ })) });
61391
+ var groupCommandsCapability = {
61392
+ name: "group-commands",
61393
+ scope: "device",
61394
+ deviceNative: true,
61395
+ mode: "singleton",
61396
+ deviceTypes: [
61397
+ DeviceType.Switch,
61398
+ DeviceType.Light,
61399
+ DeviceType.Fan,
61400
+ DeviceType.Cover,
61401
+ DeviceType.Valve,
61402
+ DeviceType.Lock,
61403
+ DeviceType.MediaPlayer,
61404
+ DeviceType.Thermostat,
61405
+ DeviceType.Climate
61406
+ ],
61407
+ methods: {},
61408
+ status: {
61409
+ schema: GroupCommandsSchema,
61410
+ kind: "push"
61411
+ },
61412
+ runtimeState: GroupCommandsSchema,
61413
+ /**
61414
+ * Runtime-state durability: **session** — the last attempt. A restored
61415
+ * failure would outlive the command that caused it.
61416
+ *
61417
+ * See `RuntimeStateDurability`. Enforced by
61418
+ * `scripts/check-runtime-state-durability.ts`.
61419
+ */
61420
+ durability: "session",
61421
+ history: "none"
61422
+ };
61423
+ /**
61424
+ * Whether a helper actuator group counts as active (D944): at least one
61425
+ * included member is, or every included member is. What "active" means is the
61426
+ * type's: a cover is open, a lock is locked, a media player is playing, a
61427
+ * climate device is not off. A group of lights, switches and fans reports
61428
+ * `switch.on` instead; this cap is for the types whose own state is not a
61429
+ * yes/no. Unknown members are left out (`group-status` counts them) and an
61430
+ * empty included set takes the device offline, never `active: false`.
61431
+ */
61432
+ var GroupActiveSchema = object({
61433
+ active: boolean().meta({ label: "Active" }),
61434
+ /** Ms epoch when the slice was last updated. */
61435
+ lastChangedAt: number()
61436
+ });
61437
+ var groupActiveCapability = {
61438
+ name: "group-active",
61439
+ scope: "device",
61440
+ deviceNative: true,
61441
+ mode: "singleton",
61442
+ deviceTypes: [
61443
+ DeviceType.Cover,
61444
+ DeviceType.Valve,
61445
+ DeviceType.Lock,
61446
+ DeviceType.MediaPlayer,
61447
+ DeviceType.Climate,
61448
+ DeviceType.Thermostat
61449
+ ],
61450
+ methods: {},
61451
+ status: {
61452
+ schema: GroupActiveSchema,
61453
+ kind: "push"
61454
+ },
61455
+ runtimeState: GroupActiveSchema,
61456
+ /**
61457
+ * Runtime-state durability: **session** — recomputed from the members on the
61458
+ * next source event; a restored value would outlive the members it named.
61459
+ *
61460
+ * See `RuntimeStateDurability`. Enforced by
61461
+ * `scripts/check-runtime-state-durability.ts`.
61462
+ */
61463
+ durability: "session",
61464
+ history: "none"
61465
+ };
61466
+ /**
61467
+ * How many members of a helper group answered (D944). Counts are integers,
61468
+ * not measurements: zero included is a real count, never a stand-in reading.
61469
+ * The reading itself stays on the output capability. History is none — the
61470
+ * series that later statistics read is the output cap's own history block.
61471
+ */
61472
+ var GroupStatusSchema = object({
61473
+ /** Members whose current value entered the aggregate. */
61474
+ included: number().int().nonnegative().meta(readingMeta("count", {
61475
+ precision: 0,
61476
+ label: "Reporting"
61477
+ })),
61478
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
61479
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
61480
+ precision: 0,
61481
+ label: "Excluded"
61482
+ })),
61483
+ /** `included` + `excluded`. */
61484
+ total: number().int().nonnegative().meta(readingMeta("count", {
61485
+ precision: 0,
61486
+ label: "Members"
61487
+ }))
61488
+ });
61489
+ var groupStatusCapability = {
61490
+ name: "group-status",
61491
+ scope: "device",
61492
+ deviceNative: true,
61493
+ mode: "singleton",
61494
+ deviceTypes: [
61495
+ DeviceType.Sensor,
61496
+ DeviceType.Switch,
61497
+ DeviceType.Light,
61498
+ DeviceType.Fan,
61499
+ DeviceType.Cover,
61500
+ DeviceType.Valve,
61501
+ DeviceType.Lock,
61502
+ DeviceType.MediaPlayer,
61503
+ DeviceType.Thermostat,
61504
+ DeviceType.Climate
61505
+ ],
61506
+ methods: {},
61507
+ status: {
61508
+ schema: GroupStatusSchema,
61509
+ kind: "push"
61510
+ },
61511
+ runtimeState: GroupStatusSchema,
61512
+ /**
61513
+ * Runtime-state durability: **session** — recomputed from members on the
61514
+ * next source event; a restored count would outlive the readings it named.
61515
+ *
61516
+ * See `RuntimeStateDurability`. Enforced by
61517
+ * `scripts/check-runtime-state-durability.ts`.
61518
+ */
61519
+ durability: "session",
61520
+ history: "none"
61521
+ };
60665
61522
  var HeatingValveStatusSchema = object({
60666
61523
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60667
- position: number().min(0).max(100).nullable(),
61524
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60668
61525
  /** Ms epoch when the slice was last updated (push or poll). */
60669
61526
  lastFetchedAt: number()
60670
61527
  });
@@ -60707,9 +61564,9 @@ var HumidifierStatusSchema = object({
60707
61564
  /** Whether the humidifier is currently on. */
60708
61565
  on: boolean(),
60709
61566
  /** Current measured relative humidity (0..100). Null when not reported. */
60710
- currentHumidity: number().min(0).max(100).nullable(),
61567
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60711
61568
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60712
- targetHumidity: number().min(0).max(100).nullable(),
61569
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60713
61570
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60714
61571
  * device has no mode surface. */
60715
61572
  mode: string().nullable(),
@@ -60719,9 +61576,9 @@ var HumidifierStatusSchema = object({
60719
61576
  * `idle` / `off`). Null when the device doesn't report it. */
60720
61577
  action: string().nullable(),
60721
61578
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60722
- minHumidity: number().nullable(),
61579
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60723
61580
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60724
- maxHumidity: number().nullable(),
61581
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60725
61582
  /** Ms epoch when the slice was last updated. */
60726
61583
  lastChangedAt: number()
60727
61584
  });
@@ -60791,22 +61648,13 @@ var humidifierCapability = {
60791
61648
  * `sourceInfo`.
60792
61649
  */
60793
61650
  var HumiditySensorStatusSchema = object({
60794
- /** Current relative humidity, 0..100. */
60795
- percent: number().min(0).max(100).meta({
60796
- unit: "%",
61651
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
61652
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
61653
+ precision: 0,
60797
61654
  label: "Humidity"
60798
- }),
61655
+ })),
60799
61656
  /** Ms epoch when the slice was last updated. */
60800
- lastFetchedAt: number(),
60801
- /** Live display unit from the upstream source (e.g. HA
60802
- * `attributes.unit_of_measurement`). The UI prefers this over the
60803
- * role's canonical unit. Absent → fall back to the canonical unit. */
60804
- unit: string().optional(),
60805
- /** Suggested decimal places for numeric display.
60806
- * Populated live from the upstream source when provided (e.g. HA
60807
- * `attributes.suggested_display_precision`). Falls back to
60808
- * auto-formatting when absent. */
60809
- precision: number().int().min(0).max(10).optional()
61657
+ lastFetchedAt: number()
60810
61658
  });
60811
61659
  var humiditySensorCapability = {
60812
61660
  name: "humidity-sensor",
@@ -60837,12 +61685,7 @@ var humiditySensorCapability = {
60837
61685
  } } },
60838
61686
  /** Clock fields: written, but excluded from the compare that decides
60839
61687
  * whether persisting is worth a SQLite commit. */
60840
- volatileStateFields: ["lastFetchedAt"],
60841
- customize: {
60842
- version: 1,
60843
- advanced: "derive",
60844
- targets: []
60845
- }
61688
+ volatileStateFields: ["lastFetchedAt"]
60846
61689
  };
60847
61690
  /**
60848
61691
  * Image display cap. Models a single still image exposed by an integration —
@@ -60863,7 +61706,7 @@ var ImageStatusSchema = object({
60863
61706
  * entity exposes no `entity_picture` (yet). */
60864
61707
  url: string().nullable(),
60865
61708
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60866
- lastUpdated: number().nullable()
61709
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60867
61710
  });
60868
61711
  var imageCapability = {
60869
61712
  name: "image",
@@ -60951,19 +61794,19 @@ var NormalizedRangeSchema = object({
60951
61794
  */
60952
61795
  var ImageSettingsStatusSchema = object({
60953
61796
  /** Normalized 0–100. */
60954
- brightness: number().optional(),
61797
+ brightness: number().optional().meta(readingMeta("none")),
60955
61798
  /** Normalized 0–100. */
60956
- contrast: number().optional(),
61799
+ contrast: number().optional().meta(readingMeta("none")),
60957
61800
  /** Normalized 0–100. */
60958
- saturation: number().optional(),
61801
+ saturation: number().optional().meta(readingMeta("none")),
60959
61802
  /** Normalized 0–100. */
60960
- sharpness: number().optional(),
61803
+ sharpness: number().optional().meta(readingMeta("none")),
60961
61804
  mirror: boolean().optional(),
60962
61805
  flip: boolean().optional(),
60963
61806
  rotate: ImageRotateSchema.optional(),
60964
61807
  whiteBalance: WhiteBalanceModeSchema.optional(),
60965
61808
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60966
- warmth: number().optional(),
61809
+ warmth: number().optional().meta(readingMeta("none")),
60967
61810
  exposureMode: ExposureModeSchema.optional(),
60968
61811
  backlightMode: BacklightModeSchema.optional(),
60969
61812
  lastFetchedAt: number()
@@ -61242,10 +62085,10 @@ var IntercomAbilitySchema = object({
61242
62085
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
61243
62086
  codecs: array(string()),
61244
62087
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
61245
- sampleRate: number(),
62088
+ sampleRate: number().meta(readingMeta("none")),
61246
62089
  duplex: _enum(["half", "full"]),
61247
62090
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
61248
- maxBacklogMs: number()
62091
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
61249
62092
  });
61250
62093
  var IntercomStatusSchema = object({
61251
62094
  /** True while a talk session is open. */
@@ -61472,13 +62315,13 @@ var LawnMowerControlStatusSchema = object({
61472
62315
  activity: LawnMowerActivitySchema,
61473
62316
  /** 0..100 battery percentage. Null when the device has no battery
61474
62317
  * reading. */
61475
- batteryLevel: number().min(0).max(100).nullable(),
62318
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61476
62319
  /** 0..100 mowing-completion percentage of the current task, or null when no
61477
62320
  * task is active / progress is unavailable. */
61478
- progressPercent: number().min(0).max(100).nullable(),
62321
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61479
62322
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61480
62323
  * or null when unknown. */
61481
- currentCode: number().nullable(),
62324
+ currentCode: number().nullable().meta(readingMeta("none")),
61482
62325
  /** Human label for {@link currentCode}, or null when undecodable. */
61483
62326
  currentCodeLabel: string().nullable(),
61484
62327
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61931,7 +62774,14 @@ var lockControlCapability = {
61931
62774
  }),
61932
62775
  unlock: method(object({
61933
62776
  deviceId: number().int().nonnegative(),
61934
- code: string().min(1).optional()
62777
+ code: string().min(1).optional(),
62778
+ /**
62779
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
62780
+ * with one call is refused unless the caller says it was confirmed. A
62781
+ * single lock ignores it. The refusal lives in the callee, so no
62782
+ * automation or API caller reaches the doors by skipping a dialog.
62783
+ */
62784
+ confirm: boolean().optional()
61935
62785
  }), _void(), {
61936
62786
  kind: "mutation",
61937
62787
  auth: "protected"
@@ -61968,6 +62818,44 @@ var lockControlCapability = {
61968
62818
  volatileStateFields: ["lastChangedAt"]
61969
62819
  };
61970
62820
  /**
62821
+ * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
62822
+ * so dashboards keep one series; every other instantaneous reading lands here.
62823
+ * `value` is the time integral in the source quantity's accumulated base
62824
+ * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
62825
+ */
62826
+ var MeterStatusSchema = object({
62827
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
62828
+ lastFetchedAt: number()
62829
+ });
62830
+ var meterCapability = {
62831
+ name: "meter",
62832
+ scope: "device",
62833
+ deviceNative: true,
62834
+ mode: "singleton",
62835
+ deviceTypes: [DeviceType.Sensor],
62836
+ methods: {},
62837
+ status: {
62838
+ schema: MeterStatusSchema,
62839
+ kind: "push"
62840
+ },
62841
+ runtimeState: MeterStatusSchema,
62842
+ /**
62843
+ * Runtime-state durability: **restored** — the total is a cache of the
62844
+ * history integral. A restart shows it only until the history pass
62845
+ * recomputes the open period; it is not an in-memory buffer.
62846
+ *
62847
+ * See `RuntimeStateDurability`. Enforced by
62848
+ * `scripts/check-runtime-state-durability.ts`.
62849
+ */
62850
+ durability: "restored",
62851
+ history: { fields: { value: {
62852
+ kind: "counter",
62853
+ deadband: .001,
62854
+ minIntervalMs: 6e4
62855
+ } } },
62856
+ volatileStateFields: ["lastFetchedAt"]
62857
+ };
62858
+ /**
61971
62859
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
61972
62860
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
61973
62861
  * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
@@ -62011,7 +62899,7 @@ var MediaPlayerStatusSchema = object({
62011
62899
  state: MediaPlayerStateSchema,
62012
62900
  /** Volume as 0..100 inclusive. Null when the device has no volume
62013
62901
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
62014
- volumeLevel: number().min(0).max(100).nullable(),
62902
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62015
62903
  /** Mute toggle distinct from volume=0. Null when no mute surface.
62016
62904
  * Pair with `DeviceFeature.MediaPlayerMute`. */
62017
62905
  isMuted: boolean().nullable(),
@@ -62024,10 +62912,10 @@ var MediaPlayerStatusSchema = object({
62024
62912
  currentMedia: MediaInfoSchema.nullable(),
62025
62913
  /** Current playback position in ms. Null when not seekable or
62026
62914
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
62027
- positionMs: number().int().nonnegative().nullable(),
62915
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62028
62916
  /** Total duration of the current media in ms. Null when unknown
62029
62917
  * (live stream) or nothing is playing. */
62030
- durationMs: number().int().nonnegative().nullable(),
62918
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62031
62919
  /** Shuffle toggle. Null when no shuffle surface.
62032
62920
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
62033
62921
  shuffle: boolean().nullable(),
@@ -62336,7 +63224,7 @@ var motionCustomize = {
62336
63224
  eventName: "onMotionChanged"
62337
63225
  },
62338
63226
  deviceTypes: [DeviceType.Camera],
62339
- apply: "replace",
63227
+ apply: "add",
62340
63228
  applyAuthority: {
62341
63229
  kind: "camera-sensors",
62342
63230
  timeoutField: "motionCooldownMs"
@@ -62408,7 +63296,7 @@ var MotionStatusSchema = object({
62408
63296
  * it, every provider seeded it at boot, and every session left it
62409
63297
  * `detected: true` after its detach.
62410
63298
  */
62411
- autoClearAfterMs: number().nullable()
63299
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62412
63300
  });
62413
63301
  /**
62414
63302
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62445,12 +63333,18 @@ var motionCapability = {
62445
63333
  methods: {
62446
63334
  /**
62447
63335
  * Pull the current motion state synchronously. Convenience shortcut
62448
- * for consumers that don't need the full status object; equivalent
62449
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62450
- * `getStatus` once the auto-injected status surface lands in every
62451
- * consumer.
63336
+ * for consumers that don't need the full status object. A composed
63337
+ * group answers from its slice (D944): unknown throws, it is not off.
62452
63338
  */
62453
63339
  isDetected: method(object({ deviceId: number() }), boolean()) },
63340
+ /**
63341
+ * A composed motion group serves `isDetected` from `detected` (D944).
63342
+ * Without this the cap stays uncomposable (`has-commands`).
63343
+ */
63344
+ composedMethods: { isDetected: {
63345
+ kind: "slice",
63346
+ field: "detected"
63347
+ } },
62454
63348
  events: {
62455
63349
  /**
62456
63350
  * Fires every time the runner transitions a camera between
@@ -62590,7 +63484,7 @@ var MotionZoneRegionSchema = object({
62590
63484
  * the grid region(s). */
62591
63485
  var MotionZoneStatusSchema = object({
62592
63486
  enabled: boolean(),
62593
- sensitivity: number(),
63487
+ sensitivity: number().meta(readingMeta("none")),
62594
63488
  /** Grid region(s). Today exactly one `grid` shape. */
62595
63489
  regions: array(MotionZoneRegionSchema),
62596
63490
  lastFetchedAt: number()
@@ -62960,7 +63854,7 @@ var NavigationStatusSchema = object({
62960
63854
  following: boolean(),
62961
63855
  flash: boolean(),
62962
63856
  lightMode: NavigationLightModeSchema,
62963
- lightLevel: number().min(40).max(100),
63857
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62964
63858
  /** Ms epoch when the slice was last updated. */
62965
63859
  lastChangedAt: number()
62966
63860
  });
@@ -63094,13 +63988,13 @@ var NetworkLinkStatusSchema = object({
63094
63988
  * one whose reading has not landed must not be drawn at 0 %. Consumers
63095
63989
  * SKIP a null rather than coerce it.
63096
63990
  */
63097
- signalPercent: number().min(0).max(100).nullable(),
63991
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63098
63992
  /** Raw received signal strength in dBm, when the firmware reports one. */
63099
- rssiDbm: number().optional(),
63993
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
63100
63994
  /** Network name of a wireless link, when the firmware reports it. */
63101
63995
  ssid: string().optional(),
63102
63996
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
63103
- lastUpdated: number()
63997
+ lastUpdated: number().meta(readingMeta("none"))
63104
63998
  });
63105
63999
  var networkLinkCapability = {
63106
64000
  name: "network-link",
@@ -63485,7 +64379,7 @@ var NotifierStatusSchema = object({
63485
64379
  lastError: string().nullable(),
63486
64380
  /** Number of deliveries currently buffered server-side (rate-limited
63487
64381
  * notifiers may queue). 0 when the provider sends synchronously. */
63488
- queueDepth: number().int().nonnegative(),
64382
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63489
64383
  /** Per-feature capability matrix — authoritative source for the
63490
64384
  * compose-form field gating. */
63491
64385
  supports: NotifierSupportsSchema
@@ -63577,11 +64471,9 @@ onSent: { data: object({
63577
64471
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63578
64472
  * for: water flow, distance, weight, frequency, signal strength, …).
63579
64473
  *
63580
- * The `unit` and `precision` fields carry the live display metadata that
63581
- * the upstream source provides with each state push (HA
63582
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63583
- * The UI reads them directly from the slice — they are the single source
63584
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
64474
+ * The quantity is not on this slice. It is chosen when the device is
64475
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
64476
+ * that quantity's base unit. `null` is a refused sample (D393).
63585
64477
  *
63586
64478
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63587
64479
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63590,15 +64482,8 @@ onSent: { data: object({
63590
64482
  * numeric values land here.
63591
64483
  */
63592
64484
  var NumericSensorStatusSchema = object({
63593
- value: number(),
63594
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63595
- * Populated live from the upstream source on each state push.
63596
- * Absent for unitless sensors. */
63597
- unit: string().optional(),
63598
- /** Suggested decimal places for numeric display.
63599
- * Populated live from the upstream source when provided.
63600
- * Falls back to auto-formatting when absent. */
63601
- precision: number().int().min(0).max(10).optional(),
64485
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
64486
+ value: number().nullable().meta(readingMeta("device")),
63602
64487
  /** Ms epoch when the slice was last updated. */
63603
64488
  lastFetchedAt: number()
63604
64489
  });
@@ -63622,11 +64507,11 @@ var numericSensorCapability = {
63622
64507
  */
63623
64508
  durability: "restored",
63624
64509
  /**
63625
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
64510
+ * History (D850): recorded series. The unit is the device quantity's base,
64511
+ * resolved when the series is read — the cap has no single unit.
63626
64512
  */
63627
64513
  history: { fields: { value: {
63628
64514
  kind: "gauge",
63629
- unitFrom: "unit",
63630
64515
  minIntervalMs: 3e4
63631
64516
  } } },
63632
64517
  /** Clock fields: written, but excluded from the compare that decides
@@ -63918,23 +64803,26 @@ var PetFeederStatusSchema = object({
63918
64803
  /** Food currently in the bowl (grams). Null when the device has not
63919
64804
  * reported a reading yet. On dual-hopper models this is the combined
63920
64805
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63921
- foodLevel: number().nullable(),
64806
+ foodLevel: number().nullable().meta(readingMeta("none")),
63922
64807
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63923
64808
  * single-hopper models. */
63924
- food1: number().nullable(),
64809
+ food1: number().nullable().meta(readingMeta("none")),
63925
64810
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63926
64811
  * single-hopper models. */
63927
- food2: number().nullable(),
64812
+ food2: number().nullable().meta(readingMeta("none")),
63928
64813
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63929
64814
  * (`device_class: problem`, on = low). True when the bowl is empty /
63930
64815
  * below the feeder's low threshold. */
63931
64816
  lowFood: boolean(),
63932
64817
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63933
64818
  * device has no battery reading. */
63934
- batteryPower: number().min(0).max(100).nullable(),
63935
- /** Days of desiccant life remaining. Null when the model has no
63936
- * desiccant sensor. */
63937
- desiccantLeftDays: number().nullable(),
64819
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64820
+ /**
64821
+ * Desiccant life remaining, stored in seconds (duration base, D944).
64822
+ * Providers convert from the days the feeder reports. Null when the
64823
+ * model has no desiccant sensor.
64824
+ */
64825
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63938
64826
  /** True while a feed is in progress. */
63939
64827
  feeding: boolean(),
63940
64828
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63945,7 +64833,7 @@ var PetFeederStatusSchema = object({
63945
64833
  * with `errorCode` for consumers that want the raw integer. */
63946
64834
  error: string().nullable(),
63947
64835
  /** Raw device error code (0 / null = no error). */
63948
- errorCode: number().nullable(),
64836
+ errorCode: number().nullable().meta(readingMeta("none")),
63949
64837
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63950
64838
  isDualHopper: boolean(),
63951
64839
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63955,7 +64843,7 @@ var PetFeederStatusSchema = object({
63955
64843
  /** Play a chime when dispensing. */
63956
64844
  feedSound: boolean(),
63957
64845
  /** Speaker / prompt volume level (device-scaled integer). */
63958
- volume: number(),
64846
+ volume: number().meta(readingMeta("none")),
63959
64847
  /** Ms epoch when the slice was last refreshed from the cloud. */
63960
64848
  lastFetchedAt: number()
63961
64849
  });
@@ -64074,7 +64962,7 @@ var petFeederCapability = {
64074
64962
  },
64075
64963
  desiccantLeftDays: {
64076
64964
  kind: "gauge",
64077
- unit: "days"
64965
+ unit: "s"
64078
64966
  }
64079
64967
  } }
64080
64968
  };
@@ -64475,43 +65363,88 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64475
65363
  */
64476
65364
  var PowerMeterStatusSchema = object({
64477
65365
  /** Instantaneous power draw in watts. */
64478
- watts: number().optional().meta({
64479
- unit: "W",
64480
- label: "Power"
64481
- }),
65366
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64482
65367
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64483
- kwhTotal: number().optional().meta({
64484
- unit: "kWh",
64485
- label: "Energy"
64486
- }),
65368
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64487
65369
  /** Voltage in volts. */
64488
- volts: number().optional(),
65370
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
64489
65371
  /** Current in amperes. */
64490
- amps: number().optional(),
64491
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64492
- va: number().optional(),
65372
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
65373
+ /** Apparent power in volt-amperes. */
65374
+ va: number().optional().meta(readingMeta("apparent-power")),
64493
65375
  /** Power factor (0..1) if reported. */
64494
- powerFactor: number().min(0).max(1).optional(),
65376
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64495
65377
  /** Ms epoch when the slice was last updated. */
64496
- lastFetchedAt: number(),
64497
- /** Live display unit of the single metric this slice carries (e.g. HA
64498
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64499
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64500
- * unit per slice is unambiguous. The UI prefers this over the role's
64501
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64502
- unit: string().optional(),
64503
- /** Suggested decimal places for numeric display.
64504
- * Populated live from the upstream source when provided (e.g. HA
64505
- * `attributes.suggested_display_precision`). Falls back to
64506
- * auto-formatting when absent. */
64507
- precision: number().int().min(0).max(10).optional()
65378
+ lastFetchedAt: number()
64508
65379
  });
65380
+ /**
65381
+ * Loads that draw metered mains power (D951). The reading is a feature of the
65382
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
65383
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
65384
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
65385
+ * camera and the like are left out: their draw is not what anyone meters.
65386
+ */
65387
+ var POWER_METER_DEVICE_TYPES = [
65388
+ DeviceType.Sensor,
65389
+ DeviceType.Switch,
65390
+ DeviceType.Light,
65391
+ DeviceType.Fan,
65392
+ DeviceType.Climate,
65393
+ DeviceType.Thermostat,
65394
+ DeviceType.Humidifier,
65395
+ DeviceType.WaterHeater,
65396
+ DeviceType.MediaPlayer,
65397
+ DeviceType.Washer,
65398
+ DeviceType.Dryer,
65399
+ DeviceType.Dishwasher,
65400
+ DeviceType.Vacuum,
65401
+ DeviceType.LawnMower,
65402
+ DeviceType.PetFeeder
65403
+ ];
65404
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
65405
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
65406
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
65407
+ function powerTarget(id, unit) {
65408
+ return {
65409
+ id,
65410
+ cardinality: "scalar",
65411
+ vocabulary: {
65412
+ cap: "power-meter",
65413
+ stateField: id
65414
+ },
65415
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
65416
+ apply: "add",
65417
+ path: id,
65418
+ sources: [{
65419
+ kind: "numeric",
65420
+ requiredUnit: unit,
65421
+ transforms: [{ kind: "identity" }]
65422
+ }],
65423
+ companions: [{
65424
+ kind: "clock",
65425
+ path: "lastFetchedAt",
65426
+ when: "always"
65427
+ }],
65428
+ sourceReport: { status: "assignment-status" },
65429
+ advancedExpression: true
65430
+ };
65431
+ }
65432
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
65433
+ var powerMeterCustomize = {
65434
+ version: 1,
65435
+ targets: [
65436
+ powerTarget("watts", "W"),
65437
+ powerTarget("kwhTotal", "kWh"),
65438
+ powerTarget("volts", "V"),
65439
+ powerTarget("amps", "A")
65440
+ ]
65441
+ };
64509
65442
  var powerMeterCapability = {
64510
65443
  name: "power-meter",
64511
65444
  scope: "device",
64512
65445
  deviceNative: true,
64513
65446
  mode: "singleton",
64514
- deviceTypes: [DeviceType.Sensor],
65447
+ deviceTypes: POWER_METER_DEVICE_TYPES,
64515
65448
  methods: {},
64516
65449
  status: {
64517
65450
  schema: PowerMeterStatusSchema,
@@ -64569,11 +65502,7 @@ var powerMeterCapability = {
64569
65502
  /** Clock fields: written, but excluded from the compare that decides
64570
65503
  * whether persisting is worth a SQLite commit. */
64571
65504
  volatileStateFields: ["lastFetchedAt"],
64572
- customize: {
64573
- version: 1,
64574
- advanced: "derive",
64575
- targets: []
64576
- }
65505
+ customize: powerMeterCustomize
64577
65506
  };
64578
65507
  /**
64579
65508
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64593,11 +65522,11 @@ var powerMeterCapability = {
64593
65522
  */
64594
65523
  var GpsLocationSchema = object({
64595
65524
  /** Latitude in decimal degrees, -90..90. */
64596
- latitude: number().min(-90).max(90),
65525
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64597
65526
  /** Longitude in decimal degrees, -180..180. */
64598
- longitude: number().min(-180).max(180),
65527
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64599
65528
  /** Reported accuracy in meters (lower = better). */
64600
- accuracyMeters: number().nonnegative()
65529
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64601
65530
  });
64602
65531
  var PresenceStatusSchema = object({
64603
65532
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64609,7 +65538,7 @@ var PresenceStatusSchema = object({
64609
65538
  gps: GpsLocationSchema.nullable(),
64610
65539
  /** Optional battery level of the tracking device (0..100). Null
64611
65540
  * when not reported. */
64612
- batteryPercent: number().min(0).max(100).nullable(),
65541
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64613
65542
  /** Ms epoch when the slice was last updated. */
64614
65543
  lastChangedAt: number()
64615
65544
  });
@@ -64659,19 +65588,10 @@ var presenceCapability = {
64659
65588
  * `sourceInfo`.
64660
65589
  */
64661
65590
  var PressureSensorStatusSchema = object({
64662
- /** Current pressure in hPa. */
64663
- hpa: number(),
65591
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
65592
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64664
65593
  /** Ms epoch when the slice was last updated. */
64665
- lastFetchedAt: number(),
64666
- /** Live display unit from the upstream source (e.g. HA
64667
- * `attributes.unit_of_measurement`). The UI prefers this over the
64668
- * role's canonical unit. Absent → fall back to the canonical unit. */
64669
- unit: string().optional(),
64670
- /** Suggested decimal places for numeric display.
64671
- * Populated live from the upstream source when provided (e.g. HA
64672
- * `attributes.suggested_display_precision`). Falls back to
64673
- * auto-formatting when absent. */
64674
- precision: number().int().min(0).max(10).optional()
65594
+ lastFetchedAt: number()
64675
65595
  });
64676
65596
  var pressureSensorCapability = {
64677
65597
  name: "pressure-sensor",
@@ -65085,8 +66005,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
65085
66005
  }), _void(), { kind: "mutation" });
65086
66006
  var PtzAutotrackSettingsSchema = object({
65087
66007
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
65088
- stopDelaySeconds: number().int().min(0).max(300),
65089
- disappearDelaySeconds: number().int().min(0).max(300)
66008
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
66009
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
65090
66010
  });
65091
66011
  /**
65092
66012
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65240,7 +66160,7 @@ var RunningStateStatusSchema = object({
65240
66160
  /** null = unknown — never coerced to false (D393). */
65241
66161
  running: boolean().nullable(),
65242
66162
  /** Epoch ms the current boolean began; null while `running` is null. */
65243
- since: number().nullable(),
66163
+ since: number().nullable().meta(readingMeta("none")),
65244
66164
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65245
66165
  phase: string().nullable(),
65246
66166
  /** null until a reading has been published (D393). */
@@ -65255,7 +66175,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65255
66175
  /** Generic Customize target for the running boolean (D936). */
65256
66176
  var runningCustomize = {
65257
66177
  version: 1,
65258
- advanced: "derive",
65259
66178
  targets: [{
65260
66179
  id: "running",
65261
66180
  cardinality: "scalar",
@@ -65264,6 +66183,8 @@ var runningCustomize = {
65264
66183
  stateField: "running"
65265
66184
  },
65266
66185
  deviceTypes: [
66186
+ DeviceType.Washer,
66187
+ DeviceType.Dryer,
65267
66188
  DeviceType.Dishwasher,
65268
66189
  DeviceType.Vacuum,
65269
66190
  DeviceType.LawnMower,
@@ -65275,7 +66196,7 @@ var runningCustomize = {
65275
66196
  DeviceType.Thermostat,
65276
66197
  DeviceType.PetFeeder
65277
66198
  ],
65278
- apply: "add-or-replace",
66199
+ apply: "add",
65279
66200
  path: "running",
65280
66201
  sources: [
65281
66202
  {
@@ -66539,9 +67460,9 @@ var RecordTriggerSchema = _enum([
66539
67460
  */
66540
67461
  var RecordWindowSchema = object({
66541
67462
  trigger: RecordTriggerSchema,
66542
- day: number().int().min(0).max(6),
66543
- startMinute: number().int().min(0).max(1439),
66544
- endMinute: number().int().min(1).max(1440)
67463
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
67464
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
67465
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
66545
67466
  });
66546
67467
  /** Status of one physical volume, as the camera itself describes it. */
66547
67468
  var OnboardStorageVolumeSchema = object({
@@ -66617,10 +67538,10 @@ var RecordingOnboardStatusSchema = object({
66617
67538
  /** Master "record to the card at all" switch. */
66618
67539
  enabled: boolean().nullable(),
66619
67540
  overwriteWhenFull: boolean().nullable(),
66620
- preRecordSec: number().nullable(),
66621
- postRecordSec: number().nullable(),
67541
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
67542
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66622
67543
  /** Length of one recorded file, in minutes. */
66623
- segmentMinutes: number().nullable(),
67544
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66624
67545
  /** The primary track's weekly windows, flattened. */
66625
67546
  windows: array(RecordWindowSchema),
66626
67547
  /**
@@ -66636,7 +67557,7 @@ var RecordingOnboardStatusSchema = object({
66636
67557
  * provider reporting a non-zero count also reports the schedule as not
66637
67558
  * writable.
66638
67559
  */
66639
- unreadableWindows: number(),
67560
+ unreadableWindows: number().meta(readingMeta("count")),
66640
67561
  /**
66641
67562
  * The camera is scheduled to record and has NO usable storage.
66642
67563
  *
@@ -67481,7 +68402,7 @@ var CamStreamDescriptorSchema = object({
67481
68402
  url: string().optional(),
67482
68403
  codec: string().optional(),
67483
68404
  resolution: CamStreamResolutionSchema.optional(),
67484
- fps: number().positive().optional(),
68405
+ fps: number().positive().optional().meta(readingMeta("rate")),
67485
68406
  label: string().optional(),
67486
68407
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
67487
68408
  deviceFeatures: array(string()).optional(),
@@ -67568,18 +68489,18 @@ var StreamProfileSchema = _enum([
67568
68489
  ]);
67569
68490
  /** The current encoding config of one stream profile. */
67570
68491
  var StreamProfileConfigSchema = object({
67571
- width: number(),
67572
- height: number(),
68492
+ width: number().meta(readingMeta("none")),
68493
+ height: number().meta(readingMeta("none")),
67573
68494
  codec: _enum(["h264", "h265"]),
67574
- framerate: number(),
67575
- bitrate: number(),
68495
+ framerate: number().meta(readingMeta("rate")),
68496
+ bitrate: number().meta(readingMeta("bitrate")),
67576
68497
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
67577
68498
  encoderProfile: _enum([
67578
68499
  "high",
67579
68500
  "main",
67580
68501
  "baseline"
67581
68502
  ]).optional(),
67582
- gop: number().optional(),
68503
+ gop: number().optional().meta(readingMeta("none")),
67583
68504
  /**
67584
68505
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
67585
68506
  *
@@ -67753,7 +68674,8 @@ var switchCapability = {
67753
68674
  deviceTypes: [
67754
68675
  DeviceType.Switch,
67755
68676
  DeviceType.Siren,
67756
- DeviceType.Light
68677
+ DeviceType.Light,
68678
+ DeviceType.Fan
67757
68679
  ],
67758
68680
  methods: { setState: method(object({
67759
68681
  deviceId: number(),
@@ -67800,12 +68722,7 @@ var switchCapability = {
67800
68722
  label: "On",
67801
68723
  kind: "boolean"
67802
68724
  }
67803
- }] },
67804
- customize: {
67805
- version: 1,
67806
- advanced: "derive",
67807
- targets: []
67808
- }
68725
+ }] }
67809
68726
  };
67810
68727
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67811
68728
  function hasControlCharacter(value) {
@@ -68388,35 +69305,21 @@ var tamperCapability = {
68388
69305
  * entries with `device_class: temperature` and any future native
68389
69306
  * thermometer.
68390
69307
  *
68391
- * Unit normalisation: providers convert to Celsius before storing.
68392
- * The slice value is always Celsius so cross-cap aggregators
68393
- * (climate-control's `currentTemp`, energy analytics) can compose
68394
- * without per-source unit fixups. The canonical display unit (`°C`) is
68395
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68396
- * `sourceInfo`.
69308
+ * The number is Celsius (D944). Providers convert on ingest. A failed
69309
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
68397
69310
  *
68398
69311
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
68399
69312
  * frozen feed (provider hung) distinct from a "temperature hasn't
68400
69313
  * changed" steady state.
68401
69314
  */
68402
69315
  var TemperatureSensorStatusSchema = object({
68403
- /** Current temperature in Celsius. */
68404
- celsius: number().meta({
68405
- unit: "°C",
69316
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
69317
+ celsius: number().nullable().meta(readingMeta("temperature", {
69318
+ precision: 1,
68406
69319
  label: "Temperature"
68407
- }),
69320
+ })),
68408
69321
  /** Ms epoch when the slice was last updated (push or poll). */
68409
- lastFetchedAt: number(),
68410
- /** Live display unit from the upstream source (e.g. HA
68411
- * `attributes.unit_of_measurement`). The UI prefers this over the
68412
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
68413
- * Absent → the UI falls back to the role's canonical unit. */
68414
- unit: string().optional(),
68415
- /** Suggested decimal places for numeric display.
68416
- * Populated live from the upstream source when provided (e.g. HA
68417
- * `attributes.suggested_display_precision`). Falls back to
68418
- * auto-formatting when absent. */
68419
- precision: number().int().min(0).max(10).optional()
69322
+ lastFetchedAt: number()
68420
69323
  });
68421
69324
  var temperatureSensorCapability = {
68422
69325
  name: "temperature-sensor",
@@ -68447,12 +69350,7 @@ var temperatureSensorCapability = {
68447
69350
  } } },
68448
69351
  /** Clock fields: written, but excluded from the compare that decides
68449
69352
  * whether persisting is worth a SQLite commit. */
68450
- volatileStateFields: ["lastFetchedAt"],
68451
- customize: {
68452
- version: 1,
68453
- advanced: "derive",
68454
- targets: []
68455
- }
69353
+ volatileStateFields: ["lastFetchedAt"]
68456
69354
  };
68457
69355
  /**
68458
69356
  * toast — system-scoped singleton capability that streams toast
@@ -68735,16 +69633,16 @@ var VacuumControlStatusSchema = object({
68735
69633
  ]),
68736
69634
  /** 0..100 battery percentage. Null when the device has no battery
68737
69635
  * reading. */
68738
- batteryLevel: number().min(0).max(100).nullable(),
69636
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68739
69637
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68740
69638
  * the vacuum has no speed control. */
68741
69639
  fanSpeed: string().nullable(),
68742
69640
  /** Speed tokens the hardware accepts — drives the UI selector. */
68743
69641
  availableFanSpeeds: array(string()),
68744
69642
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68745
- progressPercent: number().min(0).max(100).nullable(),
69643
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68746
69644
  /** Current error code (0 / null = no error). */
68747
- errorCode: number().nullable(),
69645
+ errorCode: number().nullable().meta(readingMeta("none")),
68748
69646
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68749
69647
  errorLabel: string().nullable(),
68750
69648
  /** Ms epoch when the slice was last updated. */
@@ -68822,7 +69720,7 @@ var ValveStatusSchema = object({
68822
69720
  ]),
68823
69721
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68824
69722
  * intermediate position surface. */
68825
- position: number().min(0).max(100).nullable(),
69723
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68826
69724
  /** Ms epoch when the slice was last updated. */
68827
69725
  lastChangedAt: number()
68828
69726
  });
@@ -68930,9 +69828,9 @@ var vibrationCapability = {
68930
69828
  */
68931
69829
  var WaterHeaterStatusSchema = object({
68932
69830
  /** Current measured temperature. Null when not reported. */
68933
- currentTemp: number().nullable(),
69831
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68934
69832
  /** Target temperature setpoint. Null when no setpoint surface. */
68935
- targetTemp: number().nullable(),
69833
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68936
69834
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68937
69835
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68938
69836
  * device reports an unknown state. */
@@ -68943,9 +69841,9 @@ var WaterHeaterStatusSchema = object({
68943
69841
  * has no away surface. */
68944
69842
  away: boolean().nullable(),
68945
69843
  /** HA `min_temp` attribute. Null when not reported. */
68946
- minTemp: number().nullable(),
69844
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68947
69845
  /** HA `max_temp` attribute. Null when not reported. */
68948
- maxTemp: number().nullable(),
69846
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68949
69847
  /** Ms epoch when the slice was last updated. */
68950
69848
  lastChangedAt: number()
68951
69849
  });
@@ -69030,25 +69928,18 @@ var waterHeaterCapability = {
69030
69928
  * Forecast deferred — current conditions only for v1.
69031
69929
  */
69032
69930
  var WeatherStatusSchema = object({
69033
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69034
- * when no condition has been reported yet. */
69931
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69035
69932
  condition: string().nullable(),
69036
- /** Current temperature in the reported unit. Null when not provided. */
69037
- temperature: number().nullable(),
69038
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69039
- temperatureUnit: string().nullable(),
69933
+ /** Current temperature in Celsius. Null when not provided or refused. */
69934
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69040
69935
  /** Relative humidity (0..100). Null when not provided. */
69041
- humidity: number().min(0).max(100).nullable(),
69042
- /** Barometric pressure in the reported unit. Null when not provided. */
69043
- pressure: number().nullable(),
69044
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69045
- pressureUnit: string().nullable(),
69046
- /** Wind speed in the reported unit. Null when not provided. */
69047
- windSpeed: number().nullable(),
69048
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69049
- windSpeedUnit: string().nullable(),
69936
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
69937
+ /** Barometric pressure in hPa. Null when not provided or refused. */
69938
+ pressure: number().nullable().meta(readingMeta("pressure")),
69939
+ /** Wind speed in metres per second. Null when not provided or refused. */
69940
+ windSpeed: number().nullable().meta(readingMeta("speed")),
69050
69941
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69051
- windBearing: number().nullable(),
69942
+ windBearing: number().nullable().meta(readingMeta("angle")),
69052
69943
  /** Ms epoch when the slice was last updated. */
69053
69944
  lastFetchedAt: number()
69054
69945
  });
@@ -69076,12 +69967,12 @@ var weatherCapability = {
69076
69967
  */
69077
69968
  durability: "session",
69078
69969
  /**
69079
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69970
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69080
69971
  */
69081
69972
  history: { fields: {
69082
69973
  temperature: {
69083
69974
  kind: "gauge",
69084
- unitFrom: "temperatureUnit",
69975
+ unit: "°C",
69085
69976
  deadband: .2
69086
69977
  },
69087
69978
  humidity: {
@@ -69091,12 +69982,12 @@ var weatherCapability = {
69091
69982
  },
69092
69983
  pressure: {
69093
69984
  kind: "gauge",
69094
- unitFrom: "pressureUnit",
69985
+ unit: "hPa",
69095
69986
  deadband: .5
69096
69987
  },
69097
69988
  windSpeed: {
69098
69989
  kind: "gauge",
69099
- unitFrom: "windSpeedUnit",
69990
+ unit: "m/s",
69100
69991
  deadband: .5
69101
69992
  }
69102
69993
  } }
@@ -69997,8 +70888,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
69997
70888
  kind: "mutation",
69998
70889
  auth: "admin"
69999
70890
  });
70891
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
70000
70892
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
70001
- 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;
70893
+ 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;
70002
70894
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
70003
70895
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
70004
70896
  new Set(CAP_NAMES);
@@ -70327,6 +71219,9 @@ var DEVICE_LOCAL_STATE_CAPS = {
70327
71219
  featureProbe: featureProbeCapability,
70328
71220
  flood: floodCapability,
70329
71221
  gas: gasCapability,
71222
+ groupActive: groupActiveCapability,
71223
+ groupCommands: groupCommandsCapability,
71224
+ groupStatus: groupStatusCapability,
70330
71225
  heatingValve: heatingValveCapability,
70331
71226
  humidifier: humidifierCapability,
70332
71227
  humiditySensor: humiditySensorCapability,
@@ -70336,6 +71231,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
70336
71231
  lawnMowerControl: lawnMowerControlCapability,
70337
71232
  lockControl: lockControlCapability,
70338
71233
  mediaPlayer: mediaPlayerCapability,
71234
+ meter: meterCapability,
70339
71235
  motion: motionCapability,
70340
71236
  motionTrigger: motionTriggerCapability,
70341
71237
  motionZones: motionZonesCapability,
@@ -71595,7 +72491,7 @@ var BaseDeviceProvider = class extends BaseAddon {
71595
72491
  return toDeviceSummary(device, this.addonId);
71596
72492
  }
71597
72493
  };
71598
- 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;
72494
+ 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;
71599
72495
  new Set(Object.values(DeviceType));
71600
72496
  Object.freeze({
71601
72497
  "accessories.getStatus": {
@@ -71703,6 +72599,13 @@ Object.freeze({
71703
72599
  access: "view",
71704
72600
  role: "admin"
71705
72601
  },
72602
+ "addons.getPackageDocs": {
72603
+ capName: "addons",
72604
+ capScope: "system",
72605
+ addonId: null,
72606
+ access: "view",
72607
+ role: "admin"
72608
+ },
71706
72609
  "addons.getVersions": {
71707
72610
  capName: "addons",
71708
72611
  capScope: "system",
@@ -74811,6 +75714,27 @@ Object.freeze({
74811
75714
  access: "view",
74812
75715
  role: "viewer"
74813
75716
  },
75717
+ "groupActive.getStatus": {
75718
+ capName: "group-active",
75719
+ capScope: "device",
75720
+ addonId: null,
75721
+ access: "view",
75722
+ role: "viewer"
75723
+ },
75724
+ "groupCommands.getStatus": {
75725
+ capName: "group-commands",
75726
+ capScope: "device",
75727
+ addonId: null,
75728
+ access: "view",
75729
+ role: "viewer"
75730
+ },
75731
+ "groupStatus.getStatus": {
75732
+ capName: "group-status",
75733
+ capScope: "device",
75734
+ addonId: null,
75735
+ access: "view",
75736
+ role: "viewer"
75737
+ },
74814
75738
  "heatingValve.getStatus": {
74815
75739
  capName: "heating-valve",
74816
75740
  capScope: "device",
@@ -75763,6 +76687,13 @@ Object.freeze({
75763
76687
  access: "create",
75764
76688
  role: "admin"
75765
76689
  },
76690
+ "meter.getStatus": {
76691
+ capName: "meter",
76692
+ capScope: "device",
76693
+ addonId: null,
76694
+ access: "view",
76695
+ role: "viewer"
76696
+ },
75766
76697
  "metricsProvider.collectSnapshot": {
75767
76698
  capName: "metrics-provider",
75768
76699
  capScope: "system",
@@ -83861,7 +84792,39 @@ var APPLIANCE_CAP_VOCABULARY = {
83861
84792
  phrase: t("When {device} finishes", "Quando {device} finisce"),
83862
84793
  icon: "square"
83863
84794
  }
83864
- }
84795
+ },
84796
+ overrides: { byType: {
84797
+ [DeviceType.Washer]: {
84798
+ state: { running: {
84799
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
84800
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
84801
+ } },
84802
+ events: {
84803
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
84804
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
84805
+ }
84806
+ },
84807
+ [DeviceType.Dryer]: {
84808
+ state: { running: {
84809
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
84810
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
84811
+ } },
84812
+ events: {
84813
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
84814
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
84815
+ }
84816
+ },
84817
+ [DeviceType.Dishwasher]: {
84818
+ state: { running: {
84819
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
84820
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
84821
+ } },
84822
+ events: {
84823
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
84824
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
84825
+ }
84826
+ }
84827
+ } }
83865
84828
  },
83866
84829
  update: {
83867
84830
  scope: "device",
@@ -84633,6 +85596,41 @@ var SENSOR_CAP_VOCABULARY = {
84633
85596
  icon: "flame"
84634
85597
  }]
84635
85598
  },
85599
+ "group-active": {
85600
+ scope: "device",
85601
+ noun: t("Group state", "Stato del gruppo")
85602
+ },
85603
+ "group-commands": {
85604
+ scope: "device",
85605
+ noun: t("Group command", "Comando di gruppo")
85606
+ },
85607
+ "group-status": {
85608
+ scope: "device",
85609
+ noun: t("Group", "Gruppo"),
85610
+ state: [
85611
+ {
85612
+ kind: "numeric",
85613
+ field: "included",
85614
+ quantity: t("the members reporting", "i membri che rispondono"),
85615
+ label: t("Reporting", "Rispondono"),
85616
+ icon: "users"
85617
+ },
85618
+ {
85619
+ kind: "numeric",
85620
+ field: "excluded",
85621
+ quantity: t("the members left out", "i membri esclusi"),
85622
+ label: t("Excluded", "Esclusi"),
85623
+ icon: "users"
85624
+ },
85625
+ {
85626
+ kind: "numeric",
85627
+ field: "total",
85628
+ quantity: t("the member count", "il numero di membri"),
85629
+ label: t("Members", "Membri"),
85630
+ icon: "sigma"
85631
+ }
85632
+ ]
85633
+ },
84636
85634
  "humidity-sensor": {
84637
85635
  scope: "device",
84638
85636
  noun: t("Humidity", "Umidità"),
@@ -84658,6 +85656,17 @@ var SENSOR_CAP_VOCABULARY = {
84658
85656
  }],
84659
85657
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
84660
85658
  },
85659
+ meter: {
85660
+ scope: "device",
85661
+ noun: t("Meter", "Contatore"),
85662
+ state: [{
85663
+ kind: "numeric",
85664
+ field: "value",
85665
+ quantity: t("the total", "il totale"),
85666
+ label: t("Total", "Totale"),
85667
+ icon: "gauge"
85668
+ }]
85669
+ },
84661
85670
  "numeric-sensor": {
84662
85671
  scope: "device",
84663
85672
  noun: t("Sensor", "Sensore"),
@@ -84666,7 +85675,6 @@ var SENSOR_CAP_VOCABULARY = {
84666
85675
  field: "value",
84667
85676
  quantity: t("the reading", "il valore"),
84668
85677
  label: t("Reading", "Valore"),
84669
- unitField: "unit",
84670
85678
  icon: "gauge"
84671
85679
  }]
84672
85680
  },
@@ -84802,7 +85810,7 @@ var SENSOR_CAP_VOCABULARY = {
84802
85810
  field: "temperature",
84803
85811
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84804
85812
  label: t("Temperature", "Temperatura"),
84805
- unitField: "temperatureUnit",
85813
+ unit: "°C",
84806
85814
  icon: "thermometer"
84807
85815
  },
84808
85816
  {
@@ -84818,7 +85826,7 @@ var SENSOR_CAP_VOCABULARY = {
84818
85826
  field: "windSpeed",
84819
85827
  quantity: t("the wind speed", "la velocità del vento"),
84820
85828
  label: t("Wind", "Vento"),
84821
- unitField: "windSpeedUnit",
85829
+ unit: "m/s",
84822
85830
  icon: "wind"
84823
85831
  },
84824
85832
  {
@@ -84826,7 +85834,7 @@ var SENSOR_CAP_VOCABULARY = {
84826
85834
  field: "pressure",
84827
85835
  quantity: t("the air pressure", "la pressione atmosferica"),
84828
85836
  label: t("Pressure", "Pressione"),
84829
- unitField: "pressureUnit",
85837
+ unit: "hPa",
84830
85838
  icon: "gauge"
84831
85839
  }
84832
85840
  ]
@@ -85131,8 +86139,47 @@ function projectAction(cap, action) {
85131
86139
  };
85132
86140
  }
85133
86141
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
85134
- 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;
86142
+ 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;
85135
86143
  DeviceFeature.BatteryOperated;
86144
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
86145
+ 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.";
86146
+ 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.";
86147
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
86148
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
86149
+ 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.";
86150
+ function withheld(reason) {
86151
+ return {
86152
+ [DeviceType.Washer]: {
86153
+ exported: false,
86154
+ reason
86155
+ },
86156
+ [DeviceType.Dryer]: {
86157
+ exported: false,
86158
+ reason
86159
+ },
86160
+ [DeviceType.Dishwasher]: {
86161
+ exported: false,
86162
+ reason
86163
+ }
86164
+ };
86165
+ }
86166
+ function offered(received) {
86167
+ return {
86168
+ [DeviceType.Washer]: {
86169
+ exported: true,
86170
+ as: received
86171
+ },
86172
+ [DeviceType.Dryer]: {
86173
+ exported: true,
86174
+ as: received
86175
+ },
86176
+ [DeviceType.Dishwasher]: {
86177
+ exported: true,
86178
+ as: received
86179
+ }
86180
+ };
86181
+ }
86182
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
85136
86183
  Object.freeze({
85137
86184
  "accessories.getStatus": [{
85138
86185
  name: "deviceId",
@@ -86099,6 +87146,21 @@ Object.freeze({
86099
87146
  form: "single",
86100
87147
  optional: false
86101
87148
  }],
87149
+ "groupActive.getStatus": [{
87150
+ name: "deviceId",
87151
+ form: "single",
87152
+ optional: false
87153
+ }],
87154
+ "groupCommands.getStatus": [{
87155
+ name: "deviceId",
87156
+ form: "single",
87157
+ optional: false
87158
+ }],
87159
+ "groupStatus.getStatus": [{
87160
+ name: "deviceId",
87161
+ form: "single",
87162
+ optional: false
87163
+ }],
86102
87164
  "heatingValve.getStatus": [{
86103
87165
  name: "deviceId",
86104
87166
  form: "single",
@@ -86299,6 +87361,11 @@ Object.freeze({
86299
87361
  form: "single",
86300
87362
  optional: false
86301
87363
  }],
87364
+ "meter.getStatus": [{
87365
+ name: "deviceId",
87366
+ form: "single",
87367
+ optional: false
87368
+ }],
86302
87369
  "motion.getStatus": [{
86303
87370
  name: "deviceId",
86304
87371
  form: "single",
@@ -87906,7 +88973,6 @@ Object.freeze({
87906
88973
  "network-access": "ingress",
87907
88974
  "smtp-provider": "email"
87908
88975
  });
87909
- DeviceType.Camera;
87910
88976
  /**
87911
88977
  * A saved Customize assignment (D936). The schema is the type: callers use
87912
88978
  * `z.infer`, they do not restate the shape.
@@ -88083,6 +89149,8 @@ new Map(Object.entries({
88083
89149
  "detection-blind": "device-events",
88084
89150
  "motion-noisy": "device-events",
88085
89151
  "motion-calm": "device-events",
89152
+ "package-llm-silent": "device-events",
89153
+ "package-llm-answered": "device-events",
88086
89154
  "recording-stalled": "device-events",
88087
89155
  "recording-healthy": "device-events",
88088
89156
  "recording-degraded": "device-events",
@@ -88166,6 +89234,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
88166
89234
  label: "Motion without a subject",
88167
89235
  kinds: ["motion-noisy", "motion-calm"]
88168
89236
  },
89237
+ {
89238
+ id: "package-llm",
89239
+ family: "device-events",
89240
+ label: "Package confirm",
89241
+ kinds: ["package-llm-silent", "package-llm-answered"]
89242
+ },
88169
89243
  {
88170
89244
  id: "recording",
88171
89245
  family: "device-events",
@@ -88347,6 +89421,8 @@ var DEVICE_EVENTS = [
88347
89421
  "detection-blind",
88348
89422
  "motion-noisy",
88349
89423
  "motion-calm",
89424
+ "package-llm-silent",
89425
+ "package-llm-answered",
88350
89426
  "recording-stalled",
88351
89427
  "recording-healthy",
88352
89428
  "recording-degraded",
@@ -88615,6 +89691,15 @@ new Map([
88615
89691
  snapshotLabel: "Cameras with constant motion now",
88616
89692
  snapshotSection: null
88617
89693
  },
89694
+ {
89695
+ id: "package-llm-confirm",
89696
+ family: "device-events",
89697
+ label: "Package confirm (not answering / answering again)",
89698
+ bad: ["package-llm-silent"],
89699
+ recovery: ["package-llm-answered"],
89700
+ snapshotLabel: "Cameras whose package confirm is not answering",
89701
+ snapshotSection: null
89702
+ },
88618
89703
  {
88619
89704
  id: "recording-health",
88620
89705
  family: "device-events",