@camstack/addon-provider-unifi 0.2.147 → 0.2.149

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region ../types/dist/event-category-DHXxB8t9.mjs
2
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -325,6 +325,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
325
325
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
326
326
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
327
327
  /**
328
+ * A camera's package LLM confirm has failed closed three times in a row
329
+ * (D931, 2026-10-26). One edge per camera until
330
+ * {@link PipelinePackageLlmAnswered}.
331
+ */
332
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
333
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
334
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
335
+ /**
328
336
  * Per-camera pipeline config was mutated by the orchestrator
329
337
  * (a settings change via `setAgentInferenceDevices` /
330
338
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5434,7 +5442,7 @@ var ZodIssueCode = {
5434
5442
  var ZodFirstPartyTypeKind;
5435
5443
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5436
5444
  //#endregion
5437
- //#region ../types/dist/sleep-BErAxhul.mjs
5445
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5438
5446
  /**
5439
5447
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5440
5448
  * window to float samples (D455).
@@ -7988,6 +7996,99 @@ var RecordKindDeclarationSchema = object({
7988
7996
  }).strict();
7989
7997
  /** The kind names: lower-case, dash separated, short enough for an index name. */
7990
7998
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7999
+ var READING_QUANTITIES = [
8000
+ "temperature",
8001
+ "pressure",
8002
+ "speed",
8003
+ "precipitation",
8004
+ "precipitation-rate",
8005
+ "length",
8006
+ "area",
8007
+ "illuminance",
8008
+ "irradiance",
8009
+ "power",
8010
+ "energy",
8011
+ "voltage",
8012
+ "current",
8013
+ "apparent-power",
8014
+ "humidity",
8015
+ "percentage",
8016
+ "concentration-volume",
8017
+ "concentration-mass",
8018
+ "concentration-ppb",
8019
+ "volume",
8020
+ "duration",
8021
+ "duration-ms",
8022
+ "information",
8023
+ "sound-level",
8024
+ "signal-level",
8025
+ "colour-temperature",
8026
+ "angle",
8027
+ "bitrate",
8028
+ "rate",
8029
+ "mass",
8030
+ "index",
8031
+ "ratio",
8032
+ "count",
8033
+ "none",
8034
+ "device"
8035
+ ];
8036
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8037
+ new Set(READING_QUANTITIES);
8038
+ /**
8039
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8040
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8041
+ */
8042
+ var QUANTITY_BASE = {
8043
+ temperature: "°C",
8044
+ pressure: "hPa",
8045
+ speed: "m/s",
8046
+ precipitation: "mm",
8047
+ "precipitation-rate": "mm/h",
8048
+ length: "m",
8049
+ area: "m²",
8050
+ illuminance: "lx",
8051
+ irradiance: "W/m²",
8052
+ power: "W",
8053
+ energy: "kWh",
8054
+ voltage: "V",
8055
+ current: "A",
8056
+ "apparent-power": "VA",
8057
+ humidity: "%",
8058
+ percentage: "%",
8059
+ "concentration-volume": "ppm",
8060
+ "concentration-mass": "µg/m³",
8061
+ "concentration-ppb": "ppb",
8062
+ volume: "L",
8063
+ duration: "s",
8064
+ "duration-ms": "ms",
8065
+ information: "B",
8066
+ "sound-level": "dBFS",
8067
+ "signal-level": "dBm",
8068
+ "colour-temperature": "mired",
8069
+ angle: "°",
8070
+ bitrate: "kbps",
8071
+ rate: "fps",
8072
+ mass: "kg",
8073
+ index: "none",
8074
+ ratio: "none",
8075
+ count: "none",
8076
+ none: "none",
8077
+ device: "device"
8078
+ };
8079
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8080
+ function baseUnitOf(quantity) {
8081
+ const symbol = QUANTITY_BASE[quantity];
8082
+ if (symbol === "none" || symbol === "device") return void 0;
8083
+ return symbol;
8084
+ }
8085
+ function readingMeta(quantity, extras) {
8086
+ return {
8087
+ quantity,
8088
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8089
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8090
+ };
8091
+ }
7991
8092
  /** Shared Zod schemas used across streaming capabilities. */
7992
8093
  var CamProfileSchema = _enum([
7993
8094
  "high",
@@ -8015,8 +8116,8 @@ var CamStreamKindSchema = _enum([
8015
8116
  "derived"
8016
8117
  ]);
8017
8118
  var CamStreamResolutionSchema = object({
8018
- width: number().int().positive(),
8019
- height: number().int().positive()
8119
+ width: number().int().positive().meta(readingMeta("none")),
8120
+ height: number().int().positive().meta(readingMeta("none"))
8020
8121
  });
8021
8122
  var CameraStreamSchema = object({
8022
8123
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8534,6 +8635,9 @@ var CAP_NAMES = Object.freeze([
8534
8635
  "filesystem-browse",
8535
8636
  "flood",
8536
8637
  "gas",
8638
+ "group-active",
8639
+ "group-commands",
8640
+ "group-status",
8537
8641
  "heating-valve",
8538
8642
  "humidifier",
8539
8643
  "humidity-sensor",
@@ -8554,6 +8658,7 @@ var CAP_NAMES = Object.freeze([
8554
8658
  "login-method",
8555
8659
  "media-player",
8556
8660
  "mesh-network",
8661
+ "meter",
8557
8662
  "metrics-provider",
8558
8663
  "model-convert",
8559
8664
  "model-distributor",
@@ -8663,14 +8768,6 @@ Object.freeze({
8663
8768
  {
8664
8769
  "path": "aqi",
8665
8770
  "valueType": "number"
8666
- },
8667
- {
8668
- "path": "unit",
8669
- "valueType": "string"
8670
- },
8671
- {
8672
- "path": "precision",
8673
- "valueType": "number"
8674
8771
  }
8675
8772
  ],
8676
8773
  "alarm-panel": [
@@ -8706,20 +8803,10 @@ Object.freeze({
8706
8803
  "valueType": "boolean"
8707
8804
  }
8708
8805
  ],
8709
- "ambient-light-sensor": [
8710
- {
8711
- "path": "lux",
8712
- "valueType": "number"
8713
- },
8714
- {
8715
- "path": "unit",
8716
- "valueType": "string"
8717
- },
8718
- {
8719
- "path": "precision",
8720
- "valueType": "number"
8721
- }
8722
- ],
8806
+ "ambient-light-sensor": [{
8807
+ "path": "lux",
8808
+ "valueType": "number"
8809
+ }],
8723
8810
  "audio-metrics": [
8724
8811
  {
8725
8812
  "path": "ts",
@@ -8799,6 +8886,10 @@ Object.freeze({
8799
8886
  "path": "lastUpdated",
8800
8887
  "valueType": "number"
8801
8888
  },
8889
+ {
8890
+ "path": "volts",
8891
+ "valueType": "number"
8892
+ },
8802
8893
  {
8803
8894
  "path": "lastContactAt",
8804
8895
  "valueType": "number"
@@ -9041,10 +9132,6 @@ Object.freeze({
9041
9132
  "datetime"
9042
9133
  ]
9043
9134
  },
9044
- {
9045
- "path": "unit",
9046
- "valueType": "string"
9047
- },
9048
9135
  {
9049
9136
  "path": "min",
9050
9137
  "valueType": "number"
@@ -9056,19 +9143,6 @@ Object.freeze({
9056
9143
  {
9057
9144
  "path": "step",
9058
9145
  "valueType": "number"
9059
- },
9060
- {
9061
- "path": "precision",
9062
- "valueType": "number"
9063
- },
9064
- {
9065
- "path": "format",
9066
- "valueType": "enum",
9067
- "enumValues": [
9068
- "date",
9069
- "time",
9070
- "datetime"
9071
- ]
9072
9146
  }
9073
9147
  ],
9074
9148
  "cover": [
@@ -9309,13 +9383,8 @@ Object.freeze({
9309
9383
  "path": "value",
9310
9384
  "valueType": "string"
9311
9385
  }, {
9312
- "path": "format",
9313
- "valueType": "enum",
9314
- "enumValues": [
9315
- "date",
9316
- "time",
9317
- "datetime"
9318
- ]
9386
+ "path": "timestamp",
9387
+ "valueType": "boolean"
9319
9388
  }],
9320
9389
  "event-emitter": [
9321
9390
  {
@@ -9388,6 +9457,24 @@ Object.freeze({
9388
9457
  "path": "detected",
9389
9458
  "valueType": "boolean"
9390
9459
  }],
9460
+ "group-active": [{
9461
+ "path": "active",
9462
+ "valueType": "boolean"
9463
+ }],
9464
+ "group-status": [
9465
+ {
9466
+ "path": "included",
9467
+ "valueType": "number"
9468
+ },
9469
+ {
9470
+ "path": "excluded",
9471
+ "valueType": "number"
9472
+ },
9473
+ {
9474
+ "path": "total",
9475
+ "valueType": "number"
9476
+ }
9477
+ ],
9391
9478
  "heating-valve": [{
9392
9479
  "path": "position",
9393
9480
  "valueType": "number"
@@ -9422,20 +9509,10 @@ Object.freeze({
9422
9509
  "valueType": "number"
9423
9510
  }
9424
9511
  ],
9425
- "humidity-sensor": [
9426
- {
9427
- "path": "percent",
9428
- "valueType": "number"
9429
- },
9430
- {
9431
- "path": "unit",
9432
- "valueType": "string"
9433
- },
9434
- {
9435
- "path": "precision",
9436
- "valueType": "number"
9437
- }
9438
- ],
9512
+ "humidity-sensor": [{
9513
+ "path": "percent",
9514
+ "valueType": "number"
9515
+ }],
9439
9516
  "image": [{
9440
9517
  "path": "url",
9441
9518
  "valueType": "string"
@@ -9643,6 +9720,10 @@ Object.freeze({
9643
9720
  ]
9644
9721
  }
9645
9722
  ],
9723
+ "meter": [{
9724
+ "path": "value",
9725
+ "valueType": "number"
9726
+ }],
9646
9727
  "motion": [
9647
9728
  {
9648
9729
  "path": "detected",
@@ -9774,20 +9855,10 @@ Object.freeze({
9774
9855
  "valueType": "boolean"
9775
9856
  }
9776
9857
  ],
9777
- "numeric-sensor": [
9778
- {
9779
- "path": "value",
9780
- "valueType": "number"
9781
- },
9782
- {
9783
- "path": "unit",
9784
- "valueType": "string"
9785
- },
9786
- {
9787
- "path": "precision",
9788
- "valueType": "number"
9789
- }
9790
- ],
9858
+ "numeric-sensor": [{
9859
+ "path": "value",
9860
+ "valueType": "number"
9861
+ }],
9791
9862
  "pet-feeder": [
9792
9863
  {
9793
9864
  "path": "foodLevel",
@@ -9879,14 +9950,6 @@ Object.freeze({
9879
9950
  {
9880
9951
  "path": "powerFactor",
9881
9952
  "valueType": "number"
9882
- },
9883
- {
9884
- "path": "unit",
9885
- "valueType": "string"
9886
- },
9887
- {
9888
- "path": "precision",
9889
- "valueType": "number"
9890
9953
  }
9891
9954
  ],
9892
9955
  "presence": [
@@ -9915,20 +9978,10 @@ Object.freeze({
9915
9978
  "valueType": "number"
9916
9979
  }
9917
9980
  ],
9918
- "pressure-sensor": [
9919
- {
9920
- "path": "hpa",
9921
- "valueType": "number"
9922
- },
9923
- {
9924
- "path": "unit",
9925
- "valueType": "string"
9926
- },
9927
- {
9928
- "path": "precision",
9929
- "valueType": "number"
9930
- }
9931
- ],
9981
+ "pressure-sensor": [{
9982
+ "path": "hpa",
9983
+ "valueType": "number"
9984
+ }],
9932
9985
  "privacy-mask": [{
9933
9986
  "path": "enabled",
9934
9987
  "valueType": "boolean"
@@ -10239,20 +10292,10 @@ Object.freeze({
10239
10292
  "path": "tampered",
10240
10293
  "valueType": "boolean"
10241
10294
  }],
10242
- "temperature-sensor": [
10243
- {
10244
- "path": "celsius",
10245
- "valueType": "number"
10246
- },
10247
- {
10248
- "path": "unit",
10249
- "valueType": "string"
10250
- },
10251
- {
10252
- "path": "precision",
10253
- "valueType": "number"
10254
- }
10255
- ],
10295
+ "temperature-sensor": [{
10296
+ "path": "celsius",
10297
+ "valueType": "number"
10298
+ }],
10256
10299
  "update": [
10257
10300
  {
10258
10301
  "path": "currentVersion",
@@ -10363,10 +10406,6 @@ Object.freeze({
10363
10406
  "path": "temperature",
10364
10407
  "valueType": "number"
10365
10408
  },
10366
- {
10367
- "path": "temperatureUnit",
10368
- "valueType": "string"
10369
- },
10370
10409
  {
10371
10410
  "path": "humidity",
10372
10411
  "valueType": "number"
@@ -10375,18 +10414,10 @@ Object.freeze({
10375
10414
  "path": "pressure",
10376
10415
  "valueType": "number"
10377
10416
  },
10378
- {
10379
- "path": "pressureUnit",
10380
- "valueType": "string"
10381
- },
10382
10417
  {
10383
10418
  "path": "windSpeed",
10384
10419
  "valueType": "number"
10385
10420
  },
10386
- {
10387
- "path": "windSpeedUnit",
10388
- "valueType": "string"
10389
- },
10390
10421
  {
10391
10422
  "path": "windBearing",
10392
10423
  "valueType": "number"
@@ -10450,7 +10481,7 @@ function userError(code, params, englishFallback) {
10450
10481
  return new CamstackError(code, params, englishFallback);
10451
10482
  }
10452
10483
  //#endregion
10453
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10484
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10454
10485
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10455
10486
  DeviceType["Camera"] = "camera";
10456
10487
  DeviceType["Hub"] = "hub";
@@ -10544,8 +10575,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10544
10575
  DeviceType["PetFeeder"] = "pet-feeder";
10545
10576
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10546
10577
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10547
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10578
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10579
+ * A plug-built dishwasher carries running-state and power instead. */
10548
10580
  DeviceType["Dishwasher"] = "dishwasher";
10581
+ /** Washing machine. A plug-built one is running while its power stays
10582
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10583
+ DeviceType["Washer"] = "washer";
10584
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10585
+ DeviceType["Dryer"] = "dryer";
10549
10586
  return DeviceType;
10550
10587
  }({});
10551
10588
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -10803,7 +10840,7 @@ function errMsg(err) {
10803
10840
  return String(err);
10804
10841
  }
10805
10842
  //#endregion
10806
- //#region ../types/dist/composition-kEl9mevu.mjs
10843
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
10807
10844
  /**
10808
10845
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10809
10846
  *
@@ -11118,6 +11155,13 @@ var table = {
11118
11155
  apply: () => {
11119
11156
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11120
11157
  }
11158
+ },
11159
+ schmitt: {
11160
+ minArgs: 3,
11161
+ maxArgs: 5,
11162
+ apply: () => {
11163
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11164
+ }
11121
11165
  }
11122
11166
  };
11123
11167
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11558,15 +11602,232 @@ function validateExpressionSource(src) {
11558
11602
  }
11559
11603
  return null;
11560
11604
  }
11561
- /** A composed device's stableId is this prefix plus the block id. */
11562
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11563
- /** Longest snippet a `code` source may carry (slice 4). */
11564
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11605
+ /**
11606
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11607
+ *
11608
+ * Standby of the control board is under 5 W; a motor or a heater is far
11609
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11610
+ * a cycle, so running starts only after 30 s above the line.
11611
+ *
11612
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11613
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11614
+ * report "finished" in the middle of the program, so the stop dwell is
11615
+ * 10 minutes on every type. The number is stored per type so one machine
11616
+ * can move later without moving the others.
11617
+ */
11618
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11619
+ var APPLIANCE_KINDS = [
11620
+ "washer",
11621
+ "dryer",
11622
+ "dishwasher"
11623
+ ];
11624
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11625
+ /**
11626
+ * Statistics and integrals over a history read (D944).
11627
+ *
11628
+ * Pure: the same points always give the same answer, so a restart recomputes
11629
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11630
+ * never the mean of the samples. A hole with no held value is not integrated
11631
+ * as zero. A truncated read is not published.
11632
+ */
11633
+ var STATISTIC_STATS = [
11634
+ "min",
11635
+ "max",
11636
+ "mean",
11637
+ "sum",
11638
+ "count",
11639
+ "change"
11640
+ ];
11641
+ /**
11642
+ * The range a statistics or integral helper asks history for (D944).
11643
+ *
11644
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11645
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11646
+ * is strictly longer than the longest that range can be. Forever qualifies.
11647
+ * Equal to a tier's retention is a refusal, not a short answer.
11648
+ */
11649
+ var MINUTE_MS = 6e4;
11650
+ var HOUR_MS = 60 * MINUTE_MS;
11651
+ var DAY_MS = 24 * HOUR_MS;
11652
+ var ROLLING_SPANS = [
11653
+ "1h",
11654
+ "24h",
11655
+ "2d",
11656
+ "7d",
11657
+ "30d",
11658
+ "custom"
11659
+ ];
11660
+ var CALENDAR_PERIODS = [
11661
+ "today",
11662
+ "yesterday",
11663
+ "this-week",
11664
+ "this-month"
11665
+ ];
11666
+ var RESET_PERIODS = [
11667
+ "never",
11668
+ "daily",
11669
+ "weekly",
11670
+ "monthly"
11671
+ ];
11672
+ 5 * MINUTE_MS;
11673
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11674
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11675
+ var rollingWindowObject = object({
11676
+ kind: literal("rolling"),
11677
+ span: _enum(ROLLING_SPANS),
11678
+ customMinutes: number().int().min(5).optional()
11679
+ }).check((payload) => {
11680
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11681
+ code: "custom",
11682
+ message: "a custom range needs a duration in minutes",
11683
+ path: ["customMinutes"],
11684
+ input: payload.value
11685
+ });
11686
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11687
+ code: "custom",
11688
+ message: "only a custom range carries a duration",
11689
+ path: ["customMinutes"],
11690
+ input: payload.value
11691
+ });
11692
+ });
11693
+ var RollingWindowSchema = rollingWindowObject;
11694
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
11695
+ kind: literal("calendar"),
11696
+ period: _enum(CALENDAR_PERIODS)
11697
+ })]);
11698
+ /**
11699
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11700
+ * id. Split out so helper schemas can name it without a cycle through
11701
+ * `composition.ts` (D659).
11702
+ */
11565
11703
  var CompositionSourceRefSchema = object({
11566
11704
  /** The owning addon; `stableId` is unique only within it. */
11567
11705
  addonId: string().min(1),
11568
11706
  stableId: string().min(1)
11569
11707
  });
11708
+ var GROUP_TEMPLATE_IDS = [
11709
+ "temperature",
11710
+ "humidity",
11711
+ "pressure",
11712
+ "illuminance",
11713
+ "power",
11714
+ "energy",
11715
+ "battery",
11716
+ "numeric",
11717
+ "motion",
11718
+ "contact",
11719
+ "leak",
11720
+ "smoke",
11721
+ "occupancy",
11722
+ "binary",
11723
+ "switch",
11724
+ "light",
11725
+ "fan",
11726
+ "cover",
11727
+ "valve",
11728
+ "lock",
11729
+ "media-player",
11730
+ "climate"
11731
+ ];
11732
+ var NUMERIC_AGGREGATES = [
11733
+ "mean",
11734
+ "median",
11735
+ "min",
11736
+ "max",
11737
+ "sum"
11738
+ ];
11739
+ var BINARY_AGGREGATES = ["any", "all"];
11740
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11741
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11742
+ var AGGREGATE_REDUCERS = [
11743
+ "mean",
11744
+ "median",
11745
+ "min",
11746
+ "max",
11747
+ "sum",
11748
+ "any",
11749
+ "all"
11750
+ ];
11751
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11752
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11753
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11754
+ /** One member. The field is stored so occupancy can name motion or binary. */
11755
+ var HelperMemberSchema = object({
11756
+ source: CompositionSourceRefSchema,
11757
+ cap: string().min(1),
11758
+ fieldPath: string().min(1)
11759
+ });
11760
+ var HelperSpecSchema = discriminatedUnion("kind", [
11761
+ object({
11762
+ kind: literal("group"),
11763
+ templateId: GroupTemplateIdSchema,
11764
+ aggregate: AggregateReducerSchema,
11765
+ members: array(HelperMemberSchema).min(1).max(64),
11766
+ /** Actuator groups only. Readings leave this unset. */
11767
+ active: ActiveCriterionSchema.optional()
11768
+ }),
11769
+ object({
11770
+ kind: literal("appliance"),
11771
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
11772
+ appliance: _enum(APPLIANCE_KINDS),
11773
+ thresholdWatts: number().positive(),
11774
+ startDwellMs: number().int().nonnegative(),
11775
+ stopDwellMs: number().int().nonnegative(),
11776
+ member: HelperMemberSchema
11777
+ }),
11778
+ object({
11779
+ kind: literal("statistic"),
11780
+ stat: _enum(STATISTIC_STATS),
11781
+ window: StatisticWindowSchema,
11782
+ source: HelperMemberSchema
11783
+ }),
11784
+ object({
11785
+ kind: literal("integral"),
11786
+ reset: _enum(RESET_PERIODS),
11787
+ output: _enum(["energy", "meter"]),
11788
+ source: HelperMemberSchema
11789
+ }),
11790
+ object({
11791
+ kind: literal("threshold"),
11792
+ source: HelperMemberSchema,
11793
+ high: number(),
11794
+ low: number(),
11795
+ onForMs: number().int().nonnegative(),
11796
+ offForMs: number().int().nonnegative()
11797
+ }),
11798
+ object({
11799
+ kind: literal("arithmetic"),
11800
+ op: _enum([
11801
+ "sum",
11802
+ "difference",
11803
+ "ratio"
11804
+ ]),
11805
+ inputs: array(HelperMemberSchema).min(2).max(64)
11806
+ }),
11807
+ object({
11808
+ kind: literal("expression"),
11809
+ expr: string().min(1),
11810
+ inputs: array(object({
11811
+ name: string().min(1),
11812
+ member: HelperMemberSchema
11813
+ })).min(1).max(64),
11814
+ output: _enum(["number", "boolean"])
11815
+ }),
11816
+ object({
11817
+ kind: literal("derivative"),
11818
+ window: RollingWindowSchema,
11819
+ per: _enum([
11820
+ "second",
11821
+ "minute",
11822
+ "hour"
11823
+ ]),
11824
+ source: HelperMemberSchema
11825
+ })
11826
+ ]);
11827
+ /** A composed device's stableId is this prefix plus the block id. */
11828
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11829
+ /** Longest snippet a `code` source may carry (slice 4). */
11830
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11570
11831
  /** One field of one capability of one source device. */
11571
11832
  var CompositionFieldReadSchema = object({
11572
11833
  source: CompositionSourceRefSchema,
@@ -11610,7 +11871,59 @@ var CompositionCodeSourceSchema = object({
11610
11871
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11611
11872
  CompositionFromSourceSchema,
11612
11873
  CompositionExpressionSourceSchema,
11613
- CompositionCodeSourceSchema
11874
+ CompositionCodeSourceSchema,
11875
+ object({
11876
+ kind: literal("aggregate"),
11877
+ reducer: _enum([
11878
+ "mean",
11879
+ "median",
11880
+ "min",
11881
+ "max",
11882
+ "sum",
11883
+ "any",
11884
+ "all"
11885
+ ]),
11886
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11887
+ /**
11888
+ * Set when the reads are an enum state, not a yes/no: the listed states
11889
+ * count as active (a cover is `open`). A state outside the list is inactive;
11890
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
11891
+ */
11892
+ activeWhen: array(string().min(1)).min(1).optional(),
11893
+ /** `value` is the reading; the others are the member counts. */
11894
+ publish: _enum([
11895
+ "value",
11896
+ "included",
11897
+ "excluded",
11898
+ "total"
11899
+ ]),
11900
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11901
+ empty: _enum([
11902
+ "null",
11903
+ "omit",
11904
+ "offline"
11905
+ ])
11906
+ }),
11907
+ object({
11908
+ kind: literal("series"),
11909
+ read: CompositionFieldReadSchema,
11910
+ /** `value` is the statistic or the integral. The others are the gap counts. */
11911
+ publish: _enum([
11912
+ "value",
11913
+ "included",
11914
+ "excluded",
11915
+ "total"
11916
+ ]),
11917
+ /**
11918
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
11919
+ * device unseeded until history answers, so a restart cannot publish 0.
11920
+ */
11921
+ empty: _enum([
11922
+ "null",
11923
+ "omit",
11924
+ "offline"
11925
+ ])
11926
+ })
11614
11927
  ]);
11615
11928
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11616
11929
  kind: literal("forward"),
@@ -11656,7 +11969,13 @@ var CompositionSchema = object({
11656
11969
  kind: literal("existing"),
11657
11970
  device: CompositionSourceRefSchema
11658
11971
  })]),
11659
- features: array(CompositionFeatureSchema).min(1).max(16)
11972
+ features: array(CompositionFeatureSchema).min(1).max(16),
11973
+ /**
11974
+ * Present when the block is a helper template. Absent on a free
11975
+ * composition (the room example, device 4603). The editor reads this,
11976
+ * never the compiled features.
11977
+ */
11978
+ helper: HelperSpecSchema.optional()
11660
11979
  }).superRefine((composition, ctx) => {
11661
11980
  const seen = /* @__PURE__ */ new Set();
11662
11981
  composition.features.forEach((feature, index) => {
@@ -16763,7 +17082,22 @@ var BrokerProviderInfoSchema = object({
16763
17082
  */
16764
17083
  interactiveConnectPath: string().min(1).optional(),
16765
17084
  /** Relative hub path to disconnect the linked account (same popup pattern). */
16766
- interactiveDisconnectPath: string().min(1).optional()
17085
+ interactiveDisconnectPath: string().min(1).optional(),
17086
+ /**
17087
+ * The kind signs in through a server-held interactive session instead of
17088
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17089
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17090
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17091
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17092
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17093
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17094
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17095
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17096
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17097
+ * offers it as the default path; the fields the session fills are
17098
+ * marked `filledBySignIn` in the kind's settings schema.
17099
+ */
17100
+ signIn: object({ basePath: string().min(1) }).optional()
16767
17101
  }))
16768
17102
  });
16769
17103
  var ListInputSchema = object({
@@ -17864,7 +18198,12 @@ var CompositionProblemSchema = object({
17864
18198
  "item-field-reserved",
17865
18199
  "target-missing",
17866
18200
  "target-unreadable",
17867
- "not-replaceable"
18201
+ "not-replaceable",
18202
+ "native-capability-replace",
18203
+ "mixed-quantity",
18204
+ "helper-mismatch",
18205
+ "member-constraint",
18206
+ "retention"
17868
18207
  ]),
17869
18208
  path: string(),
17870
18209
  message: string()
@@ -17885,7 +18224,8 @@ var CompositionUnavailableModeSchema = _enum([
17885
18224
  "null",
17886
18225
  "offline",
17887
18226
  "unknown",
17888
- "release"
18227
+ "release",
18228
+ "omit"
17889
18229
  ]);
17890
18230
  /**
17891
18231
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17922,9 +18262,11 @@ var CompositionFieldPlanSchema = object({
17922
18262
  reads: array(CompositionFieldReadSchema)
17923
18263
  });
17924
18264
  /**
17925
- * What a feature does to its target (D663): `new` builds a capability on a new
17926
- * device; on an existing device it `add`s a capability the device lacks, or
17927
- * `replace`s named fields of one it has natively.
18265
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18266
+ * new device; on an existing device it `add`s a capability the device lacks.
18267
+ * Replacing a capability the device already serves is refused
18268
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18269
+ * wire for a claim the hub already holds.
17928
18270
  */
17929
18271
  var CompositionFeatureModeSchema = _enum([
17930
18272
  "new",
@@ -18095,8 +18437,392 @@ Object.freeze({
18095
18437
  latchedAt: {
18096
18438
  kind: "fixed",
18097
18439
  result: ["number", "null"]
18440
+ },
18441
+ schmitt: {
18442
+ kind: "fixed",
18443
+ result: ["boolean", "null"]
18098
18444
  }
18099
18445
  });
18446
+ /**
18447
+ * One catalog row per base reading a group helper can build (D944 §3).
18448
+ * Data, not a form default written into the block.
18449
+ */
18450
+ var NOW$2 = {
18451
+ fieldPath: "lastFetchedAt",
18452
+ expr: "now"
18453
+ };
18454
+ var CHANGED = {
18455
+ fieldPath: "lastChangedAt",
18456
+ expr: "now"
18457
+ };
18458
+ function numeric(input) {
18459
+ return {
18460
+ ...input,
18461
+ family: "numeric",
18462
+ deviceType: DeviceType.Sensor,
18463
+ memberDeviceType: DeviceType.Sensor,
18464
+ memberDeviceTypes: [DeviceType.Sensor],
18465
+ activeWhen: void 0,
18466
+ commandCaps: [],
18467
+ baseUnit: baseUnitOf(input.quantity),
18468
+ aggregates: NUMERIC_AGGREGATES,
18469
+ accept: [{
18470
+ cap: input.outputCap,
18471
+ fieldPath: input.outputField
18472
+ }]
18473
+ };
18474
+ }
18475
+ function binary(input) {
18476
+ return {
18477
+ ...input,
18478
+ family: "binary",
18479
+ deviceType: DeviceType.Sensor,
18480
+ memberDeviceType: DeviceType.Sensor,
18481
+ memberDeviceTypes: [DeviceType.Sensor],
18482
+ activeWhen: void 0,
18483
+ commandCaps: [],
18484
+ quantity: "none",
18485
+ baseUnit: void 0,
18486
+ aggregates: BINARY_AGGREGATES,
18487
+ empty: "offline"
18488
+ };
18489
+ }
18490
+ var GROUP_TEMPLATES = [
18491
+ numeric({
18492
+ id: "temperature",
18493
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18494
+ role: DeviceRole.TemperatureSensor,
18495
+ outputCap: "temperature-sensor",
18496
+ outputField: "celsius",
18497
+ quantity: "temperature",
18498
+ defaultAggregate: "mean",
18499
+ empty: "null",
18500
+ companions: [NOW$2]
18501
+ }),
18502
+ numeric({
18503
+ id: "humidity",
18504
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18505
+ role: DeviceRole.HumiditySensor,
18506
+ outputCap: "humidity-sensor",
18507
+ outputField: "percent",
18508
+ quantity: "humidity",
18509
+ defaultAggregate: "mean",
18510
+ empty: "null",
18511
+ companions: [NOW$2]
18512
+ }),
18513
+ numeric({
18514
+ id: "pressure",
18515
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18516
+ role: DeviceRole.PressureSensor,
18517
+ outputCap: "pressure-sensor",
18518
+ outputField: "hpa",
18519
+ quantity: "pressure",
18520
+ defaultAggregate: "mean",
18521
+ empty: "null",
18522
+ companions: [NOW$2]
18523
+ }),
18524
+ numeric({
18525
+ id: "illuminance",
18526
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18527
+ role: DeviceRole.AmbientLightSensor,
18528
+ outputCap: "ambient-light-sensor",
18529
+ outputField: "lux",
18530
+ quantity: "illuminance",
18531
+ defaultAggregate: "mean",
18532
+ empty: "null",
18533
+ companions: [NOW$2]
18534
+ }),
18535
+ numeric({
18536
+ id: "power",
18537
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18538
+ role: DeviceRole.PowerSensor,
18539
+ outputCap: "power-meter",
18540
+ outputField: "watts",
18541
+ quantity: "power",
18542
+ defaultAggregate: "sum",
18543
+ empty: "omit",
18544
+ companions: [NOW$2]
18545
+ }),
18546
+ numeric({
18547
+ id: "energy",
18548
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18549
+ role: DeviceRole.EnergySensor,
18550
+ outputCap: "power-meter",
18551
+ outputField: "kwhTotal",
18552
+ quantity: "energy",
18553
+ defaultAggregate: "sum",
18554
+ empty: "omit",
18555
+ companions: [NOW$2]
18556
+ }),
18557
+ numeric({
18558
+ id: "battery",
18559
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18560
+ role: DeviceRole.BatterySensor,
18561
+ outputCap: "battery",
18562
+ outputField: "percentage",
18563
+ quantity: "percentage",
18564
+ defaultAggregate: "mean",
18565
+ empty: "null",
18566
+ companions: [
18567
+ {
18568
+ fieldPath: "charging",
18569
+ expr: "\"unknown\""
18570
+ },
18571
+ {
18572
+ fieldPath: "sleeping",
18573
+ expr: "false"
18574
+ },
18575
+ {
18576
+ fieldPath: "lastUpdated",
18577
+ expr: "now"
18578
+ }
18579
+ ]
18580
+ }),
18581
+ numeric({
18582
+ id: "numeric",
18583
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18584
+ role: DeviceRole.NumericSensor,
18585
+ outputCap: "numeric-sensor",
18586
+ outputField: "value",
18587
+ quantity: "device",
18588
+ defaultAggregate: "mean",
18589
+ empty: "null",
18590
+ companions: [NOW$2]
18591
+ }),
18592
+ binary({
18593
+ id: "motion",
18594
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18595
+ role: DeviceRole.MotionSensor,
18596
+ outputCap: "motion",
18597
+ outputField: "detected",
18598
+ defaultAggregate: "any",
18599
+ accept: [{
18600
+ cap: "motion",
18601
+ fieldPath: "detected"
18602
+ }],
18603
+ companions: [{
18604
+ fieldPath: "lastDetectedAt",
18605
+ expr: "null"
18606
+ }, {
18607
+ fieldPath: "autoClearAfterMs",
18608
+ expr: "null"
18609
+ }]
18610
+ }),
18611
+ binary({
18612
+ id: "contact",
18613
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18614
+ role: DeviceRole.ContactSensor,
18615
+ outputCap: "contact",
18616
+ outputField: "entryOpen",
18617
+ defaultAggregate: "any",
18618
+ accept: [{
18619
+ cap: "contact",
18620
+ fieldPath: "entryOpen"
18621
+ }],
18622
+ companions: [CHANGED]
18623
+ }),
18624
+ binary({
18625
+ id: "leak",
18626
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18627
+ role: DeviceRole.LeakSensor,
18628
+ outputCap: "flood",
18629
+ outputField: "flooded",
18630
+ defaultAggregate: "any",
18631
+ accept: [{
18632
+ cap: "flood",
18633
+ fieldPath: "flooded"
18634
+ }],
18635
+ companions: [CHANGED]
18636
+ }),
18637
+ binary({
18638
+ id: "smoke",
18639
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18640
+ role: DeviceRole.SmokeSensor,
18641
+ outputCap: "smoke",
18642
+ outputField: "detected",
18643
+ defaultAggregate: "any",
18644
+ accept: [{
18645
+ cap: "smoke",
18646
+ fieldPath: "detected"
18647
+ }],
18648
+ companions: [CHANGED]
18649
+ }),
18650
+ binary({
18651
+ id: "occupancy",
18652
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18653
+ role: DeviceRole.BinarySensor,
18654
+ outputCap: "binary",
18655
+ outputField: "on",
18656
+ defaultAggregate: "any",
18657
+ accept: [{
18658
+ cap: "motion",
18659
+ fieldPath: "detected"
18660
+ }, {
18661
+ cap: "binary",
18662
+ fieldPath: "on"
18663
+ }],
18664
+ companions: [CHANGED]
18665
+ }),
18666
+ binary({
18667
+ id: "binary",
18668
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18669
+ role: DeviceRole.BinarySensor,
18670
+ outputCap: "binary",
18671
+ outputField: "on",
18672
+ defaultAggregate: "any",
18673
+ accept: [{
18674
+ cap: "binary",
18675
+ fieldPath: "on"
18676
+ }],
18677
+ companions: [CHANGED]
18678
+ }),
18679
+ actuator({
18680
+ id: "switch",
18681
+ 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.",
18682
+ deviceType: DeviceType.Switch,
18683
+ outputCap: "switch",
18684
+ outputField: "on",
18685
+ member: {
18686
+ cap: "switch",
18687
+ fieldPath: "on"
18688
+ },
18689
+ commandCaps: ["switch"]
18690
+ }),
18691
+ actuator({
18692
+ id: "light",
18693
+ 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.",
18694
+ deviceType: DeviceType.Light,
18695
+ outputCap: "switch",
18696
+ outputField: "on",
18697
+ member: {
18698
+ cap: "switch",
18699
+ fieldPath: "on"
18700
+ },
18701
+ commandCaps: [
18702
+ "switch",
18703
+ "brightness",
18704
+ "color"
18705
+ ]
18706
+ }),
18707
+ actuator({
18708
+ id: "fan",
18709
+ 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.",
18710
+ deviceType: DeviceType.Fan,
18711
+ outputCap: "switch",
18712
+ outputField: "on",
18713
+ member: {
18714
+ cap: "switch",
18715
+ fieldPath: "on"
18716
+ },
18717
+ commandCaps: ["switch", "fan-control"]
18718
+ }),
18719
+ actuator({
18720
+ id: "cover",
18721
+ 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.",
18722
+ deviceType: DeviceType.Cover,
18723
+ outputCap: "group-active",
18724
+ outputField: "active",
18725
+ member: {
18726
+ cap: "cover",
18727
+ fieldPath: "state"
18728
+ },
18729
+ activeWhen: [
18730
+ "open",
18731
+ "opening",
18732
+ "stopped"
18733
+ ],
18734
+ commandCaps: ["cover"]
18735
+ }),
18736
+ actuator({
18737
+ id: "valve",
18738
+ 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.",
18739
+ deviceType: DeviceType.Valve,
18740
+ outputCap: "group-active",
18741
+ outputField: "active",
18742
+ member: {
18743
+ cap: "valve",
18744
+ fieldPath: "state"
18745
+ },
18746
+ activeWhen: [
18747
+ "open",
18748
+ "opening",
18749
+ "stopped"
18750
+ ],
18751
+ commandCaps: ["valve"]
18752
+ }),
18753
+ actuator({
18754
+ id: "lock",
18755
+ 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.",
18756
+ deviceType: DeviceType.Lock,
18757
+ outputCap: "group-active",
18758
+ outputField: "active",
18759
+ member: {
18760
+ cap: "lock-control",
18761
+ fieldPath: "state"
18762
+ },
18763
+ activeWhen: ["locked"],
18764
+ commandCaps: ["lock-control"]
18765
+ }),
18766
+ actuator({
18767
+ id: "media-player",
18768
+ 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.",
18769
+ deviceType: DeviceType.MediaPlayer,
18770
+ outputCap: "group-active",
18771
+ outputField: "active",
18772
+ member: {
18773
+ cap: "media-player",
18774
+ fieldPath: "state"
18775
+ },
18776
+ activeWhen: ["playing", "buffering"],
18777
+ commandCaps: ["media-player"]
18778
+ }),
18779
+ actuator({
18780
+ id: "climate",
18781
+ 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.",
18782
+ deviceType: DeviceType.Thermostat,
18783
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
18784
+ outputCap: "group-active",
18785
+ outputField: "active",
18786
+ member: {
18787
+ cap: "climate-control",
18788
+ fieldPath: "mode"
18789
+ },
18790
+ activeWhen: [
18791
+ "heat",
18792
+ "cool",
18793
+ "auto",
18794
+ "heat_cool",
18795
+ "fan_only",
18796
+ "dry"
18797
+ ],
18798
+ commandCaps: ["climate-control"]
18799
+ })
18800
+ ];
18801
+ function actuator(input) {
18802
+ return {
18803
+ id: input.id,
18804
+ summary: input.summary,
18805
+ deviceType: input.deviceType,
18806
+ outputCap: input.outputCap,
18807
+ outputField: input.outputField,
18808
+ commandCaps: input.commandCaps,
18809
+ family: "actuator",
18810
+ role: void 0,
18811
+ memberDeviceType: input.deviceType,
18812
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
18813
+ activeWhen: input.activeWhen,
18814
+ quantity: "none",
18815
+ baseUnit: void 0,
18816
+ aggregates: BINARY_AGGREGATES,
18817
+ defaultAggregate: "any",
18818
+ empty: "offline",
18819
+ accept: [input.member],
18820
+ companions: [CHANGED]
18821
+ };
18822
+ }
18823
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18824
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18825
+ DeviceType.Camera;
18100
18826
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18101
18827
  /**
18102
18828
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19923,12 +20649,12 @@ function buildDeviceAdminUrl(parts) {
19923
20649
  * providers detect renames by matching `uniqueId` and patch `id`
19924
20650
  * in-place so dispatch keeps working.
19925
20651
  *
19926
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19927
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19928
- * state push and are the single source of truth. The `device_class` tag is
19929
- * an HA-specific input that is consumed at the discovery boundary
19930
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19931
- * stored after that point.
20652
+ * **Display unit and precision are NOT stored here.** They are a read-time
20653
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20654
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20655
+ * neither is repeated on each sample. The `device_class` tag is an
20656
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20657
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19932
20658
  *
19933
20659
  * The `raw` escape hatch carries provider-specific attributes that don't
19934
20660
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19942,6 +20668,14 @@ var SourceInfoSchema = object({
19942
20668
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19943
20669
  * Used to detect rename when `id` changes. */
19944
20670
  uniqueId: string().optional(),
20671
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20672
+ quantity: ReadingQuantitySchema.optional(),
20673
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20674
+ dateTimeShape: _enum([
20675
+ "date",
20676
+ "time",
20677
+ "datetime"
20678
+ ]).optional(),
19945
20679
  /** Provider-specific extras that don't fit the structured slots.
19946
20680
  * Whitelist what goes here per provider — do NOT dump entire HA
19947
20681
  * attribute blobs (would explode the meta JSON column). */
@@ -41046,7 +41780,8 @@ method(object({
41046
41780
  "enum"
41047
41781
  ]),
41048
41782
  enumValues: array(string()).optional(),
41049
- item: boolean().optional()
41783
+ item: boolean().optional(),
41784
+ quantity: ReadingQuantitySchema.optional()
41050
41785
  })).readonly(),
41051
41786
  itemArray: object({
41052
41787
  path: string(),
@@ -43407,7 +44142,7 @@ var NetworkAccessStatusSchema = object({
43407
44142
  });
43408
44143
  /**
43409
44144
  * Optional, richer endpoint shape returned by providers that expose
43410
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44145
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43411
44146
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43412
44147
  * the originating provider config (mode + sourcePort) so the
43413
44148
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44890,8 +45625,8 @@ var MaskPolygonShapeSchema = object({
44890
45625
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44891
45626
  var MaskGridShapeSchema = object({
44892
45627
  kind: literal("grid"),
44893
- gridWidth: number(),
44894
- gridHeight: number(),
45628
+ gridWidth: number().meta(readingMeta("none")),
45629
+ gridHeight: number().meta(readingMeta("none")),
44895
45630
  cells: array(boolean())
44896
45631
  });
44897
45632
  discriminatedUnion("kind", [
@@ -45003,6 +45738,8 @@ var NcSystemEventKindSchema = _enum([
45003
45738
  "detection-blind",
45004
45739
  "motion-noisy",
45005
45740
  "motion-calm",
45741
+ "package-llm-silent",
45742
+ "package-llm-answered",
45006
45743
  "recording-stalled",
45007
45744
  "recording-healthy",
45008
45745
  "recording-degraded",
@@ -46092,7 +46829,20 @@ var NcRuleInputSchema = object({
46092
46829
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46093
46830
  * path does not run Zod defaults.
46094
46831
  */
46095
- groupingSec: number().int().min(5).max(60).optional()
46832
+ groupingSec: number().int().min(5).max(60).optional(),
46833
+ /**
46834
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46835
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46836
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46837
+ * same mark the Library writes, and records whose notification it was and
46838
+ * when. A channel with no buttons gets the same two choices as links.
46839
+ *
46840
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46841
+ * cap path): absent and `false` both mean off, which is what every rule
46842
+ * authored before this field meant. An editor turns it off by writing
46843
+ * `false`, so it needs no `clear` key.
46844
+ */
46845
+ askVerdict: boolean().optional()
46096
46846
  });
46097
46847
  /**
46098
46848
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -56094,6 +56844,28 @@ var PackageVersionInfoSchema = object({
56094
56844
  deprecated: string().optional(),
56095
56845
  distTags: array(string()).readonly()
56096
56846
  });
56847
+ /**
56848
+ * One published document. `ok` is markdown from the registry. `missing` means
56849
+ * the registry answered and the file was not there. `error` is a registry or
56850
+ * tarball failure — never an empty string standing in for either.
56851
+ */
56852
+ var PackageDocBodySchema = discriminatedUnion("status", [
56853
+ object({
56854
+ status: literal("ok"),
56855
+ markdown: string()
56856
+ }),
56857
+ object({ status: literal("missing") }),
56858
+ object({
56859
+ status: literal("error"),
56860
+ message: string()
56861
+ })
56862
+ ]);
56863
+ var PackageDocsSchema = object({
56864
+ name: string(),
56865
+ version: string(),
56866
+ readme: PackageDocBodySchema,
56867
+ changelog: PackageDocBodySchema
56868
+ });
56097
56869
  var SearchResultSchema = object({
56098
56870
  name: string(),
56099
56871
  version: string(),
@@ -56312,7 +57084,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56312
57084
  }), object({ success: literal(true) }), {
56313
57085
  kind: "mutation",
56314
57086
  auth: "admin"
56315
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57087
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57088
+ name: string().min(1),
57089
+ version: string().min(1).optional()
57090
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56316
57091
  kind: "mutation",
56317
57092
  auth: "admin"
56318
57093
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56371,27 +57146,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56371
57146
  */
56372
57147
  var AirQualitySensorStatusSchema = object({
56373
57148
  /** Carbon dioxide concentration in ppm. */
56374
- co2Ppm: number().min(0).optional(),
57149
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56375
57150
  /** Total volatile organic compounds in ppb. */
56376
- vocPpb: number().min(0).optional(),
57151
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56377
57152
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56378
- pm25: number().min(0).optional(),
57153
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56379
57154
  /** Particulate matter ≤ 10 μm in µg/m³. */
56380
- pm10: number().min(0).optional(),
57155
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56381
57156
  /** Composite AQI value (typically 0..500). */
56382
- aqi: number().optional(),
57157
+ aqi: number().optional().meta(readingMeta("index")),
56383
57158
  /** Ms epoch when the slice was last updated. */
56384
- lastFetchedAt: number(),
56385
- /** Live display unit of the single metric this slice carries (e.g. HA
56386
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56387
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56388
- * per slice is unambiguous. */
56389
- unit: string().optional(),
56390
- /** Suggested decimal places for numeric display.
56391
- * Populated live from the upstream source when provided (e.g. HA
56392
- * `attributes.suggested_display_precision`). Falls back to
56393
- * auto-formatting when absent. */
56394
- precision: number().int().min(0).max(10).optional()
57159
+ lastFetchedAt: number()
56395
57160
  });
56396
57161
  var airQualitySensorCapability = {
56397
57162
  name: "air-quality-sensor",
@@ -56428,12 +57193,12 @@ var airQualitySensorCapability = {
56428
57193
  },
56429
57194
  pm25: {
56430
57195
  kind: "gauge",
56431
- unit: "ug/m3",
57196
+ unit: "µg/m³",
56432
57197
  deadband: 1
56433
57198
  },
56434
57199
  pm10: {
56435
57200
  kind: "gauge",
56436
- unit: "ug/m3",
57201
+ unit: "µg/m³",
56437
57202
  deadband: 1
56438
57203
  },
56439
57204
  aqi: {
@@ -56451,22 +57216,13 @@ var airQualitySensorCapability = {
56451
57216
  * entries with `device_class: illuminance`.
56452
57217
  */
56453
57218
  var AmbientLightSensorStatusSchema = object({
56454
- /** Current illuminance in lux (lx). */
56455
- lux: number().min(0).meta({
56456
- unit: "lx",
57219
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57220
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57221
+ precision: 0,
56457
57222
  label: "Illuminance"
56458
- }),
57223
+ })),
56459
57224
  /** Ms epoch when the slice was last updated. */
56460
- lastFetchedAt: number(),
56461
- /** Live display unit from the upstream source (e.g. HA
56462
- * `attributes.unit_of_measurement`). The UI prefers this over the
56463
- * role's canonical unit. Absent → fall back to the canonical unit. */
56464
- unit: string().optional(),
56465
- /** Suggested decimal places for numeric display.
56466
- * Populated live from the upstream source when provided (e.g. HA
56467
- * `attributes.suggested_display_precision`). Falls back to
56468
- * auto-formatting when absent. */
56469
- precision: number().int().min(0).max(10).optional()
57225
+ lastFetchedAt: number()
56470
57226
  });
56471
57227
  var ambientLightSensorCapability = {
56472
57228
  name: "ambient-light-sensor",
@@ -56498,12 +57254,7 @@ var ambientLightSensorCapability = {
56498
57254
  } } },
56499
57255
  /** Clock fields: written, but excluded from the compare that decides
56500
57256
  * whether persisting is worth a SQLite commit. */
56501
- volatileStateFields: ["lastFetchedAt"],
56502
- customize: {
56503
- version: 1,
56504
- advanced: "derive",
56505
- targets: []
56506
- }
57257
+ volatileStateFields: ["lastFetchedAt"]
56507
57258
  };
56508
57259
  /**
56509
57260
  * Per-class audio metrics aggregated over a sliding window.
@@ -56730,10 +57481,7 @@ var BatteryStatusSchema = object({
56730
57481
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56731
57482
  * declines to band a non-finite reading, which is the correct shape.
56732
57483
  */
56733
- percentage: number().min(0).max(100).nullable().meta({
56734
- unit: "%",
56735
- label: "Charge"
56736
- }),
57484
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56737
57485
  /**
56738
57486
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56739
57487
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56760,7 +57508,14 @@ var BatteryStatusSchema = object({
56760
57508
  */
56761
57509
  sleeping: boolean().meta({ label: "Sleeping" }),
56762
57510
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56763
- lastUpdated: number(),
57511
+ lastUpdated: number().meta(readingMeta("none")),
57512
+ /**
57513
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57514
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57515
+ * that only report charge. A second reading on this cap, not a second
57516
+ * `numeric-sensor` on the same device.
57517
+ */
57518
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56764
57519
  /**
56765
57520
  * Ms epoch of the last time the device PROVED it was reachable — a
56766
57521
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56807,12 +57562,12 @@ var BATTERY_DEVICE_TYPES = [
56807
57562
  DeviceType.Switch,
56808
57563
  DeviceType.PetFeeder,
56809
57564
  DeviceType.Container,
56810
- DeviceType.Thermostat
57565
+ DeviceType.Thermostat,
57566
+ DeviceType.EventEmitter
56811
57567
  ];
56812
57568
  /** Generic Customize target for the percentage reading (D936). */
56813
57569
  var batteryCustomize = {
56814
57570
  version: 1,
56815
- advanced: "derive",
56816
57571
  targets: [{
56817
57572
  id: "percentage",
56818
57573
  cardinality: "scalar",
@@ -56821,7 +57576,7 @@ var batteryCustomize = {
56821
57576
  stateField: "percentage"
56822
57577
  },
56823
57578
  deviceTypes: BATTERY_DEVICE_TYPES,
56824
- apply: "add-or-replace",
57579
+ apply: "add",
56825
57580
  path: "percentage",
56826
57581
  sources: [{
56827
57582
  kind: "field",
@@ -56942,10 +57697,16 @@ onStatusChanged: { data: object({
56942
57697
  /**
56943
57698
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
56944
57699
  */
56945
- history: { fields: { percentage: {
56946
- kind: "gauge",
56947
- unit: "%"
56948
- } } },
57700
+ history: { fields: {
57701
+ percentage: {
57702
+ kind: "gauge",
57703
+ unit: "%"
57704
+ },
57705
+ volts: {
57706
+ kind: "gauge",
57707
+ unit: "V"
57708
+ }
57709
+ } },
56949
57710
  customize: batteryCustomize,
56950
57711
  /** Clock fields: written, but excluded from the compare that decides
56951
57712
  * whether persisting is worth a SQLite commit. */
@@ -56992,12 +57753,7 @@ var binaryCapability = {
56992
57753
  history: { fields: { on: { kind: "boolean" } } },
56993
57754
  /** Clock fields: written, but excluded from the compare that decides
56994
57755
  * whether persisting is worth a SQLite commit. */
56995
- volatileStateFields: ["lastChangedAt"],
56996
- customize: {
56997
- version: 1,
56998
- advanced: "derive",
56999
- targets: []
57000
- }
57756
+ volatileStateFields: ["lastChangedAt"]
57001
57757
  };
57002
57758
  /**
57003
57759
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57013,7 +57769,7 @@ var binaryCapability = {
57013
57769
  */
57014
57770
  var BrightnessStatusSchema = object({
57015
57771
  /** Current level as 0..100 inclusive. Firmware-reported. */
57016
- percentage: number().min(0).max(100),
57772
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57017
57773
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57018
57774
  lastChangedAt: number()
57019
57775
  });
@@ -57484,21 +58240,21 @@ var ClimateControlStatusSchema = object({
57484
58240
  /** Single setpoint in Celsius. Used by single-target modes
57485
58241
  * (heat / cool / dry). Null when the device is in a range mode
57486
58242
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57487
- target: number().nullable(),
58243
+ target: number().nullable().meta(readingMeta("temperature")),
57488
58244
  /** Upper bound of the dual setpoint in Celsius. Populated only
57489
58245
  * in `heat_cool` mode. */
57490
- targetHigh: number().nullable(),
58246
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57491
58247
  /** Lower bound of the dual setpoint in Celsius. Populated only
57492
58248
  * in `heat_cool` mode. */
57493
- targetLow: number().nullable(),
58249
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57494
58250
  /** Target relative humidity (0..100). Null when the device has no
57495
58251
  * humidity control. */
57496
- targetHumidity: number().min(0).max(100).nullable(),
58252
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57497
58253
  /** Current measured temperature in Celsius. Read-only — pushed by
57498
58254
  * the device's internal sensor. */
57499
- currentTemp: number().nullable(),
58255
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57500
58256
  /** Current measured relative humidity (0..100). Read-only. */
57501
- currentHumidity: number().min(0).max(100).nullable(),
58257
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57502
58258
  /** Vertical louver swing on/off. Null when the device has no vertical
57503
58259
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57504
58260
  swingVertical: boolean().nullable(),
@@ -57506,11 +58262,11 @@ var ClimateControlStatusSchema = object({
57506
58262
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57507
58263
  swingHorizontal: boolean().nullable(),
57508
58264
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57509
- minTemp: number().nullable().optional(),
58265
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57510
58266
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57511
- maxTemp: number().nullable().optional(),
58267
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57512
58268
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57513
- tempStep: number().positive().nullable().optional(),
58269
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57514
58270
  /** Ms epoch when the slice was last updated (push or command). */
57515
58271
  lastFetchedAt: number()
57516
58272
  });
@@ -57652,17 +58408,17 @@ var climateControlCapability = {
57652
58408
  * modes to expose in the picker without a slice fetch.
57653
58409
  */
57654
58410
  var RgbTripletSchema = object({
57655
- r: number().int().min(0).max(255),
57656
- g: number().int().min(0).max(255),
57657
- b: number().int().min(0).max(255)
58411
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58412
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58413
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57658
58414
  });
57659
58415
  var HsvTripletSchema = object({
57660
58416
  /** Hue in degrees, 0..360. */
57661
- h: number().min(0).max(360),
58417
+ h: number().min(0).max(360).meta(readingMeta("none")),
57662
58418
  /** Saturation as 0..100 inclusive. */
57663
- s: number().min(0).max(100),
58419
+ s: number().min(0).max(100).meta(readingMeta("none")),
57664
58420
  /** Value/brightness as 0..100 inclusive. */
57665
- v: number().min(0).max(100)
58421
+ v: number().min(0).max(100).meta(readingMeta("none"))
57666
58422
  });
57667
58423
  var ColorInputSchema = discriminatedUnion("mode", [
57668
58424
  object({
@@ -57691,7 +58447,7 @@ var ColorStatusSchema = object({
57691
58447
  /** Populated when `mode === 'hsv'`. */
57692
58448
  hsv: HsvTripletSchema.optional(),
57693
58449
  /** Populated when `mode === 'mired'`. */
57694
- mireds: number().int().optional(),
58450
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57695
58451
  /** Ms epoch of the last operator-driven change. */
57696
58452
  lastChangedAt: number()
57697
58453
  });
@@ -57886,18 +58642,12 @@ _enum([
57886
58642
  "empty",
57887
58643
  "full"
57888
58644
  ]);
57889
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57890
- * each. */
57891
- var ConsumableRemainingUnitSchema = _enum([
57892
- "days",
57893
- "hours",
57894
- "cycles"
57895
- ]);
57896
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57897
- * days). `value` null = unknown (D393). */
58645
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58646
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58647
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57898
58648
  var ConsumableRemainingSchema = object({
57899
- value: number().min(0).nullable(),
57900
- unit: ConsumableRemainingUnitSchema
58649
+ value: number().min(0).nullable().meta(readingMeta("device")),
58650
+ kind: ConsumableRemainingKindSchema
57901
58651
  });
57902
58652
  /** A single consumable item. Either a continuous `level` (remaining
57903
58653
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57910,7 +58660,7 @@ var ConsumableItemSchema = object({
57910
58660
  /** Display name. */
57911
58661
  label: string().min(1),
57912
58662
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57913
- level: number().min(0).max(100).nullable(),
58663
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57914
58664
  /** The provider's discrete verdict when known (binary mode). */
57915
58665
  status: ConsumableStatusSchema.nullable(),
57916
58666
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57992,8 +58742,8 @@ var consumablesCustomize = {
57992
58742
  transforms: [{ kind: "identity" }]
57993
58743
  }],
57994
58744
  literals: [{
57995
- path: "remaining.unit",
57996
- values: [...ConsumableRemainingUnitSchema.options]
58745
+ path: "remaining.kind",
58746
+ values: [...ConsumableRemainingKindSchema.options]
57997
58747
  }]
57998
58748
  },
57999
58749
  {
@@ -58122,7 +58872,6 @@ reset: method(object({
58122
58872
  },
58123
58873
  "remaining.value": {
58124
58874
  kind: "gauge",
58125
- unitFrom: "remaining.unit",
58126
58875
  deadband: 1,
58127
58876
  displayName: "Remaining"
58128
58877
  }
@@ -58192,10 +58941,9 @@ var contactCapability = {
58192
58941
  * (empty array for the other kinds). It lives in the slice because
58193
58942
  * HA `select.*` entities may update their options at runtime.
58194
58943
  *
58195
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58196
- * `kind === 'numeric'` and are absent (undefined) for all other
58197
- * kinds. They are populated live from HA attributes on each state
58198
- * push — consistent with `options` and the sensor-unit approach.
58944
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58945
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58946
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58199
58947
  */
58200
58948
  var ControlStatusSchema = object({
58201
58949
  kind: _enum([
@@ -58207,39 +58955,17 @@ var ControlStatusSchema = object({
58207
58955
  /** Current value. Numeric kind → number; select/text/datetime → string.
58208
58956
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58209
58957
  * or time-only — HA accepts all three forms). */
58210
- value: union([number(), string()]),
58958
+ value: union([number().meta(readingMeta("device")), string()]),
58211
58959
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58212
58960
  options: array(string()),
58213
58961
  /** Ms epoch when the slice was last updated. */
58214
58962
  lastChangedAt: number(),
58215
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58216
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58217
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58218
- unit: string().optional(),
58219
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58220
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58221
- min: number().optional(),
58222
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58223
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58224
- max: number().optional(),
58225
- /** Step increment. Populated live from HA `attributes.step`.
58226
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58227
- step: number().optional(),
58228
- /** Suggested decimal places for numeric display. Populated live from HA
58229
- * `attributes.suggested_display_precision`, or derived from `step` when the
58230
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58231
- * other kinds. Falls back to auto-formatting when absent. */
58232
- precision: number().int().min(0).max(10).optional(),
58233
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58234
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58235
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58236
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58237
- * `has_date`/`has_time` attributes on each push. */
58238
- format: _enum([
58239
- "date",
58240
- "time",
58241
- "datetime"
58242
- ]).optional()
58963
+ /** Minimum allowed value, in the device quantity's base unit. */
58964
+ min: number().optional().meta(readingMeta("device")),
58965
+ /** Maximum allowed value, in the device quantity's base unit. */
58966
+ max: number().optional().meta(readingMeta("device")),
58967
+ /** Step increment, in the device quantity's base unit. */
58968
+ step: number().optional().meta(readingMeta("device"))
58243
58969
  });
58244
58970
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58245
58971
  object({
@@ -58305,9 +59031,9 @@ var CoverStatusSchema = object({
58305
59031
  ]),
58306
59032
  /** 0 = fully closed, 100 = fully open. Null when the device has
58307
59033
  * no intermediate position surface. */
58308
- position: number().min(0).max(100).nullable(),
59034
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58309
59035
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58310
- tiltPosition: number().min(0).max(100).nullable(),
59036
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58311
59037
  /** Ms epoch when the slice was last updated. */
58312
59038
  lastChangedAt: number()
58313
59039
  });
@@ -58410,9 +59136,9 @@ var NormalizedRangeSchema$1 = object({
58410
59136
  var DayNightStatusSchema = object({
58411
59137
  mode: DayNightModeSchema,
58412
59138
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58413
- sensitivity: number().optional(),
59139
+ sensitivity: number().optional().meta(readingMeta("none")),
58414
59140
  /** Delay before the IR-cut filter flips, in seconds. */
58415
- switchDelaySec: number().optional(),
59141
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58416
59142
  lastFetchedAt: number()
58417
59143
  });
58418
59144
  /**
@@ -58630,11 +59356,11 @@ var DishwasherStatusSchema = object({
58630
59356
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58631
59357
  availablePrograms: array(string()),
58632
59358
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58633
- remainingSeconds: number().int().nonnegative().nullable(),
59359
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58634
59360
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58635
- delayedStartSeconds: number().int().nonnegative().nullable(),
59361
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58636
59362
  /** Cycle completion 0..100, or null when not applicable. */
58637
- progressPercent: number().min(0).max(100).nullable(),
59363
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58638
59364
  /** True when the door is reported open; null when the door state is not known. */
58639
59365
  doorOpen: boolean().nullable(),
58640
59366
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58658,7 +59384,7 @@ var DishwasherStatusSchema = object({
58658
59384
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58659
59385
  machineCareDue: boolean().nullable(),
58660
59386
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58661
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59387
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58662
59388
  /** Option states: null when the appliance does not expose the option or has not said. */
58663
59389
  halfLoad: boolean().nullable(),
58664
59390
  hygienePlus: boolean().nullable(),
@@ -58673,7 +59399,7 @@ var DishwasherStatusSchema = object({
58673
59399
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58674
59400
  activeEvents: array(string()),
58675
59401
  /** Numeric error code when the appliance reports one; null otherwise. */
58676
- errorCode: number().nullable(),
59402
+ errorCode: number().nullable().meta(readingMeta("none")),
58677
59403
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58678
59404
  errorLabel: string().nullable(),
58679
59405
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58904,7 +59630,7 @@ var DoorbellStatusSchema = object({
58904
59630
  /** Ms epoch of the last press. null = never observed since this provider started. */
58905
59631
  lastPressedAt: number().nullable(),
58906
59632
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58907
- pressCountSinceStart: number()
59633
+ pressCountSinceStart: number().meta(readingMeta("count"))
58908
59634
  });
58909
59635
  var DoorbellPressEventSchema = object({
58910
59636
  deviceId: number(),
@@ -58969,17 +59695,7 @@ onPressed: {
58969
59695
  history: "none",
58970
59696
  customize: doorbellCustomize
58971
59697
  };
58972
- /**
58973
- * Enum-state sensor — a string value picked from a finite option set.
58974
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
58975
- * states, weather conditions, contact-source channels, mode strings).
58976
- * The option set is stable across observations; it is persisted in the
58977
- * device config blob (via the control slice at adoption) and is NOT
58978
- * stored in `sourceInfo`.
58979
- */
58980
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
58981
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
58982
- var EnumSensorDateTimeFormatSchema = _enum([
59698
+ _enum([
58983
59699
  "date",
58984
59700
  "time",
58985
59701
  "datetime"
@@ -58987,12 +59703,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
58987
59703
  var EnumSensorStatusSchema = object({
58988
59704
  value: string(),
58989
59705
  /**
58990
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
58991
- * as a locale date/time/datetime; absent for plain enum sensors. HA
58992
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
58993
- * a time-only sensor → `'time'`.
59706
+ * Set when `value` is a timestamp rather than an enum state, so history
59707
+ * does not learn ISO strings as states. The display shape (date / time /
59708
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
58994
59709
  */
58995
- format: EnumSensorDateTimeFormatSchema.optional(),
59710
+ timestamp: boolean().optional(),
58996
59711
  /** Ms epoch when the slice was last updated. */
58997
59712
  lastFetchedAt: number()
58998
59713
  });
@@ -59019,11 +59734,11 @@ var enumSensorCapability = {
59019
59734
  * History (D850): the value as an `enum` series (an appliance's program or
59020
59735
  * operation state, an HVAC action). The option set is per device, so the
59021
59736
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59022
- * `format` carries a timestamp, not a state, and is not recorded.
59737
+ * `timestamp` carries a clock, not a state, and is not recorded.
59023
59738
  */
59024
59739
  history: { fields: { value: {
59025
59740
  kind: "enum",
59026
- skipWhenSet: "format"
59741
+ skipWhenSet: "timestamp"
59027
59742
  } } },
59028
59743
  /** Clock fields: written, but excluded from the compare that decides
59029
59744
  * whether persisting is worth a SQLite commit. */
@@ -59043,13 +59758,13 @@ var EventFireSchema = object({
59043
59758
  deviceId: number(),
59044
59759
  eventType: string(),
59045
59760
  data: record(string(), unknown()).nullable(),
59046
- timestamp: number(),
59047
- seq: number()
59761
+ timestamp: number().meta(readingMeta("none")),
59762
+ seq: number().meta(readingMeta("count"))
59048
59763
  });
59049
59764
  var EventEmitterStatusSchema = object({
59050
59765
  eventTypes: array(string()),
59051
59766
  lastEvent: EventFireSchema.nullable(),
59052
- eventCountSinceStart: number()
59767
+ eventCountSinceStart: number().meta(readingMeta("count"))
59053
59768
  });
59054
59769
  var eventEmitterCapability = {
59055
59770
  name: "event-emitter",
@@ -59462,10 +60177,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59462
60177
  var FanControlStatusSchema = object({
59463
60178
  /** Active speed as 0..100 inclusive. Null when the device has no
59464
60179
  * speed surface (single-speed fan). */
59465
- percentage: number().min(0).max(100).nullable(),
60180
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59466
60181
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59467
60182
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59468
- percentageStep: number().positive().optional(),
60183
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59469
60184
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59470
60185
  * — empty string when no preset is active or supported. */
59471
60186
  preset: string(),
@@ -59586,7 +60301,7 @@ var FeatureProbeStatusSchema = object({
59586
60301
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59587
60302
  model: string().nullable(),
59588
60303
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59589
- channelCount: number().nullable(),
60304
+ channelCount: number().nullable().meta(readingMeta("count")),
59590
60305
  /**
59591
60306
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59592
60307
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59709,9 +60424,151 @@ var gasCapability = {
59709
60424
  * whether persisting is worth a SQLite commit. */
59710
60425
  volatileStateFields: ["lastChangedAt"]
59711
60426
  };
60427
+ var GroupCommandsSchema = object({
60428
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60429
+ results: array(object({
60430
+ deviceId: number().int().nonnegative(),
60431
+ cap: string().min(1),
60432
+ method: string().min(1),
60433
+ ok: boolean(),
60434
+ error: string().optional(),
60435
+ /** Ms epoch of the attempt. */
60436
+ at: number()
60437
+ })) });
60438
+ var groupCommandsCapability = {
60439
+ name: "group-commands",
60440
+ scope: "device",
60441
+ deviceNative: true,
60442
+ mode: "singleton",
60443
+ deviceTypes: [
60444
+ DeviceType.Switch,
60445
+ DeviceType.Light,
60446
+ DeviceType.Fan,
60447
+ DeviceType.Cover,
60448
+ DeviceType.Valve,
60449
+ DeviceType.Lock,
60450
+ DeviceType.MediaPlayer,
60451
+ DeviceType.Thermostat,
60452
+ DeviceType.Climate
60453
+ ],
60454
+ methods: {},
60455
+ status: {
60456
+ schema: GroupCommandsSchema,
60457
+ kind: "push"
60458
+ },
60459
+ runtimeState: GroupCommandsSchema,
60460
+ /**
60461
+ * Runtime-state durability: **session** — the last attempt. A restored
60462
+ * failure would outlive the command that caused it.
60463
+ *
60464
+ * See `RuntimeStateDurability`. Enforced by
60465
+ * `scripts/check-runtime-state-durability.ts`.
60466
+ */
60467
+ durability: "session",
60468
+ history: "none"
60469
+ };
60470
+ /**
60471
+ * Whether a helper actuator group counts as active (D944): at least one
60472
+ * included member is, or every included member is. What "active" means is the
60473
+ * type's: a cover is open, a lock is locked, a media player is playing, a
60474
+ * climate device is not off. A group of lights, switches and fans reports
60475
+ * `switch.on` instead; this cap is for the types whose own state is not a
60476
+ * yes/no. Unknown members are left out (`group-status` counts them) and an
60477
+ * empty included set takes the device offline, never `active: false`.
60478
+ */
60479
+ var GroupActiveSchema = object({
60480
+ active: boolean().meta({ label: "Active" }),
60481
+ /** Ms epoch when the slice was last updated. */
60482
+ lastChangedAt: number()
60483
+ });
60484
+ var groupActiveCapability = {
60485
+ name: "group-active",
60486
+ scope: "device",
60487
+ deviceNative: true,
60488
+ mode: "singleton",
60489
+ deviceTypes: [
60490
+ DeviceType.Cover,
60491
+ DeviceType.Valve,
60492
+ DeviceType.Lock,
60493
+ DeviceType.MediaPlayer,
60494
+ DeviceType.Climate,
60495
+ DeviceType.Thermostat
60496
+ ],
60497
+ methods: {},
60498
+ status: {
60499
+ schema: GroupActiveSchema,
60500
+ kind: "push"
60501
+ },
60502
+ runtimeState: GroupActiveSchema,
60503
+ /**
60504
+ * Runtime-state durability: **session** — recomputed from the members on the
60505
+ * next source event; a restored value would outlive the members it named.
60506
+ *
60507
+ * See `RuntimeStateDurability`. Enforced by
60508
+ * `scripts/check-runtime-state-durability.ts`.
60509
+ */
60510
+ durability: "session",
60511
+ history: "none"
60512
+ };
60513
+ /**
60514
+ * How many members of a helper group answered (D944). Counts are integers,
60515
+ * not measurements: zero included is a real count, never a stand-in reading.
60516
+ * The reading itself stays on the output capability. History is none — the
60517
+ * series that later statistics read is the output cap's own history block.
60518
+ */
60519
+ var GroupStatusSchema = object({
60520
+ /** Members whose current value entered the aggregate. */
60521
+ included: number().int().nonnegative().meta(readingMeta("count", {
60522
+ precision: 0,
60523
+ label: "Reporting"
60524
+ })),
60525
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60526
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60527
+ precision: 0,
60528
+ label: "Excluded"
60529
+ })),
60530
+ /** `included` + `excluded`. */
60531
+ total: number().int().nonnegative().meta(readingMeta("count", {
60532
+ precision: 0,
60533
+ label: "Members"
60534
+ }))
60535
+ });
60536
+ var groupStatusCapability = {
60537
+ name: "group-status",
60538
+ scope: "device",
60539
+ deviceNative: true,
60540
+ mode: "singleton",
60541
+ deviceTypes: [
60542
+ DeviceType.Sensor,
60543
+ DeviceType.Switch,
60544
+ DeviceType.Light,
60545
+ DeviceType.Fan,
60546
+ DeviceType.Cover,
60547
+ DeviceType.Valve,
60548
+ DeviceType.Lock,
60549
+ DeviceType.MediaPlayer,
60550
+ DeviceType.Thermostat,
60551
+ DeviceType.Climate
60552
+ ],
60553
+ methods: {},
60554
+ status: {
60555
+ schema: GroupStatusSchema,
60556
+ kind: "push"
60557
+ },
60558
+ runtimeState: GroupStatusSchema,
60559
+ /**
60560
+ * Runtime-state durability: **session** — recomputed from members on the
60561
+ * next source event; a restored count would outlive the readings it named.
60562
+ *
60563
+ * See `RuntimeStateDurability`. Enforced by
60564
+ * `scripts/check-runtime-state-durability.ts`.
60565
+ */
60566
+ durability: "session",
60567
+ history: "none"
60568
+ };
59712
60569
  var HeatingValveStatusSchema = object({
59713
60570
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59714
- position: number().min(0).max(100).nullable(),
60571
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59715
60572
  /** Ms epoch when the slice was last updated (push or poll). */
59716
60573
  lastFetchedAt: number()
59717
60574
  });
@@ -59754,9 +60611,9 @@ var HumidifierStatusSchema = object({
59754
60611
  /** Whether the humidifier is currently on. */
59755
60612
  on: boolean(),
59756
60613
  /** Current measured relative humidity (0..100). Null when not reported. */
59757
- currentHumidity: number().min(0).max(100).nullable(),
60614
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59758
60615
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59759
- targetHumidity: number().min(0).max(100).nullable(),
60616
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59760
60617
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59761
60618
  * device has no mode surface. */
59762
60619
  mode: string().nullable(),
@@ -59766,9 +60623,9 @@ var HumidifierStatusSchema = object({
59766
60623
  * `idle` / `off`). Null when the device doesn't report it. */
59767
60624
  action: string().nullable(),
59768
60625
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59769
- minHumidity: number().nullable(),
60626
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59770
60627
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59771
- maxHumidity: number().nullable(),
60628
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59772
60629
  /** Ms epoch when the slice was last updated. */
59773
60630
  lastChangedAt: number()
59774
60631
  });
@@ -59838,22 +60695,13 @@ var humidifierCapability = {
59838
60695
  * `sourceInfo`.
59839
60696
  */
59840
60697
  var HumiditySensorStatusSchema = object({
59841
- /** Current relative humidity, 0..100. */
59842
- percent: number().min(0).max(100).meta({
59843
- unit: "%",
60698
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60699
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60700
+ precision: 0,
59844
60701
  label: "Humidity"
59845
- }),
60702
+ })),
59846
60703
  /** Ms epoch when the slice was last updated. */
59847
- lastFetchedAt: number(),
59848
- /** Live display unit from the upstream source (e.g. HA
59849
- * `attributes.unit_of_measurement`). The UI prefers this over the
59850
- * role's canonical unit. Absent → fall back to the canonical unit. */
59851
- unit: string().optional(),
59852
- /** Suggested decimal places for numeric display.
59853
- * Populated live from the upstream source when provided (e.g. HA
59854
- * `attributes.suggested_display_precision`). Falls back to
59855
- * auto-formatting when absent. */
59856
- precision: number().int().min(0).max(10).optional()
60704
+ lastFetchedAt: number()
59857
60705
  });
59858
60706
  var humiditySensorCapability = {
59859
60707
  name: "humidity-sensor",
@@ -59884,12 +60732,7 @@ var humiditySensorCapability = {
59884
60732
  } } },
59885
60733
  /** Clock fields: written, but excluded from the compare that decides
59886
60734
  * whether persisting is worth a SQLite commit. */
59887
- volatileStateFields: ["lastFetchedAt"],
59888
- customize: {
59889
- version: 1,
59890
- advanced: "derive",
59891
- targets: []
59892
- }
60735
+ volatileStateFields: ["lastFetchedAt"]
59893
60736
  };
59894
60737
  /**
59895
60738
  * Image display cap. Models a single still image exposed by an integration —
@@ -59910,7 +60753,7 @@ var ImageStatusSchema = object({
59910
60753
  * entity exposes no `entity_picture` (yet). */
59911
60754
  url: string().nullable(),
59912
60755
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59913
- lastUpdated: number().nullable()
60756
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59914
60757
  });
59915
60758
  var imageCapability = {
59916
60759
  name: "image",
@@ -59998,19 +60841,19 @@ var NormalizedRangeSchema = object({
59998
60841
  */
59999
60842
  var ImageSettingsStatusSchema = object({
60000
60843
  /** Normalized 0–100. */
60001
- brightness: number().optional(),
60844
+ brightness: number().optional().meta(readingMeta("none")),
60002
60845
  /** Normalized 0–100. */
60003
- contrast: number().optional(),
60846
+ contrast: number().optional().meta(readingMeta("none")),
60004
60847
  /** Normalized 0–100. */
60005
- saturation: number().optional(),
60848
+ saturation: number().optional().meta(readingMeta("none")),
60006
60849
  /** Normalized 0–100. */
60007
- sharpness: number().optional(),
60850
+ sharpness: number().optional().meta(readingMeta("none")),
60008
60851
  mirror: boolean().optional(),
60009
60852
  flip: boolean().optional(),
60010
60853
  rotate: ImageRotateSchema.optional(),
60011
60854
  whiteBalance: WhiteBalanceModeSchema.optional(),
60012
60855
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60013
- warmth: number().optional(),
60856
+ warmth: number().optional().meta(readingMeta("none")),
60014
60857
  exposureMode: ExposureModeSchema.optional(),
60015
60858
  backlightMode: BacklightModeSchema.optional(),
60016
60859
  lastFetchedAt: number()
@@ -60289,10 +61132,10 @@ var IntercomAbilitySchema = object({
60289
61132
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60290
61133
  codecs: array(string()),
60291
61134
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60292
- sampleRate: number(),
61135
+ sampleRate: number().meta(readingMeta("none")),
60293
61136
  duplex: _enum(["half", "full"]),
60294
61137
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60295
- maxBacklogMs: number()
61138
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60296
61139
  });
60297
61140
  var IntercomStatusSchema = object({
60298
61141
  /** True while a talk session is open. */
@@ -60519,13 +61362,13 @@ var LawnMowerControlStatusSchema = object({
60519
61362
  activity: LawnMowerActivitySchema,
60520
61363
  /** 0..100 battery percentage. Null when the device has no battery
60521
61364
  * reading. */
60522
- batteryLevel: number().min(0).max(100).nullable(),
61365
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60523
61366
  /** 0..100 mowing-completion percentage of the current task, or null when no
60524
61367
  * task is active / progress is unavailable. */
60525
- progressPercent: number().min(0).max(100).nullable(),
61368
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60526
61369
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60527
61370
  * or null when unknown. */
60528
- currentCode: number().nullable(),
61371
+ currentCode: number().nullable().meta(readingMeta("none")),
60529
61372
  /** Human label for {@link currentCode}, or null when undecodable. */
60530
61373
  currentCodeLabel: string().nullable(),
60531
61374
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -60978,7 +61821,14 @@ var lockControlCapability = {
60978
61821
  }),
60979
61822
  unlock: method(object({
60980
61823
  deviceId: number().int().nonnegative(),
60981
- code: string().min(1).optional()
61824
+ code: string().min(1).optional(),
61825
+ /**
61826
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
61827
+ * with one call is refused unless the caller says it was confirmed. A
61828
+ * single lock ignores it. The refusal lives in the callee, so no
61829
+ * automation or API caller reaches the doors by skipping a dialog.
61830
+ */
61831
+ confirm: boolean().optional()
60982
61832
  }), _void(), {
60983
61833
  kind: "mutation",
60984
61834
  auth: "protected"
@@ -61015,6 +61865,44 @@ var lockControlCapability = {
61015
61865
  volatileStateFields: ["lastChangedAt"]
61016
61866
  };
61017
61867
  /**
61868
+ * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
61869
+ * so dashboards keep one series; every other instantaneous reading lands here.
61870
+ * `value` is the time integral in the source quantity's accumulated base
61871
+ * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
61872
+ */
61873
+ var MeterStatusSchema = object({
61874
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
61875
+ lastFetchedAt: number()
61876
+ });
61877
+ var meterCapability = {
61878
+ name: "meter",
61879
+ scope: "device",
61880
+ deviceNative: true,
61881
+ mode: "singleton",
61882
+ deviceTypes: [DeviceType.Sensor],
61883
+ methods: {},
61884
+ status: {
61885
+ schema: MeterStatusSchema,
61886
+ kind: "push"
61887
+ },
61888
+ runtimeState: MeterStatusSchema,
61889
+ /**
61890
+ * Runtime-state durability: **restored** — the total is a cache of the
61891
+ * history integral. A restart shows it only until the history pass
61892
+ * recomputes the open period; it is not an in-memory buffer.
61893
+ *
61894
+ * See `RuntimeStateDurability`. Enforced by
61895
+ * `scripts/check-runtime-state-durability.ts`.
61896
+ */
61897
+ durability: "restored",
61898
+ history: { fields: { value: {
61899
+ kind: "counter",
61900
+ deadband: .001,
61901
+ minIntervalMs: 6e4
61902
+ } } },
61903
+ volatileStateFields: ["lastFetchedAt"]
61904
+ };
61905
+ /**
61018
61906
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
61019
61907
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
61020
61908
  * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
@@ -61058,7 +61946,7 @@ var MediaPlayerStatusSchema = object({
61058
61946
  state: MediaPlayerStateSchema,
61059
61947
  /** Volume as 0..100 inclusive. Null when the device has no volume
61060
61948
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61061
- volumeLevel: number().min(0).max(100).nullable(),
61949
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61062
61950
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61063
61951
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61064
61952
  isMuted: boolean().nullable(),
@@ -61071,10 +61959,10 @@ var MediaPlayerStatusSchema = object({
61071
61959
  currentMedia: MediaInfoSchema.nullable(),
61072
61960
  /** Current playback position in ms. Null when not seekable or
61073
61961
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61074
- positionMs: number().int().nonnegative().nullable(),
61962
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61075
61963
  /** Total duration of the current media in ms. Null when unknown
61076
61964
  * (live stream) or nothing is playing. */
61077
- durationMs: number().int().nonnegative().nullable(),
61965
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61078
61966
  /** Shuffle toggle. Null when no shuffle surface.
61079
61967
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61080
61968
  shuffle: boolean().nullable(),
@@ -61383,7 +62271,7 @@ var motionCustomize = {
61383
62271
  eventName: "onMotionChanged"
61384
62272
  },
61385
62273
  deviceTypes: [DeviceType.Camera],
61386
- apply: "replace",
62274
+ apply: "add",
61387
62275
  applyAuthority: {
61388
62276
  kind: "camera-sensors",
61389
62277
  timeoutField: "motionCooldownMs"
@@ -61455,7 +62343,7 @@ var MotionStatusSchema = object({
61455
62343
  * it, every provider seeded it at boot, and every session left it
61456
62344
  * `detected: true` after its detach.
61457
62345
  */
61458
- autoClearAfterMs: number().nullable()
62346
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61459
62347
  });
61460
62348
  /**
61461
62349
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61492,12 +62380,18 @@ var motionCapability = {
61492
62380
  methods: {
61493
62381
  /**
61494
62382
  * Pull the current motion state synchronously. Convenience shortcut
61495
- * for consumers that don't need the full status object; equivalent
61496
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61497
- * `getStatus` once the auto-injected status surface lands in every
61498
- * consumer.
62383
+ * for consumers that don't need the full status object. A composed
62384
+ * group answers from its slice (D944): unknown throws, it is not off.
61499
62385
  */
61500
62386
  isDetected: method(object({ deviceId: number() }), boolean()) },
62387
+ /**
62388
+ * A composed motion group serves `isDetected` from `detected` (D944).
62389
+ * Without this the cap stays uncomposable (`has-commands`).
62390
+ */
62391
+ composedMethods: { isDetected: {
62392
+ kind: "slice",
62393
+ field: "detected"
62394
+ } },
61501
62395
  events: {
61502
62396
  /**
61503
62397
  * Fires every time the runner transitions a camera between
@@ -61637,7 +62531,7 @@ var MotionZoneRegionSchema = object({
61637
62531
  * the grid region(s). */
61638
62532
  var MotionZoneStatusSchema = object({
61639
62533
  enabled: boolean(),
61640
- sensitivity: number(),
62534
+ sensitivity: number().meta(readingMeta("none")),
61641
62535
  /** Grid region(s). Today exactly one `grid` shape. */
61642
62536
  regions: array(MotionZoneRegionSchema),
61643
62537
  lastFetchedAt: number()
@@ -62007,7 +62901,7 @@ var NavigationStatusSchema = object({
62007
62901
  following: boolean(),
62008
62902
  flash: boolean(),
62009
62903
  lightMode: NavigationLightModeSchema,
62010
- lightLevel: number().min(40).max(100),
62904
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62011
62905
  /** Ms epoch when the slice was last updated. */
62012
62906
  lastChangedAt: number()
62013
62907
  });
@@ -62141,13 +63035,13 @@ var NetworkLinkStatusSchema = object({
62141
63035
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62142
63036
  * SKIP a null rather than coerce it.
62143
63037
  */
62144
- signalPercent: number().min(0).max(100).nullable(),
63038
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62145
63039
  /** Raw received signal strength in dBm, when the firmware reports one. */
62146
- rssiDbm: number().optional(),
63040
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62147
63041
  /** Network name of a wireless link, when the firmware reports it. */
62148
63042
  ssid: string().optional(),
62149
63043
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62150
- lastUpdated: number()
63044
+ lastUpdated: number().meta(readingMeta("none"))
62151
63045
  });
62152
63046
  var networkLinkCapability = {
62153
63047
  name: "network-link",
@@ -62532,7 +63426,7 @@ var NotifierStatusSchema = object({
62532
63426
  lastError: string().nullable(),
62533
63427
  /** Number of deliveries currently buffered server-side (rate-limited
62534
63428
  * notifiers may queue). 0 when the provider sends synchronously. */
62535
- queueDepth: number().int().nonnegative(),
63429
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62536
63430
  /** Per-feature capability matrix — authoritative source for the
62537
63431
  * compose-form field gating. */
62538
63432
  supports: NotifierSupportsSchema
@@ -62624,11 +63518,9 @@ onSent: { data: object({
62624
63518
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62625
63519
  * for: water flow, distance, weight, frequency, signal strength, …).
62626
63520
  *
62627
- * The `unit` and `precision` fields carry the live display metadata that
62628
- * the upstream source provides with each state push (HA
62629
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62630
- * The UI reads them directly from the slice — they are the single source
62631
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63521
+ * The quantity is not on this slice. It is chosen when the device is
63522
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63523
+ * that quantity's base unit. `null` is a refused sample (D393).
62632
63524
  *
62633
63525
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62634
63526
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62637,15 +63529,8 @@ onSent: { data: object({
62637
63529
  * numeric values land here.
62638
63530
  */
62639
63531
  var NumericSensorStatusSchema = object({
62640
- value: number(),
62641
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62642
- * Populated live from the upstream source on each state push.
62643
- * Absent for unitless sensors. */
62644
- unit: string().optional(),
62645
- /** Suggested decimal places for numeric display.
62646
- * Populated live from the upstream source when provided.
62647
- * Falls back to auto-formatting when absent. */
62648
- precision: number().int().min(0).max(10).optional(),
63532
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63533
+ value: number().nullable().meta(readingMeta("device")),
62649
63534
  /** Ms epoch when the slice was last updated. */
62650
63535
  lastFetchedAt: number()
62651
63536
  });
@@ -62669,11 +63554,11 @@ var numericSensorCapability = {
62669
63554
  */
62670
63555
  durability: "restored",
62671
63556
  /**
62672
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63557
+ * History (D850): recorded series. The unit is the device quantity's base,
63558
+ * resolved when the series is read — the cap has no single unit.
62673
63559
  */
62674
63560
  history: { fields: { value: {
62675
63561
  kind: "gauge",
62676
- unitFrom: "unit",
62677
63562
  minIntervalMs: 3e4
62678
63563
  } } },
62679
63564
  /** Clock fields: written, but excluded from the compare that decides
@@ -62965,23 +63850,26 @@ var PetFeederStatusSchema = object({
62965
63850
  /** Food currently in the bowl (grams). Null when the device has not
62966
63851
  * reported a reading yet. On dual-hopper models this is the combined
62967
63852
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
62968
- foodLevel: number().nullable(),
63853
+ foodLevel: number().nullable().meta(readingMeta("none")),
62969
63854
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
62970
63855
  * single-hopper models. */
62971
- food1: number().nullable(),
63856
+ food1: number().nullable().meta(readingMeta("none")),
62972
63857
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
62973
63858
  * single-hopper models. */
62974
- food2: number().nullable(),
63859
+ food2: number().nullable().meta(readingMeta("none")),
62975
63860
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
62976
63861
  * (`device_class: problem`, on = low). True when the bowl is empty /
62977
63862
  * below the feeder's low threshold. */
62978
63863
  lowFood: boolean(),
62979
63864
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
62980
63865
  * device has no battery reading. */
62981
- batteryPower: number().min(0).max(100).nullable(),
62982
- /** Days of desiccant life remaining. Null when the model has no
62983
- * desiccant sensor. */
62984
- desiccantLeftDays: number().nullable(),
63866
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63867
+ /**
63868
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63869
+ * Providers convert from the days the feeder reports. Null when the
63870
+ * model has no desiccant sensor.
63871
+ */
63872
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
62985
63873
  /** True while a feed is in progress. */
62986
63874
  feeding: boolean(),
62987
63875
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -62992,7 +63880,7 @@ var PetFeederStatusSchema = object({
62992
63880
  * with `errorCode` for consumers that want the raw integer. */
62993
63881
  error: string().nullable(),
62994
63882
  /** Raw device error code (0 / null = no error). */
62995
- errorCode: number().nullable(),
63883
+ errorCode: number().nullable().meta(readingMeta("none")),
62996
63884
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
62997
63885
  isDualHopper: boolean(),
62998
63886
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63002,7 +63890,7 @@ var PetFeederStatusSchema = object({
63002
63890
  /** Play a chime when dispensing. */
63003
63891
  feedSound: boolean(),
63004
63892
  /** Speaker / prompt volume level (device-scaled integer). */
63005
- volume: number(),
63893
+ volume: number().meta(readingMeta("none")),
63006
63894
  /** Ms epoch when the slice was last refreshed from the cloud. */
63007
63895
  lastFetchedAt: number()
63008
63896
  });
@@ -63121,7 +64009,7 @@ var petFeederCapability = {
63121
64009
  },
63122
64010
  desiccantLeftDays: {
63123
64011
  kind: "gauge",
63124
- unit: "days"
64012
+ unit: "s"
63125
64013
  }
63126
64014
  } }
63127
64015
  };
@@ -63522,43 +64410,88 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63522
64410
  */
63523
64411
  var PowerMeterStatusSchema = object({
63524
64412
  /** Instantaneous power draw in watts. */
63525
- watts: number().optional().meta({
63526
- unit: "W",
63527
- label: "Power"
63528
- }),
64413
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63529
64414
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63530
- kwhTotal: number().optional().meta({
63531
- unit: "kWh",
63532
- label: "Energy"
63533
- }),
64415
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63534
64416
  /** Voltage in volts. */
63535
- volts: number().optional(),
64417
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
63536
64418
  /** Current in amperes. */
63537
- amps: number().optional(),
63538
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63539
- va: number().optional(),
64419
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
64420
+ /** Apparent power in volt-amperes. */
64421
+ va: number().optional().meta(readingMeta("apparent-power")),
63540
64422
  /** Power factor (0..1) if reported. */
63541
- powerFactor: number().min(0).max(1).optional(),
64423
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63542
64424
  /** Ms epoch when the slice was last updated. */
63543
- lastFetchedAt: number(),
63544
- /** Live display unit of the single metric this slice carries (e.g. HA
63545
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63546
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63547
- * unit per slice is unambiguous. The UI prefers this over the role's
63548
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63549
- unit: string().optional(),
63550
- /** Suggested decimal places for numeric display.
63551
- * Populated live from the upstream source when provided (e.g. HA
63552
- * `attributes.suggested_display_precision`). Falls back to
63553
- * auto-formatting when absent. */
63554
- precision: number().int().min(0).max(10).optional()
64425
+ lastFetchedAt: number()
63555
64426
  });
64427
+ /**
64428
+ * Loads that draw metered mains power (D951). The reading is a feature of the
64429
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
64430
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
64431
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
64432
+ * camera and the like are left out: their draw is not what anyone meters.
64433
+ */
64434
+ var POWER_METER_DEVICE_TYPES = [
64435
+ DeviceType.Sensor,
64436
+ DeviceType.Switch,
64437
+ DeviceType.Light,
64438
+ DeviceType.Fan,
64439
+ DeviceType.Climate,
64440
+ DeviceType.Thermostat,
64441
+ DeviceType.Humidifier,
64442
+ DeviceType.WaterHeater,
64443
+ DeviceType.MediaPlayer,
64444
+ DeviceType.Washer,
64445
+ DeviceType.Dryer,
64446
+ DeviceType.Dishwasher,
64447
+ DeviceType.Vacuum,
64448
+ DeviceType.LawnMower,
64449
+ DeviceType.PetFeeder
64450
+ ];
64451
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
64452
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
64453
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
64454
+ function powerTarget(id, unit) {
64455
+ return {
64456
+ id,
64457
+ cardinality: "scalar",
64458
+ vocabulary: {
64459
+ cap: "power-meter",
64460
+ stateField: id
64461
+ },
64462
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
64463
+ apply: "add",
64464
+ path: id,
64465
+ sources: [{
64466
+ kind: "numeric",
64467
+ requiredUnit: unit,
64468
+ transforms: [{ kind: "identity" }]
64469
+ }],
64470
+ companions: [{
64471
+ kind: "clock",
64472
+ path: "lastFetchedAt",
64473
+ when: "always"
64474
+ }],
64475
+ sourceReport: { status: "assignment-status" },
64476
+ advancedExpression: true
64477
+ };
64478
+ }
64479
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
64480
+ var powerMeterCustomize = {
64481
+ version: 1,
64482
+ targets: [
64483
+ powerTarget("watts", "W"),
64484
+ powerTarget("kwhTotal", "kWh"),
64485
+ powerTarget("volts", "V"),
64486
+ powerTarget("amps", "A")
64487
+ ]
64488
+ };
63556
64489
  var powerMeterCapability = {
63557
64490
  name: "power-meter",
63558
64491
  scope: "device",
63559
64492
  deviceNative: true,
63560
64493
  mode: "singleton",
63561
- deviceTypes: [DeviceType.Sensor],
64494
+ deviceTypes: POWER_METER_DEVICE_TYPES,
63562
64495
  methods: {},
63563
64496
  status: {
63564
64497
  schema: PowerMeterStatusSchema,
@@ -63616,11 +64549,7 @@ var powerMeterCapability = {
63616
64549
  /** Clock fields: written, but excluded from the compare that decides
63617
64550
  * whether persisting is worth a SQLite commit. */
63618
64551
  volatileStateFields: ["lastFetchedAt"],
63619
- customize: {
63620
- version: 1,
63621
- advanced: "derive",
63622
- targets: []
63623
- }
64552
+ customize: powerMeterCustomize
63624
64553
  };
63625
64554
  /**
63626
64555
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63640,11 +64569,11 @@ var powerMeterCapability = {
63640
64569
  */
63641
64570
  var GpsLocationSchema = object({
63642
64571
  /** Latitude in decimal degrees, -90..90. */
63643
- latitude: number().min(-90).max(90),
64572
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63644
64573
  /** Longitude in decimal degrees, -180..180. */
63645
- longitude: number().min(-180).max(180),
64574
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63646
64575
  /** Reported accuracy in meters (lower = better). */
63647
- accuracyMeters: number().nonnegative()
64576
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63648
64577
  });
63649
64578
  var PresenceStatusSchema = object({
63650
64579
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63656,7 +64585,7 @@ var PresenceStatusSchema = object({
63656
64585
  gps: GpsLocationSchema.nullable(),
63657
64586
  /** Optional battery level of the tracking device (0..100). Null
63658
64587
  * when not reported. */
63659
- batteryPercent: number().min(0).max(100).nullable(),
64588
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63660
64589
  /** Ms epoch when the slice was last updated. */
63661
64590
  lastChangedAt: number()
63662
64591
  });
@@ -63706,19 +64635,10 @@ var presenceCapability = {
63706
64635
  * `sourceInfo`.
63707
64636
  */
63708
64637
  var PressureSensorStatusSchema = object({
63709
- /** Current pressure in hPa. */
63710
- hpa: number(),
64638
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64639
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63711
64640
  /** Ms epoch when the slice was last updated. */
63712
- lastFetchedAt: number(),
63713
- /** Live display unit from the upstream source (e.g. HA
63714
- * `attributes.unit_of_measurement`). The UI prefers this over the
63715
- * role's canonical unit. Absent → fall back to the canonical unit. */
63716
- unit: string().optional(),
63717
- /** Suggested decimal places for numeric display.
63718
- * Populated live from the upstream source when provided (e.g. HA
63719
- * `attributes.suggested_display_precision`). Falls back to
63720
- * auto-formatting when absent. */
63721
- precision: number().int().min(0).max(10).optional()
64641
+ lastFetchedAt: number()
63722
64642
  });
63723
64643
  var pressureSensorCapability = {
63724
64644
  name: "pressure-sensor",
@@ -64132,8 +65052,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64132
65052
  }), _void(), { kind: "mutation" });
64133
65053
  var PtzAutotrackSettingsSchema = object({
64134
65054
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64135
- stopDelaySeconds: number().int().min(0).max(300),
64136
- disappearDelaySeconds: number().int().min(0).max(300)
65055
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65056
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64137
65057
  });
64138
65058
  /**
64139
65059
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64287,7 +65207,7 @@ var RunningStateStatusSchema = object({
64287
65207
  /** null = unknown — never coerced to false (D393). */
64288
65208
  running: boolean().nullable(),
64289
65209
  /** Epoch ms the current boolean began; null while `running` is null. */
64290
- since: number().nullable(),
65210
+ since: number().nullable().meta(readingMeta("none")),
64291
65211
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64292
65212
  phase: string().nullable(),
64293
65213
  /** null until a reading has been published (D393). */
@@ -64302,7 +65222,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64302
65222
  /** Generic Customize target for the running boolean (D936). */
64303
65223
  var runningCustomize = {
64304
65224
  version: 1,
64305
- advanced: "derive",
64306
65225
  targets: [{
64307
65226
  id: "running",
64308
65227
  cardinality: "scalar",
@@ -64311,6 +65230,8 @@ var runningCustomize = {
64311
65230
  stateField: "running"
64312
65231
  },
64313
65232
  deviceTypes: [
65233
+ DeviceType.Washer,
65234
+ DeviceType.Dryer,
64314
65235
  DeviceType.Dishwasher,
64315
65236
  DeviceType.Vacuum,
64316
65237
  DeviceType.LawnMower,
@@ -64322,7 +65243,7 @@ var runningCustomize = {
64322
65243
  DeviceType.Thermostat,
64323
65244
  DeviceType.PetFeeder
64324
65245
  ],
64325
- apply: "add-or-replace",
65246
+ apply: "add",
64326
65247
  path: "running",
64327
65248
  sources: [
64328
65249
  {
@@ -65586,9 +66507,9 @@ var RecordTriggerSchema = _enum([
65586
66507
  */
65587
66508
  var RecordWindowSchema = object({
65588
66509
  trigger: RecordTriggerSchema,
65589
- day: number().int().min(0).max(6),
65590
- startMinute: number().int().min(0).max(1439),
65591
- endMinute: number().int().min(1).max(1440)
66510
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66511
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66512
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65592
66513
  });
65593
66514
  /** Status of one physical volume, as the camera itself describes it. */
65594
66515
  var OnboardStorageVolumeSchema = object({
@@ -65664,10 +66585,10 @@ var RecordingOnboardStatusSchema = object({
65664
66585
  /** Master "record to the card at all" switch. */
65665
66586
  enabled: boolean().nullable(),
65666
66587
  overwriteWhenFull: boolean().nullable(),
65667
- preRecordSec: number().nullable(),
65668
- postRecordSec: number().nullable(),
66588
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66589
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65669
66590
  /** Length of one recorded file, in minutes. */
65670
- segmentMinutes: number().nullable(),
66591
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65671
66592
  /** The primary track's weekly windows, flattened. */
65672
66593
  windows: array(RecordWindowSchema),
65673
66594
  /**
@@ -65683,7 +66604,7 @@ var RecordingOnboardStatusSchema = object({
65683
66604
  * provider reporting a non-zero count also reports the schedule as not
65684
66605
  * writable.
65685
66606
  */
65686
- unreadableWindows: number(),
66607
+ unreadableWindows: number().meta(readingMeta("count")),
65687
66608
  /**
65688
66609
  * The camera is scheduled to record and has NO usable storage.
65689
66610
  *
@@ -66528,7 +67449,7 @@ var CamStreamDescriptorSchema = object({
66528
67449
  url: string().optional(),
66529
67450
  codec: string().optional(),
66530
67451
  resolution: CamStreamResolutionSchema.optional(),
66531
- fps: number().positive().optional(),
67452
+ fps: number().positive().optional().meta(readingMeta("rate")),
66532
67453
  label: string().optional(),
66533
67454
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66534
67455
  deviceFeatures: array(string()).optional(),
@@ -66615,18 +67536,18 @@ var StreamProfileSchema = _enum([
66615
67536
  ]);
66616
67537
  /** The current encoding config of one stream profile. */
66617
67538
  var StreamProfileConfigSchema = object({
66618
- width: number(),
66619
- height: number(),
67539
+ width: number().meta(readingMeta("none")),
67540
+ height: number().meta(readingMeta("none")),
66620
67541
  codec: _enum(["h264", "h265"]),
66621
- framerate: number(),
66622
- bitrate: number(),
67542
+ framerate: number().meta(readingMeta("rate")),
67543
+ bitrate: number().meta(readingMeta("bitrate")),
66623
67544
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66624
67545
  encoderProfile: _enum([
66625
67546
  "high",
66626
67547
  "main",
66627
67548
  "baseline"
66628
67549
  ]).optional(),
66629
- gop: number().optional(),
67550
+ gop: number().optional().meta(readingMeta("none")),
66630
67551
  /**
66631
67552
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66632
67553
  *
@@ -66800,7 +67721,8 @@ var switchCapability = {
66800
67721
  deviceTypes: [
66801
67722
  DeviceType.Switch,
66802
67723
  DeviceType.Siren,
66803
- DeviceType.Light
67724
+ DeviceType.Light,
67725
+ DeviceType.Fan
66804
67726
  ],
66805
67727
  methods: { setState: method(object({
66806
67728
  deviceId: number(),
@@ -66847,12 +67769,7 @@ var switchCapability = {
66847
67769
  label: "On",
66848
67770
  kind: "boolean"
66849
67771
  }
66850
- }] },
66851
- customize: {
66852
- version: 1,
66853
- advanced: "derive",
66854
- targets: []
66855
- }
67772
+ }] }
66856
67773
  };
66857
67774
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66858
67775
  function hasControlCharacter(value) {
@@ -67435,35 +68352,21 @@ var tamperCapability = {
67435
68352
  * entries with `device_class: temperature` and any future native
67436
68353
  * thermometer.
67437
68354
  *
67438
- * Unit normalisation: providers convert to Celsius before storing.
67439
- * The slice value is always Celsius so cross-cap aggregators
67440
- * (climate-control's `currentTemp`, energy analytics) can compose
67441
- * without per-source unit fixups. The canonical display unit (`°C`) is
67442
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67443
- * `sourceInfo`.
68355
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68356
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67444
68357
  *
67445
68358
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67446
68359
  * frozen feed (provider hung) distinct from a "temperature hasn't
67447
68360
  * changed" steady state.
67448
68361
  */
67449
68362
  var TemperatureSensorStatusSchema = object({
67450
- /** Current temperature in Celsius. */
67451
- celsius: number().meta({
67452
- unit: "°C",
68363
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68364
+ celsius: number().nullable().meta(readingMeta("temperature", {
68365
+ precision: 1,
67453
68366
  label: "Temperature"
67454
- }),
68367
+ })),
67455
68368
  /** Ms epoch when the slice was last updated (push or poll). */
67456
- lastFetchedAt: number(),
67457
- /** Live display unit from the upstream source (e.g. HA
67458
- * `attributes.unit_of_measurement`). The UI prefers this over the
67459
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67460
- * Absent → the UI falls back to the role's canonical unit. */
67461
- unit: string().optional(),
67462
- /** Suggested decimal places for numeric display.
67463
- * Populated live from the upstream source when provided (e.g. HA
67464
- * `attributes.suggested_display_precision`). Falls back to
67465
- * auto-formatting when absent. */
67466
- precision: number().int().min(0).max(10).optional()
68369
+ lastFetchedAt: number()
67467
68370
  });
67468
68371
  var temperatureSensorCapability = {
67469
68372
  name: "temperature-sensor",
@@ -67494,12 +68397,7 @@ var temperatureSensorCapability = {
67494
68397
  } } },
67495
68398
  /** Clock fields: written, but excluded from the compare that decides
67496
68399
  * whether persisting is worth a SQLite commit. */
67497
- volatileStateFields: ["lastFetchedAt"],
67498
- customize: {
67499
- version: 1,
67500
- advanced: "derive",
67501
- targets: []
67502
- }
68400
+ volatileStateFields: ["lastFetchedAt"]
67503
68401
  };
67504
68402
  /**
67505
68403
  * toast — system-scoped singleton capability that streams toast
@@ -67782,16 +68680,16 @@ var VacuumControlStatusSchema = object({
67782
68680
  ]),
67783
68681
  /** 0..100 battery percentage. Null when the device has no battery
67784
68682
  * reading. */
67785
- batteryLevel: number().min(0).max(100).nullable(),
68683
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67786
68684
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67787
68685
  * the vacuum has no speed control. */
67788
68686
  fanSpeed: string().nullable(),
67789
68687
  /** Speed tokens the hardware accepts — drives the UI selector. */
67790
68688
  availableFanSpeeds: array(string()),
67791
68689
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67792
- progressPercent: number().min(0).max(100).nullable(),
68690
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67793
68691
  /** Current error code (0 / null = no error). */
67794
- errorCode: number().nullable(),
68692
+ errorCode: number().nullable().meta(readingMeta("none")),
67795
68693
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67796
68694
  errorLabel: string().nullable(),
67797
68695
  /** Ms epoch when the slice was last updated. */
@@ -67869,7 +68767,7 @@ var ValveStatusSchema = object({
67869
68767
  ]),
67870
68768
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67871
68769
  * intermediate position surface. */
67872
- position: number().min(0).max(100).nullable(),
68770
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67873
68771
  /** Ms epoch when the slice was last updated. */
67874
68772
  lastChangedAt: number()
67875
68773
  });
@@ -67977,9 +68875,9 @@ var vibrationCapability = {
67977
68875
  */
67978
68876
  var WaterHeaterStatusSchema = object({
67979
68877
  /** Current measured temperature. Null when not reported. */
67980
- currentTemp: number().nullable(),
68878
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
67981
68879
  /** Target temperature setpoint. Null when no setpoint surface. */
67982
- targetTemp: number().nullable(),
68880
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
67983
68881
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
67984
68882
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
67985
68883
  * device reports an unknown state. */
@@ -67990,9 +68888,9 @@ var WaterHeaterStatusSchema = object({
67990
68888
  * has no away surface. */
67991
68889
  away: boolean().nullable(),
67992
68890
  /** HA `min_temp` attribute. Null when not reported. */
67993
- minTemp: number().nullable(),
68891
+ minTemp: number().nullable().meta(readingMeta("temperature")),
67994
68892
  /** HA `max_temp` attribute. Null when not reported. */
67995
- maxTemp: number().nullable(),
68893
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
67996
68894
  /** Ms epoch when the slice was last updated. */
67997
68895
  lastChangedAt: number()
67998
68896
  });
@@ -68077,25 +68975,18 @@ var waterHeaterCapability = {
68077
68975
  * Forecast deferred — current conditions only for v1.
68078
68976
  */
68079
68977
  var WeatherStatusSchema = object({
68080
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68081
- * when no condition has been reported yet. */
68978
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68082
68979
  condition: string().nullable(),
68083
- /** Current temperature in the reported unit. Null when not provided. */
68084
- temperature: number().nullable(),
68085
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68086
- temperatureUnit: string().nullable(),
68980
+ /** Current temperature in Celsius. Null when not provided or refused. */
68981
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68087
68982
  /** Relative humidity (0..100). Null when not provided. */
68088
- humidity: number().min(0).max(100).nullable(),
68089
- /** Barometric pressure in the reported unit. Null when not provided. */
68090
- pressure: number().nullable(),
68091
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68092
- pressureUnit: string().nullable(),
68093
- /** Wind speed in the reported unit. Null when not provided. */
68094
- windSpeed: number().nullable(),
68095
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68096
- windSpeedUnit: string().nullable(),
68983
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68984
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68985
+ pressure: number().nullable().meta(readingMeta("pressure")),
68986
+ /** Wind speed in metres per second. Null when not provided or refused. */
68987
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68097
68988
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68098
- windBearing: number().nullable(),
68989
+ windBearing: number().nullable().meta(readingMeta("angle")),
68099
68990
  /** Ms epoch when the slice was last updated. */
68100
68991
  lastFetchedAt: number()
68101
68992
  });
@@ -68123,12 +69014,12 @@ var weatherCapability = {
68123
69014
  */
68124
69015
  durability: "session",
68125
69016
  /**
68126
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69017
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68127
69018
  */
68128
69019
  history: { fields: {
68129
69020
  temperature: {
68130
69021
  kind: "gauge",
68131
- unitFrom: "temperatureUnit",
69022
+ unit: "°C",
68132
69023
  deadband: .2
68133
69024
  },
68134
69025
  humidity: {
@@ -68138,12 +69029,12 @@ var weatherCapability = {
68138
69029
  },
68139
69030
  pressure: {
68140
69031
  kind: "gauge",
68141
- unitFrom: "pressureUnit",
69032
+ unit: "hPa",
68142
69033
  deadband: .5
68143
69034
  },
68144
69035
  windSpeed: {
68145
69036
  kind: "gauge",
68146
- unitFrom: "windSpeedUnit",
69037
+ unit: "m/s",
68147
69038
  deadband: .5
68148
69039
  }
68149
69040
  } }
@@ -69044,8 +69935,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
69044
69935
  kind: "mutation",
69045
69936
  auth: "admin"
69046
69937
  });
69938
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
69047
69939
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69048
- 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;
69940
+ 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;
69049
69941
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
69050
69942
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
69051
69943
  new Set(CAP_NAMES);
@@ -69374,6 +70266,9 @@ var DEVICE_LOCAL_STATE_CAPS = {
69374
70266
  featureProbe: featureProbeCapability,
69375
70267
  flood: floodCapability,
69376
70268
  gas: gasCapability,
70269
+ groupActive: groupActiveCapability,
70270
+ groupCommands: groupCommandsCapability,
70271
+ groupStatus: groupStatusCapability,
69377
70272
  heatingValve: heatingValveCapability,
69378
70273
  humidifier: humidifierCapability,
69379
70274
  humiditySensor: humiditySensorCapability,
@@ -69383,6 +70278,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
69383
70278
  lawnMowerControl: lawnMowerControlCapability,
69384
70279
  lockControl: lockControlCapability,
69385
70280
  mediaPlayer: mediaPlayerCapability,
70281
+ meter: meterCapability,
69386
70282
  motion: motionCapability,
69387
70283
  motionTrigger: motionTriggerCapability,
69388
70284
  motionZones: motionZonesCapability,
@@ -70642,7 +71538,7 @@ var BaseDeviceProvider = class extends BaseAddon {
70642
71538
  return toDeviceSummary(device, this.addonId);
70643
71539
  }
70644
71540
  };
70645
- 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;
71541
+ 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;
70646
71542
  new Set(Object.values(DeviceType));
70647
71543
  Object.freeze({
70648
71544
  "accessories.getStatus": {
@@ -70750,6 +71646,13 @@ Object.freeze({
70750
71646
  access: "view",
70751
71647
  role: "admin"
70752
71648
  },
71649
+ "addons.getPackageDocs": {
71650
+ capName: "addons",
71651
+ capScope: "system",
71652
+ addonId: null,
71653
+ access: "view",
71654
+ role: "admin"
71655
+ },
70753
71656
  "addons.getVersions": {
70754
71657
  capName: "addons",
70755
71658
  capScope: "system",
@@ -73858,6 +74761,27 @@ Object.freeze({
73858
74761
  access: "view",
73859
74762
  role: "viewer"
73860
74763
  },
74764
+ "groupActive.getStatus": {
74765
+ capName: "group-active",
74766
+ capScope: "device",
74767
+ addonId: null,
74768
+ access: "view",
74769
+ role: "viewer"
74770
+ },
74771
+ "groupCommands.getStatus": {
74772
+ capName: "group-commands",
74773
+ capScope: "device",
74774
+ addonId: null,
74775
+ access: "view",
74776
+ role: "viewer"
74777
+ },
74778
+ "groupStatus.getStatus": {
74779
+ capName: "group-status",
74780
+ capScope: "device",
74781
+ addonId: null,
74782
+ access: "view",
74783
+ role: "viewer"
74784
+ },
73861
74785
  "heatingValve.getStatus": {
73862
74786
  capName: "heating-valve",
73863
74787
  capScope: "device",
@@ -74810,6 +75734,13 @@ Object.freeze({
74810
75734
  access: "create",
74811
75735
  role: "admin"
74812
75736
  },
75737
+ "meter.getStatus": {
75738
+ capName: "meter",
75739
+ capScope: "device",
75740
+ addonId: null,
75741
+ access: "view",
75742
+ role: "viewer"
75743
+ },
74813
75744
  "metricsProvider.collectSnapshot": {
74814
75745
  capName: "metrics-provider",
74815
75746
  capScope: "system",
@@ -82908,7 +83839,39 @@ var APPLIANCE_CAP_VOCABULARY = {
82908
83839
  phrase: t("When {device} finishes", "Quando {device} finisce"),
82909
83840
  icon: "square"
82910
83841
  }
82911
- }
83842
+ },
83843
+ overrides: { byType: {
83844
+ [DeviceType.Washer]: {
83845
+ state: { running: {
83846
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
83847
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
83848
+ } },
83849
+ events: {
83850
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
83851
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
83852
+ }
83853
+ },
83854
+ [DeviceType.Dryer]: {
83855
+ state: { running: {
83856
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
83857
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
83858
+ } },
83859
+ events: {
83860
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
83861
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
83862
+ }
83863
+ },
83864
+ [DeviceType.Dishwasher]: {
83865
+ state: { running: {
83866
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
83867
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
83868
+ } },
83869
+ events: {
83870
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
83871
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
83872
+ }
83873
+ }
83874
+ } }
82912
83875
  },
82913
83876
  update: {
82914
83877
  scope: "device",
@@ -83680,6 +84643,41 @@ var SENSOR_CAP_VOCABULARY = {
83680
84643
  icon: "flame"
83681
84644
  }]
83682
84645
  },
84646
+ "group-active": {
84647
+ scope: "device",
84648
+ noun: t("Group state", "Stato del gruppo")
84649
+ },
84650
+ "group-commands": {
84651
+ scope: "device",
84652
+ noun: t("Group command", "Comando di gruppo")
84653
+ },
84654
+ "group-status": {
84655
+ scope: "device",
84656
+ noun: t("Group", "Gruppo"),
84657
+ state: [
84658
+ {
84659
+ kind: "numeric",
84660
+ field: "included",
84661
+ quantity: t("the members reporting", "i membri che rispondono"),
84662
+ label: t("Reporting", "Rispondono"),
84663
+ icon: "users"
84664
+ },
84665
+ {
84666
+ kind: "numeric",
84667
+ field: "excluded",
84668
+ quantity: t("the members left out", "i membri esclusi"),
84669
+ label: t("Excluded", "Esclusi"),
84670
+ icon: "users"
84671
+ },
84672
+ {
84673
+ kind: "numeric",
84674
+ field: "total",
84675
+ quantity: t("the member count", "il numero di membri"),
84676
+ label: t("Members", "Membri"),
84677
+ icon: "sigma"
84678
+ }
84679
+ ]
84680
+ },
83683
84681
  "humidity-sensor": {
83684
84682
  scope: "device",
83685
84683
  noun: t("Humidity", "Umidità"),
@@ -83705,6 +84703,17 @@ var SENSOR_CAP_VOCABULARY = {
83705
84703
  }],
83706
84704
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
83707
84705
  },
84706
+ meter: {
84707
+ scope: "device",
84708
+ noun: t("Meter", "Contatore"),
84709
+ state: [{
84710
+ kind: "numeric",
84711
+ field: "value",
84712
+ quantity: t("the total", "il totale"),
84713
+ label: t("Total", "Totale"),
84714
+ icon: "gauge"
84715
+ }]
84716
+ },
83708
84717
  "numeric-sensor": {
83709
84718
  scope: "device",
83710
84719
  noun: t("Sensor", "Sensore"),
@@ -83713,7 +84722,6 @@ var SENSOR_CAP_VOCABULARY = {
83713
84722
  field: "value",
83714
84723
  quantity: t("the reading", "il valore"),
83715
84724
  label: t("Reading", "Valore"),
83716
- unitField: "unit",
83717
84725
  icon: "gauge"
83718
84726
  }]
83719
84727
  },
@@ -83849,7 +84857,7 @@ var SENSOR_CAP_VOCABULARY = {
83849
84857
  field: "temperature",
83850
84858
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83851
84859
  label: t("Temperature", "Temperatura"),
83852
- unitField: "temperatureUnit",
84860
+ unit: "°C",
83853
84861
  icon: "thermometer"
83854
84862
  },
83855
84863
  {
@@ -83865,7 +84873,7 @@ var SENSOR_CAP_VOCABULARY = {
83865
84873
  field: "windSpeed",
83866
84874
  quantity: t("the wind speed", "la velocità del vento"),
83867
84875
  label: t("Wind", "Vento"),
83868
- unitField: "windSpeedUnit",
84876
+ unit: "m/s",
83869
84877
  icon: "wind"
83870
84878
  },
83871
84879
  {
@@ -83873,7 +84881,7 @@ var SENSOR_CAP_VOCABULARY = {
83873
84881
  field: "pressure",
83874
84882
  quantity: t("the air pressure", "la pressione atmosferica"),
83875
84883
  label: t("Pressure", "Pressione"),
83876
- unitField: "pressureUnit",
84884
+ unit: "hPa",
83877
84885
  icon: "gauge"
83878
84886
  }
83879
84887
  ]
@@ -84178,8 +85186,47 @@ function projectAction(cap, action) {
84178
85186
  };
84179
85187
  }
84180
85188
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
84181
- 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;
85189
+ 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;
84182
85190
  DeviceFeature.BatteryOperated;
85191
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
85192
+ 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.";
85193
+ 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.";
85194
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
85195
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
85196
+ 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.";
85197
+ function withheld(reason) {
85198
+ return {
85199
+ [DeviceType.Washer]: {
85200
+ exported: false,
85201
+ reason
85202
+ },
85203
+ [DeviceType.Dryer]: {
85204
+ exported: false,
85205
+ reason
85206
+ },
85207
+ [DeviceType.Dishwasher]: {
85208
+ exported: false,
85209
+ reason
85210
+ }
85211
+ };
85212
+ }
85213
+ function offered(received) {
85214
+ return {
85215
+ [DeviceType.Washer]: {
85216
+ exported: true,
85217
+ as: received
85218
+ },
85219
+ [DeviceType.Dryer]: {
85220
+ exported: true,
85221
+ as: received
85222
+ },
85223
+ [DeviceType.Dishwasher]: {
85224
+ exported: true,
85225
+ as: received
85226
+ }
85227
+ };
85228
+ }
85229
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
84183
85230
  Object.freeze({
84184
85231
  "accessories.getStatus": [{
84185
85232
  name: "deviceId",
@@ -85146,6 +86193,21 @@ Object.freeze({
85146
86193
  form: "single",
85147
86194
  optional: false
85148
86195
  }],
86196
+ "groupActive.getStatus": [{
86197
+ name: "deviceId",
86198
+ form: "single",
86199
+ optional: false
86200
+ }],
86201
+ "groupCommands.getStatus": [{
86202
+ name: "deviceId",
86203
+ form: "single",
86204
+ optional: false
86205
+ }],
86206
+ "groupStatus.getStatus": [{
86207
+ name: "deviceId",
86208
+ form: "single",
86209
+ optional: false
86210
+ }],
85149
86211
  "heatingValve.getStatus": [{
85150
86212
  name: "deviceId",
85151
86213
  form: "single",
@@ -85346,6 +86408,11 @@ Object.freeze({
85346
86408
  form: "single",
85347
86409
  optional: false
85348
86410
  }],
86411
+ "meter.getStatus": [{
86412
+ name: "deviceId",
86413
+ form: "single",
86414
+ optional: false
86415
+ }],
85349
86416
  "motion.getStatus": [{
85350
86417
  name: "deviceId",
85351
86418
  form: "single",
@@ -86953,7 +88020,6 @@ Object.freeze({
86953
88020
  "network-access": "ingress",
86954
88021
  "smtp-provider": "email"
86955
88022
  });
86956
- DeviceType.Camera;
86957
88023
  /**
86958
88024
  * A saved Customize assignment (D936). The schema is the type: callers use
86959
88025
  * `z.infer`, they do not restate the shape.
@@ -87130,6 +88196,8 @@ new Map(Object.entries({
87130
88196
  "detection-blind": "device-events",
87131
88197
  "motion-noisy": "device-events",
87132
88198
  "motion-calm": "device-events",
88199
+ "package-llm-silent": "device-events",
88200
+ "package-llm-answered": "device-events",
87133
88201
  "recording-stalled": "device-events",
87134
88202
  "recording-healthy": "device-events",
87135
88203
  "recording-degraded": "device-events",
@@ -87213,6 +88281,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87213
88281
  label: "Motion without a subject",
87214
88282
  kinds: ["motion-noisy", "motion-calm"]
87215
88283
  },
88284
+ {
88285
+ id: "package-llm",
88286
+ family: "device-events",
88287
+ label: "Package confirm",
88288
+ kinds: ["package-llm-silent", "package-llm-answered"]
88289
+ },
87216
88290
  {
87217
88291
  id: "recording",
87218
88292
  family: "device-events",
@@ -87394,6 +88468,8 @@ var DEVICE_EVENTS = [
87394
88468
  "detection-blind",
87395
88469
  "motion-noisy",
87396
88470
  "motion-calm",
88471
+ "package-llm-silent",
88472
+ "package-llm-answered",
87397
88473
  "recording-stalled",
87398
88474
  "recording-healthy",
87399
88475
  "recording-degraded",
@@ -87662,6 +88738,15 @@ new Map([
87662
88738
  snapshotLabel: "Cameras with constant motion now",
87663
88739
  snapshotSection: null
87664
88740
  },
88741
+ {
88742
+ id: "package-llm-confirm",
88743
+ family: "device-events",
88744
+ label: "Package confirm (not answering / answering again)",
88745
+ bad: ["package-llm-silent"],
88746
+ recovery: ["package-llm-answered"],
88747
+ snapshotLabel: "Cameras whose package confirm is not answering",
88748
+ snapshotSection: null
88749
+ },
87665
88750
  {
87666
88751
  id: "recording-health",
87667
88752
  family: "device-events",