@camstack/addon-provider-velux 0.2.146 → 0.2.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.mjs CHANGED
@@ -1,4 +1,4 @@
1
- //#region ../types/dist/event-category-DHXxB8t9.mjs
1
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
2
2
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
3
  EventCategory["SystemBoot"] = "system.boot";
4
4
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -324,6 +324,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
324
324
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
325
325
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
326
326
  /**
327
+ * A camera's package LLM confirm has failed closed three times in a row
328
+ * (D931, 2026-10-26). One edge per camera until
329
+ * {@link PipelinePackageLlmAnswered}.
330
+ */
331
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
332
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
333
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
334
+ /**
327
335
  * Per-camera pipeline config was mutated by the orchestrator
328
336
  * (a settings change via `setAgentInferenceDevices` /
329
337
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5433,7 +5441,7 @@ var ZodIssueCode = {
5433
5441
  var ZodFirstPartyTypeKind;
5434
5442
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5435
5443
  //#endregion
5436
- //#region ../types/dist/sleep-BErAxhul.mjs
5444
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5437
5445
  /**
5438
5446
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5439
5447
  * window to float samples (D455).
@@ -7987,6 +7995,99 @@ var RecordKindDeclarationSchema = object({
7987
7995
  }).strict();
7988
7996
  /** The kind names: lower-case, dash separated, short enough for an index name. */
7989
7997
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7998
+ var READING_QUANTITIES = [
7999
+ "temperature",
8000
+ "pressure",
8001
+ "speed",
8002
+ "precipitation",
8003
+ "precipitation-rate",
8004
+ "length",
8005
+ "area",
8006
+ "illuminance",
8007
+ "irradiance",
8008
+ "power",
8009
+ "energy",
8010
+ "voltage",
8011
+ "current",
8012
+ "apparent-power",
8013
+ "humidity",
8014
+ "percentage",
8015
+ "concentration-volume",
8016
+ "concentration-mass",
8017
+ "concentration-ppb",
8018
+ "volume",
8019
+ "duration",
8020
+ "duration-ms",
8021
+ "information",
8022
+ "sound-level",
8023
+ "signal-level",
8024
+ "colour-temperature",
8025
+ "angle",
8026
+ "bitrate",
8027
+ "rate",
8028
+ "mass",
8029
+ "index",
8030
+ "ratio",
8031
+ "count",
8032
+ "none",
8033
+ "device"
8034
+ ];
8035
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8036
+ new Set(READING_QUANTITIES);
8037
+ /**
8038
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8039
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8040
+ */
8041
+ var QUANTITY_BASE = {
8042
+ temperature: "°C",
8043
+ pressure: "hPa",
8044
+ speed: "m/s",
8045
+ precipitation: "mm",
8046
+ "precipitation-rate": "mm/h",
8047
+ length: "m",
8048
+ area: "m²",
8049
+ illuminance: "lx",
8050
+ irradiance: "W/m²",
8051
+ power: "W",
8052
+ energy: "kWh",
8053
+ voltage: "V",
8054
+ current: "A",
8055
+ "apparent-power": "VA",
8056
+ humidity: "%",
8057
+ percentage: "%",
8058
+ "concentration-volume": "ppm",
8059
+ "concentration-mass": "µg/m³",
8060
+ "concentration-ppb": "ppb",
8061
+ volume: "L",
8062
+ duration: "s",
8063
+ "duration-ms": "ms",
8064
+ information: "B",
8065
+ "sound-level": "dBFS",
8066
+ "signal-level": "dBm",
8067
+ "colour-temperature": "mired",
8068
+ angle: "°",
8069
+ bitrate: "kbps",
8070
+ rate: "fps",
8071
+ mass: "kg",
8072
+ index: "none",
8073
+ ratio: "none",
8074
+ count: "none",
8075
+ none: "none",
8076
+ device: "device"
8077
+ };
8078
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8079
+ function baseUnitOf(quantity) {
8080
+ const symbol = QUANTITY_BASE[quantity];
8081
+ if (symbol === "none" || symbol === "device") return void 0;
8082
+ return symbol;
8083
+ }
8084
+ function readingMeta(quantity, extras) {
8085
+ return {
8086
+ quantity,
8087
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8088
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8089
+ };
8090
+ }
7990
8091
  /** Shared Zod schemas used across streaming capabilities. */
7991
8092
  var CamProfileSchema = _enum([
7992
8093
  "high",
@@ -8014,8 +8115,8 @@ var CamStreamKindSchema = _enum([
8014
8115
  "derived"
8015
8116
  ]);
8016
8117
  var CamStreamResolutionSchema = object({
8017
- width: number().int().positive(),
8018
- height: number().int().positive()
8118
+ width: number().int().positive().meta(readingMeta("none")),
8119
+ height: number().int().positive().meta(readingMeta("none"))
8019
8120
  });
8020
8121
  var CameraStreamSchema = object({
8021
8122
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8533,6 +8634,9 @@ var CAP_NAMES = Object.freeze([
8533
8634
  "filesystem-browse",
8534
8635
  "flood",
8535
8636
  "gas",
8637
+ "group-active",
8638
+ "group-commands",
8639
+ "group-status",
8536
8640
  "heating-valve",
8537
8641
  "humidifier",
8538
8642
  "humidity-sensor",
@@ -8553,6 +8657,7 @@ var CAP_NAMES = Object.freeze([
8553
8657
  "login-method",
8554
8658
  "media-player",
8555
8659
  "mesh-network",
8660
+ "meter",
8556
8661
  "metrics-provider",
8557
8662
  "model-convert",
8558
8663
  "model-distributor",
@@ -8662,14 +8767,6 @@ Object.freeze({
8662
8767
  {
8663
8768
  "path": "aqi",
8664
8769
  "valueType": "number"
8665
- },
8666
- {
8667
- "path": "unit",
8668
- "valueType": "string"
8669
- },
8670
- {
8671
- "path": "precision",
8672
- "valueType": "number"
8673
8770
  }
8674
8771
  ],
8675
8772
  "alarm-panel": [
@@ -8705,20 +8802,10 @@ Object.freeze({
8705
8802
  "valueType": "boolean"
8706
8803
  }
8707
8804
  ],
8708
- "ambient-light-sensor": [
8709
- {
8710
- "path": "lux",
8711
- "valueType": "number"
8712
- },
8713
- {
8714
- "path": "unit",
8715
- "valueType": "string"
8716
- },
8717
- {
8718
- "path": "precision",
8719
- "valueType": "number"
8720
- }
8721
- ],
8805
+ "ambient-light-sensor": [{
8806
+ "path": "lux",
8807
+ "valueType": "number"
8808
+ }],
8722
8809
  "audio-metrics": [
8723
8810
  {
8724
8811
  "path": "ts",
@@ -8798,6 +8885,10 @@ Object.freeze({
8798
8885
  "path": "lastUpdated",
8799
8886
  "valueType": "number"
8800
8887
  },
8888
+ {
8889
+ "path": "volts",
8890
+ "valueType": "number"
8891
+ },
8801
8892
  {
8802
8893
  "path": "lastContactAt",
8803
8894
  "valueType": "number"
@@ -9040,10 +9131,6 @@ Object.freeze({
9040
9131
  "datetime"
9041
9132
  ]
9042
9133
  },
9043
- {
9044
- "path": "unit",
9045
- "valueType": "string"
9046
- },
9047
9134
  {
9048
9135
  "path": "min",
9049
9136
  "valueType": "number"
@@ -9055,19 +9142,6 @@ Object.freeze({
9055
9142
  {
9056
9143
  "path": "step",
9057
9144
  "valueType": "number"
9058
- },
9059
- {
9060
- "path": "precision",
9061
- "valueType": "number"
9062
- },
9063
- {
9064
- "path": "format",
9065
- "valueType": "enum",
9066
- "enumValues": [
9067
- "date",
9068
- "time",
9069
- "datetime"
9070
- ]
9071
9145
  }
9072
9146
  ],
9073
9147
  "cover": [
@@ -9308,13 +9382,8 @@ Object.freeze({
9308
9382
  "path": "value",
9309
9383
  "valueType": "string"
9310
9384
  }, {
9311
- "path": "format",
9312
- "valueType": "enum",
9313
- "enumValues": [
9314
- "date",
9315
- "time",
9316
- "datetime"
9317
- ]
9385
+ "path": "timestamp",
9386
+ "valueType": "boolean"
9318
9387
  }],
9319
9388
  "event-emitter": [
9320
9389
  {
@@ -9387,6 +9456,24 @@ Object.freeze({
9387
9456
  "path": "detected",
9388
9457
  "valueType": "boolean"
9389
9458
  }],
9459
+ "group-active": [{
9460
+ "path": "active",
9461
+ "valueType": "boolean"
9462
+ }],
9463
+ "group-status": [
9464
+ {
9465
+ "path": "included",
9466
+ "valueType": "number"
9467
+ },
9468
+ {
9469
+ "path": "excluded",
9470
+ "valueType": "number"
9471
+ },
9472
+ {
9473
+ "path": "total",
9474
+ "valueType": "number"
9475
+ }
9476
+ ],
9390
9477
  "heating-valve": [{
9391
9478
  "path": "position",
9392
9479
  "valueType": "number"
@@ -9421,20 +9508,10 @@ Object.freeze({
9421
9508
  "valueType": "number"
9422
9509
  }
9423
9510
  ],
9424
- "humidity-sensor": [
9425
- {
9426
- "path": "percent",
9427
- "valueType": "number"
9428
- },
9429
- {
9430
- "path": "unit",
9431
- "valueType": "string"
9432
- },
9433
- {
9434
- "path": "precision",
9435
- "valueType": "number"
9436
- }
9437
- ],
9511
+ "humidity-sensor": [{
9512
+ "path": "percent",
9513
+ "valueType": "number"
9514
+ }],
9438
9515
  "image": [{
9439
9516
  "path": "url",
9440
9517
  "valueType": "string"
@@ -9642,6 +9719,10 @@ Object.freeze({
9642
9719
  ]
9643
9720
  }
9644
9721
  ],
9722
+ "meter": [{
9723
+ "path": "value",
9724
+ "valueType": "number"
9725
+ }],
9645
9726
  "motion": [
9646
9727
  {
9647
9728
  "path": "detected",
@@ -9773,20 +9854,10 @@ Object.freeze({
9773
9854
  "valueType": "boolean"
9774
9855
  }
9775
9856
  ],
9776
- "numeric-sensor": [
9777
- {
9778
- "path": "value",
9779
- "valueType": "number"
9780
- },
9781
- {
9782
- "path": "unit",
9783
- "valueType": "string"
9784
- },
9785
- {
9786
- "path": "precision",
9787
- "valueType": "number"
9788
- }
9789
- ],
9857
+ "numeric-sensor": [{
9858
+ "path": "value",
9859
+ "valueType": "number"
9860
+ }],
9790
9861
  "pet-feeder": [
9791
9862
  {
9792
9863
  "path": "foodLevel",
@@ -9878,14 +9949,6 @@ Object.freeze({
9878
9949
  {
9879
9950
  "path": "powerFactor",
9880
9951
  "valueType": "number"
9881
- },
9882
- {
9883
- "path": "unit",
9884
- "valueType": "string"
9885
- },
9886
- {
9887
- "path": "precision",
9888
- "valueType": "number"
9889
9952
  }
9890
9953
  ],
9891
9954
  "presence": [
@@ -9914,20 +9977,10 @@ Object.freeze({
9914
9977
  "valueType": "number"
9915
9978
  }
9916
9979
  ],
9917
- "pressure-sensor": [
9918
- {
9919
- "path": "hpa",
9920
- "valueType": "number"
9921
- },
9922
- {
9923
- "path": "unit",
9924
- "valueType": "string"
9925
- },
9926
- {
9927
- "path": "precision",
9928
- "valueType": "number"
9929
- }
9930
- ],
9980
+ "pressure-sensor": [{
9981
+ "path": "hpa",
9982
+ "valueType": "number"
9983
+ }],
9931
9984
  "privacy-mask": [{
9932
9985
  "path": "enabled",
9933
9986
  "valueType": "boolean"
@@ -10238,20 +10291,10 @@ Object.freeze({
10238
10291
  "path": "tampered",
10239
10292
  "valueType": "boolean"
10240
10293
  }],
10241
- "temperature-sensor": [
10242
- {
10243
- "path": "celsius",
10244
- "valueType": "number"
10245
- },
10246
- {
10247
- "path": "unit",
10248
- "valueType": "string"
10249
- },
10250
- {
10251
- "path": "precision",
10252
- "valueType": "number"
10253
- }
10254
- ],
10294
+ "temperature-sensor": [{
10295
+ "path": "celsius",
10296
+ "valueType": "number"
10297
+ }],
10255
10298
  "update": [
10256
10299
  {
10257
10300
  "path": "currentVersion",
@@ -10362,10 +10405,6 @@ Object.freeze({
10362
10405
  "path": "temperature",
10363
10406
  "valueType": "number"
10364
10407
  },
10365
- {
10366
- "path": "temperatureUnit",
10367
- "valueType": "string"
10368
- },
10369
10408
  {
10370
10409
  "path": "humidity",
10371
10410
  "valueType": "number"
@@ -10374,18 +10413,10 @@ Object.freeze({
10374
10413
  "path": "pressure",
10375
10414
  "valueType": "number"
10376
10415
  },
10377
- {
10378
- "path": "pressureUnit",
10379
- "valueType": "string"
10380
- },
10381
10416
  {
10382
10417
  "path": "windSpeed",
10383
10418
  "valueType": "number"
10384
10419
  },
10385
- {
10386
- "path": "windSpeedUnit",
10387
- "valueType": "string"
10388
- },
10389
10420
  {
10390
10421
  "path": "windBearing",
10391
10422
  "valueType": "number"
@@ -10449,7 +10480,7 @@ function userError(code, params, englishFallback) {
10449
10480
  return new CamstackError(code, params, englishFallback);
10450
10481
  }
10451
10482
  //#endregion
10452
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10483
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10453
10484
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10454
10485
  DeviceType["Camera"] = "camera";
10455
10486
  DeviceType["Hub"] = "hub";
@@ -10543,8 +10574,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10543
10574
  DeviceType["PetFeeder"] = "pet-feeder";
10544
10575
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10545
10576
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10546
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10577
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10578
+ * A plug-built dishwasher carries running-state and power instead. */
10547
10579
  DeviceType["Dishwasher"] = "dishwasher";
10580
+ /** Washing machine. A plug-built one is running while its power stays
10581
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10582
+ DeviceType["Washer"] = "washer";
10583
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10584
+ DeviceType["Dryer"] = "dryer";
10548
10585
  return DeviceType;
10549
10586
  }({});
10550
10587
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -10802,7 +10839,7 @@ function errMsg(err) {
10802
10839
  return String(err);
10803
10840
  }
10804
10841
  //#endregion
10805
- //#region ../types/dist/composition-kEl9mevu.mjs
10842
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
10806
10843
  /**
10807
10844
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10808
10845
  *
@@ -11117,6 +11154,13 @@ var table = {
11117
11154
  apply: () => {
11118
11155
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11119
11156
  }
11157
+ },
11158
+ schmitt: {
11159
+ minArgs: 3,
11160
+ maxArgs: 5,
11161
+ apply: () => {
11162
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11163
+ }
11120
11164
  }
11121
11165
  };
11122
11166
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11557,15 +11601,232 @@ function validateExpressionSource(src) {
11557
11601
  }
11558
11602
  return null;
11559
11603
  }
11560
- /** A composed device's stableId is this prefix plus the block id. */
11561
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11562
- /** Longest snippet a `code` source may carry (slice 4). */
11563
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11604
+ /**
11605
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11606
+ *
11607
+ * Standby of the control board is under 5 W; a motor or a heater is far
11608
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11609
+ * a cycle, so running starts only after 30 s above the line.
11610
+ *
11611
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11612
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11613
+ * report "finished" in the middle of the program, so the stop dwell is
11614
+ * 10 minutes on every type. The number is stored per type so one machine
11615
+ * can move later without moving the others.
11616
+ */
11617
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11618
+ var APPLIANCE_KINDS = [
11619
+ "washer",
11620
+ "dryer",
11621
+ "dishwasher"
11622
+ ];
11623
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11624
+ /**
11625
+ * Statistics and integrals over a history read (D944).
11626
+ *
11627
+ * Pure: the same points always give the same answer, so a restart recomputes
11628
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11629
+ * never the mean of the samples. A hole with no held value is not integrated
11630
+ * as zero. A truncated read is not published.
11631
+ */
11632
+ var STATISTIC_STATS = [
11633
+ "min",
11634
+ "max",
11635
+ "mean",
11636
+ "sum",
11637
+ "count",
11638
+ "change"
11639
+ ];
11640
+ /**
11641
+ * The range a statistics or integral helper asks history for (D944).
11642
+ *
11643
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11644
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11645
+ * is strictly longer than the longest that range can be. Forever qualifies.
11646
+ * Equal to a tier's retention is a refusal, not a short answer.
11647
+ */
11648
+ var MINUTE_MS = 6e4;
11649
+ var HOUR_MS = 60 * MINUTE_MS;
11650
+ var DAY_MS = 24 * HOUR_MS;
11651
+ var ROLLING_SPANS = [
11652
+ "1h",
11653
+ "24h",
11654
+ "2d",
11655
+ "7d",
11656
+ "30d",
11657
+ "custom"
11658
+ ];
11659
+ var CALENDAR_PERIODS = [
11660
+ "today",
11661
+ "yesterday",
11662
+ "this-week",
11663
+ "this-month"
11664
+ ];
11665
+ var RESET_PERIODS = [
11666
+ "never",
11667
+ "daily",
11668
+ "weekly",
11669
+ "monthly"
11670
+ ];
11671
+ 5 * MINUTE_MS;
11672
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11673
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11674
+ var rollingWindowObject = object({
11675
+ kind: literal("rolling"),
11676
+ span: _enum(ROLLING_SPANS),
11677
+ customMinutes: number().int().min(5).optional()
11678
+ }).check((payload) => {
11679
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11680
+ code: "custom",
11681
+ message: "a custom range needs a duration in minutes",
11682
+ path: ["customMinutes"],
11683
+ input: payload.value
11684
+ });
11685
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11686
+ code: "custom",
11687
+ message: "only a custom range carries a duration",
11688
+ path: ["customMinutes"],
11689
+ input: payload.value
11690
+ });
11691
+ });
11692
+ var RollingWindowSchema = rollingWindowObject;
11693
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
11694
+ kind: literal("calendar"),
11695
+ period: _enum(CALENDAR_PERIODS)
11696
+ })]);
11697
+ /**
11698
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11699
+ * id. Split out so helper schemas can name it without a cycle through
11700
+ * `composition.ts` (D659).
11701
+ */
11564
11702
  var CompositionSourceRefSchema = object({
11565
11703
  /** The owning addon; `stableId` is unique only within it. */
11566
11704
  addonId: string().min(1),
11567
11705
  stableId: string().min(1)
11568
11706
  });
11707
+ var GROUP_TEMPLATE_IDS = [
11708
+ "temperature",
11709
+ "humidity",
11710
+ "pressure",
11711
+ "illuminance",
11712
+ "power",
11713
+ "energy",
11714
+ "battery",
11715
+ "numeric",
11716
+ "motion",
11717
+ "contact",
11718
+ "leak",
11719
+ "smoke",
11720
+ "occupancy",
11721
+ "binary",
11722
+ "switch",
11723
+ "light",
11724
+ "fan",
11725
+ "cover",
11726
+ "valve",
11727
+ "lock",
11728
+ "media-player",
11729
+ "climate"
11730
+ ];
11731
+ var NUMERIC_AGGREGATES = [
11732
+ "mean",
11733
+ "median",
11734
+ "min",
11735
+ "max",
11736
+ "sum"
11737
+ ];
11738
+ var BINARY_AGGREGATES = ["any", "all"];
11739
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11740
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11741
+ var AGGREGATE_REDUCERS = [
11742
+ "mean",
11743
+ "median",
11744
+ "min",
11745
+ "max",
11746
+ "sum",
11747
+ "any",
11748
+ "all"
11749
+ ];
11750
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11751
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11752
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11753
+ /** One member. The field is stored so occupancy can name motion or binary. */
11754
+ var HelperMemberSchema = object({
11755
+ source: CompositionSourceRefSchema,
11756
+ cap: string().min(1),
11757
+ fieldPath: string().min(1)
11758
+ });
11759
+ var HelperSpecSchema = discriminatedUnion("kind", [
11760
+ object({
11761
+ kind: literal("group"),
11762
+ templateId: GroupTemplateIdSchema,
11763
+ aggregate: AggregateReducerSchema,
11764
+ members: array(HelperMemberSchema).min(1).max(64),
11765
+ /** Actuator groups only. Readings leave this unset. */
11766
+ active: ActiveCriterionSchema.optional()
11767
+ }),
11768
+ object({
11769
+ kind: literal("appliance"),
11770
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
11771
+ appliance: _enum(APPLIANCE_KINDS),
11772
+ thresholdWatts: number().positive(),
11773
+ startDwellMs: number().int().nonnegative(),
11774
+ stopDwellMs: number().int().nonnegative(),
11775
+ member: HelperMemberSchema
11776
+ }),
11777
+ object({
11778
+ kind: literal("statistic"),
11779
+ stat: _enum(STATISTIC_STATS),
11780
+ window: StatisticWindowSchema,
11781
+ source: HelperMemberSchema
11782
+ }),
11783
+ object({
11784
+ kind: literal("integral"),
11785
+ reset: _enum(RESET_PERIODS),
11786
+ output: _enum(["energy", "meter"]),
11787
+ source: HelperMemberSchema
11788
+ }),
11789
+ object({
11790
+ kind: literal("threshold"),
11791
+ source: HelperMemberSchema,
11792
+ high: number(),
11793
+ low: number(),
11794
+ onForMs: number().int().nonnegative(),
11795
+ offForMs: number().int().nonnegative()
11796
+ }),
11797
+ object({
11798
+ kind: literal("arithmetic"),
11799
+ op: _enum([
11800
+ "sum",
11801
+ "difference",
11802
+ "ratio"
11803
+ ]),
11804
+ inputs: array(HelperMemberSchema).min(2).max(64)
11805
+ }),
11806
+ object({
11807
+ kind: literal("expression"),
11808
+ expr: string().min(1),
11809
+ inputs: array(object({
11810
+ name: string().min(1),
11811
+ member: HelperMemberSchema
11812
+ })).min(1).max(64),
11813
+ output: _enum(["number", "boolean"])
11814
+ }),
11815
+ object({
11816
+ kind: literal("derivative"),
11817
+ window: RollingWindowSchema,
11818
+ per: _enum([
11819
+ "second",
11820
+ "minute",
11821
+ "hour"
11822
+ ]),
11823
+ source: HelperMemberSchema
11824
+ })
11825
+ ]);
11826
+ /** A composed device's stableId is this prefix plus the block id. */
11827
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11828
+ /** Longest snippet a `code` source may carry (slice 4). */
11829
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11569
11830
  /** One field of one capability of one source device. */
11570
11831
  var CompositionFieldReadSchema = object({
11571
11832
  source: CompositionSourceRefSchema,
@@ -11609,7 +11870,59 @@ var CompositionCodeSourceSchema = object({
11609
11870
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11610
11871
  CompositionFromSourceSchema,
11611
11872
  CompositionExpressionSourceSchema,
11612
- CompositionCodeSourceSchema
11873
+ CompositionCodeSourceSchema,
11874
+ object({
11875
+ kind: literal("aggregate"),
11876
+ reducer: _enum([
11877
+ "mean",
11878
+ "median",
11879
+ "min",
11880
+ "max",
11881
+ "sum",
11882
+ "any",
11883
+ "all"
11884
+ ]),
11885
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11886
+ /**
11887
+ * Set when the reads are an enum state, not a yes/no: the listed states
11888
+ * count as active (a cover is `open`). A state outside the list is inactive;
11889
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
11890
+ */
11891
+ activeWhen: array(string().min(1)).min(1).optional(),
11892
+ /** `value` is the reading; the others are the member counts. */
11893
+ publish: _enum([
11894
+ "value",
11895
+ "included",
11896
+ "excluded",
11897
+ "total"
11898
+ ]),
11899
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11900
+ empty: _enum([
11901
+ "null",
11902
+ "omit",
11903
+ "offline"
11904
+ ])
11905
+ }),
11906
+ object({
11907
+ kind: literal("series"),
11908
+ read: CompositionFieldReadSchema,
11909
+ /** `value` is the statistic or the integral. The others are the gap counts. */
11910
+ publish: _enum([
11911
+ "value",
11912
+ "included",
11913
+ "excluded",
11914
+ "total"
11915
+ ]),
11916
+ /**
11917
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
11918
+ * device unseeded until history answers, so a restart cannot publish 0.
11919
+ */
11920
+ empty: _enum([
11921
+ "null",
11922
+ "omit",
11923
+ "offline"
11924
+ ])
11925
+ })
11613
11926
  ]);
11614
11927
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11615
11928
  kind: literal("forward"),
@@ -11655,7 +11968,13 @@ var CompositionSchema = object({
11655
11968
  kind: literal("existing"),
11656
11969
  device: CompositionSourceRefSchema
11657
11970
  })]),
11658
- features: array(CompositionFeatureSchema).min(1).max(16)
11971
+ features: array(CompositionFeatureSchema).min(1).max(16),
11972
+ /**
11973
+ * Present when the block is a helper template. Absent on a free
11974
+ * composition (the room example, device 4603). The editor reads this,
11975
+ * never the compiled features.
11976
+ */
11977
+ helper: HelperSpecSchema.optional()
11659
11978
  }).superRefine((composition, ctx) => {
11660
11979
  const seen = /* @__PURE__ */ new Set();
11661
11980
  composition.features.forEach((feature, index) => {
@@ -16762,7 +17081,22 @@ var BrokerProviderInfoSchema = object({
16762
17081
  */
16763
17082
  interactiveConnectPath: string().min(1).optional(),
16764
17083
  /** Relative hub path to disconnect the linked account (same popup pattern). */
16765
- interactiveDisconnectPath: string().min(1).optional()
17084
+ interactiveDisconnectPath: string().min(1).optional(),
17085
+ /**
17086
+ * The kind signs in through a server-held interactive session instead of
17087
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17088
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17089
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17090
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17091
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17092
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17093
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17094
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17095
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17096
+ * offers it as the default path; the fields the session fills are
17097
+ * marked `filledBySignIn` in the kind's settings schema.
17098
+ */
17099
+ signIn: object({ basePath: string().min(1) }).optional()
16766
17100
  }))
16767
17101
  });
16768
17102
  var ListInputSchema = object({
@@ -17863,7 +18197,12 @@ var CompositionProblemSchema = object({
17863
18197
  "item-field-reserved",
17864
18198
  "target-missing",
17865
18199
  "target-unreadable",
17866
- "not-replaceable"
18200
+ "not-replaceable",
18201
+ "native-capability-replace",
18202
+ "mixed-quantity",
18203
+ "helper-mismatch",
18204
+ "member-constraint",
18205
+ "retention"
17867
18206
  ]),
17868
18207
  path: string(),
17869
18208
  message: string()
@@ -17884,7 +18223,8 @@ var CompositionUnavailableModeSchema = _enum([
17884
18223
  "null",
17885
18224
  "offline",
17886
18225
  "unknown",
17887
- "release"
18226
+ "release",
18227
+ "omit"
17888
18228
  ]);
17889
18229
  /**
17890
18230
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17921,9 +18261,11 @@ var CompositionFieldPlanSchema = object({
17921
18261
  reads: array(CompositionFieldReadSchema)
17922
18262
  });
17923
18263
  /**
17924
- * What a feature does to its target (D663): `new` builds a capability on a new
17925
- * device; on an existing device it `add`s a capability the device lacks, or
17926
- * `replace`s named fields of one it has natively.
18264
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18265
+ * new device; on an existing device it `add`s a capability the device lacks.
18266
+ * Replacing a capability the device already serves is refused
18267
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18268
+ * wire for a claim the hub already holds.
17927
18269
  */
17928
18270
  var CompositionFeatureModeSchema = _enum([
17929
18271
  "new",
@@ -18094,8 +18436,392 @@ Object.freeze({
18094
18436
  latchedAt: {
18095
18437
  kind: "fixed",
18096
18438
  result: ["number", "null"]
18439
+ },
18440
+ schmitt: {
18441
+ kind: "fixed",
18442
+ result: ["boolean", "null"]
18097
18443
  }
18098
18444
  });
18445
+ /**
18446
+ * One catalog row per base reading a group helper can build (D944 §3).
18447
+ * Data, not a form default written into the block.
18448
+ */
18449
+ var NOW$2 = {
18450
+ fieldPath: "lastFetchedAt",
18451
+ expr: "now"
18452
+ };
18453
+ var CHANGED = {
18454
+ fieldPath: "lastChangedAt",
18455
+ expr: "now"
18456
+ };
18457
+ function numeric(input) {
18458
+ return {
18459
+ ...input,
18460
+ family: "numeric",
18461
+ deviceType: DeviceType.Sensor,
18462
+ memberDeviceType: DeviceType.Sensor,
18463
+ memberDeviceTypes: [DeviceType.Sensor],
18464
+ activeWhen: void 0,
18465
+ commandCaps: [],
18466
+ baseUnit: baseUnitOf(input.quantity),
18467
+ aggregates: NUMERIC_AGGREGATES,
18468
+ accept: [{
18469
+ cap: input.outputCap,
18470
+ fieldPath: input.outputField
18471
+ }]
18472
+ };
18473
+ }
18474
+ function binary(input) {
18475
+ return {
18476
+ ...input,
18477
+ family: "binary",
18478
+ deviceType: DeviceType.Sensor,
18479
+ memberDeviceType: DeviceType.Sensor,
18480
+ memberDeviceTypes: [DeviceType.Sensor],
18481
+ activeWhen: void 0,
18482
+ commandCaps: [],
18483
+ quantity: "none",
18484
+ baseUnit: void 0,
18485
+ aggregates: BINARY_AGGREGATES,
18486
+ empty: "offline"
18487
+ };
18488
+ }
18489
+ var GROUP_TEMPLATES = [
18490
+ numeric({
18491
+ id: "temperature",
18492
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18493
+ role: DeviceRole.TemperatureSensor,
18494
+ outputCap: "temperature-sensor",
18495
+ outputField: "celsius",
18496
+ quantity: "temperature",
18497
+ defaultAggregate: "mean",
18498
+ empty: "null",
18499
+ companions: [NOW$2]
18500
+ }),
18501
+ numeric({
18502
+ id: "humidity",
18503
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18504
+ role: DeviceRole.HumiditySensor,
18505
+ outputCap: "humidity-sensor",
18506
+ outputField: "percent",
18507
+ quantity: "humidity",
18508
+ defaultAggregate: "mean",
18509
+ empty: "null",
18510
+ companions: [NOW$2]
18511
+ }),
18512
+ numeric({
18513
+ id: "pressure",
18514
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18515
+ role: DeviceRole.PressureSensor,
18516
+ outputCap: "pressure-sensor",
18517
+ outputField: "hpa",
18518
+ quantity: "pressure",
18519
+ defaultAggregate: "mean",
18520
+ empty: "null",
18521
+ companions: [NOW$2]
18522
+ }),
18523
+ numeric({
18524
+ id: "illuminance",
18525
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18526
+ role: DeviceRole.AmbientLightSensor,
18527
+ outputCap: "ambient-light-sensor",
18528
+ outputField: "lux",
18529
+ quantity: "illuminance",
18530
+ defaultAggregate: "mean",
18531
+ empty: "null",
18532
+ companions: [NOW$2]
18533
+ }),
18534
+ numeric({
18535
+ id: "power",
18536
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18537
+ role: DeviceRole.PowerSensor,
18538
+ outputCap: "power-meter",
18539
+ outputField: "watts",
18540
+ quantity: "power",
18541
+ defaultAggregate: "sum",
18542
+ empty: "omit",
18543
+ companions: [NOW$2]
18544
+ }),
18545
+ numeric({
18546
+ id: "energy",
18547
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18548
+ role: DeviceRole.EnergySensor,
18549
+ outputCap: "power-meter",
18550
+ outputField: "kwhTotal",
18551
+ quantity: "energy",
18552
+ defaultAggregate: "sum",
18553
+ empty: "omit",
18554
+ companions: [NOW$2]
18555
+ }),
18556
+ numeric({
18557
+ id: "battery",
18558
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18559
+ role: DeviceRole.BatterySensor,
18560
+ outputCap: "battery",
18561
+ outputField: "percentage",
18562
+ quantity: "percentage",
18563
+ defaultAggregate: "mean",
18564
+ empty: "null",
18565
+ companions: [
18566
+ {
18567
+ fieldPath: "charging",
18568
+ expr: "\"unknown\""
18569
+ },
18570
+ {
18571
+ fieldPath: "sleeping",
18572
+ expr: "false"
18573
+ },
18574
+ {
18575
+ fieldPath: "lastUpdated",
18576
+ expr: "now"
18577
+ }
18578
+ ]
18579
+ }),
18580
+ numeric({
18581
+ id: "numeric",
18582
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18583
+ role: DeviceRole.NumericSensor,
18584
+ outputCap: "numeric-sensor",
18585
+ outputField: "value",
18586
+ quantity: "device",
18587
+ defaultAggregate: "mean",
18588
+ empty: "null",
18589
+ companions: [NOW$2]
18590
+ }),
18591
+ binary({
18592
+ id: "motion",
18593
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18594
+ role: DeviceRole.MotionSensor,
18595
+ outputCap: "motion",
18596
+ outputField: "detected",
18597
+ defaultAggregate: "any",
18598
+ accept: [{
18599
+ cap: "motion",
18600
+ fieldPath: "detected"
18601
+ }],
18602
+ companions: [{
18603
+ fieldPath: "lastDetectedAt",
18604
+ expr: "null"
18605
+ }, {
18606
+ fieldPath: "autoClearAfterMs",
18607
+ expr: "null"
18608
+ }]
18609
+ }),
18610
+ binary({
18611
+ id: "contact",
18612
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18613
+ role: DeviceRole.ContactSensor,
18614
+ outputCap: "contact",
18615
+ outputField: "entryOpen",
18616
+ defaultAggregate: "any",
18617
+ accept: [{
18618
+ cap: "contact",
18619
+ fieldPath: "entryOpen"
18620
+ }],
18621
+ companions: [CHANGED]
18622
+ }),
18623
+ binary({
18624
+ id: "leak",
18625
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18626
+ role: DeviceRole.LeakSensor,
18627
+ outputCap: "flood",
18628
+ outputField: "flooded",
18629
+ defaultAggregate: "any",
18630
+ accept: [{
18631
+ cap: "flood",
18632
+ fieldPath: "flooded"
18633
+ }],
18634
+ companions: [CHANGED]
18635
+ }),
18636
+ binary({
18637
+ id: "smoke",
18638
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18639
+ role: DeviceRole.SmokeSensor,
18640
+ outputCap: "smoke",
18641
+ outputField: "detected",
18642
+ defaultAggregate: "any",
18643
+ accept: [{
18644
+ cap: "smoke",
18645
+ fieldPath: "detected"
18646
+ }],
18647
+ companions: [CHANGED]
18648
+ }),
18649
+ binary({
18650
+ id: "occupancy",
18651
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18652
+ role: DeviceRole.BinarySensor,
18653
+ outputCap: "binary",
18654
+ outputField: "on",
18655
+ defaultAggregate: "any",
18656
+ accept: [{
18657
+ cap: "motion",
18658
+ fieldPath: "detected"
18659
+ }, {
18660
+ cap: "binary",
18661
+ fieldPath: "on"
18662
+ }],
18663
+ companions: [CHANGED]
18664
+ }),
18665
+ binary({
18666
+ id: "binary",
18667
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18668
+ role: DeviceRole.BinarySensor,
18669
+ outputCap: "binary",
18670
+ outputField: "on",
18671
+ defaultAggregate: "any",
18672
+ accept: [{
18673
+ cap: "binary",
18674
+ fieldPath: "on"
18675
+ }],
18676
+ companions: [CHANGED]
18677
+ }),
18678
+ actuator({
18679
+ id: "switch",
18680
+ 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.",
18681
+ deviceType: DeviceType.Switch,
18682
+ outputCap: "switch",
18683
+ outputField: "on",
18684
+ member: {
18685
+ cap: "switch",
18686
+ fieldPath: "on"
18687
+ },
18688
+ commandCaps: ["switch"]
18689
+ }),
18690
+ actuator({
18691
+ id: "light",
18692
+ 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.",
18693
+ deviceType: DeviceType.Light,
18694
+ outputCap: "switch",
18695
+ outputField: "on",
18696
+ member: {
18697
+ cap: "switch",
18698
+ fieldPath: "on"
18699
+ },
18700
+ commandCaps: [
18701
+ "switch",
18702
+ "brightness",
18703
+ "color"
18704
+ ]
18705
+ }),
18706
+ actuator({
18707
+ id: "fan",
18708
+ 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.",
18709
+ deviceType: DeviceType.Fan,
18710
+ outputCap: "switch",
18711
+ outputField: "on",
18712
+ member: {
18713
+ cap: "switch",
18714
+ fieldPath: "on"
18715
+ },
18716
+ commandCaps: ["switch", "fan-control"]
18717
+ }),
18718
+ actuator({
18719
+ id: "cover",
18720
+ 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.",
18721
+ deviceType: DeviceType.Cover,
18722
+ outputCap: "group-active",
18723
+ outputField: "active",
18724
+ member: {
18725
+ cap: "cover",
18726
+ fieldPath: "state"
18727
+ },
18728
+ activeWhen: [
18729
+ "open",
18730
+ "opening",
18731
+ "stopped"
18732
+ ],
18733
+ commandCaps: ["cover"]
18734
+ }),
18735
+ actuator({
18736
+ id: "valve",
18737
+ 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.",
18738
+ deviceType: DeviceType.Valve,
18739
+ outputCap: "group-active",
18740
+ outputField: "active",
18741
+ member: {
18742
+ cap: "valve",
18743
+ fieldPath: "state"
18744
+ },
18745
+ activeWhen: [
18746
+ "open",
18747
+ "opening",
18748
+ "stopped"
18749
+ ],
18750
+ commandCaps: ["valve"]
18751
+ }),
18752
+ actuator({
18753
+ id: "lock",
18754
+ 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.",
18755
+ deviceType: DeviceType.Lock,
18756
+ outputCap: "group-active",
18757
+ outputField: "active",
18758
+ member: {
18759
+ cap: "lock-control",
18760
+ fieldPath: "state"
18761
+ },
18762
+ activeWhen: ["locked"],
18763
+ commandCaps: ["lock-control"]
18764
+ }),
18765
+ actuator({
18766
+ id: "media-player",
18767
+ 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.",
18768
+ deviceType: DeviceType.MediaPlayer,
18769
+ outputCap: "group-active",
18770
+ outputField: "active",
18771
+ member: {
18772
+ cap: "media-player",
18773
+ fieldPath: "state"
18774
+ },
18775
+ activeWhen: ["playing", "buffering"],
18776
+ commandCaps: ["media-player"]
18777
+ }),
18778
+ actuator({
18779
+ id: "climate",
18780
+ 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.",
18781
+ deviceType: DeviceType.Thermostat,
18782
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
18783
+ outputCap: "group-active",
18784
+ outputField: "active",
18785
+ member: {
18786
+ cap: "climate-control",
18787
+ fieldPath: "mode"
18788
+ },
18789
+ activeWhen: [
18790
+ "heat",
18791
+ "cool",
18792
+ "auto",
18793
+ "heat_cool",
18794
+ "fan_only",
18795
+ "dry"
18796
+ ],
18797
+ commandCaps: ["climate-control"]
18798
+ })
18799
+ ];
18800
+ function actuator(input) {
18801
+ return {
18802
+ id: input.id,
18803
+ summary: input.summary,
18804
+ deviceType: input.deviceType,
18805
+ outputCap: input.outputCap,
18806
+ outputField: input.outputField,
18807
+ commandCaps: input.commandCaps,
18808
+ family: "actuator",
18809
+ role: void 0,
18810
+ memberDeviceType: input.deviceType,
18811
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
18812
+ activeWhen: input.activeWhen,
18813
+ quantity: "none",
18814
+ baseUnit: void 0,
18815
+ aggregates: BINARY_AGGREGATES,
18816
+ defaultAggregate: "any",
18817
+ empty: "offline",
18818
+ accept: [input.member],
18819
+ companions: [CHANGED]
18820
+ };
18821
+ }
18822
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18823
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18824
+ DeviceType.Camera;
18099
18825
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18100
18826
  /**
18101
18827
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19922,12 +20648,12 @@ function buildDeviceAdminUrl(parts) {
19922
20648
  * providers detect renames by matching `uniqueId` and patch `id`
19923
20649
  * in-place so dispatch keeps working.
19924
20650
  *
19925
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19926
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19927
- * state push and are the single source of truth. The `device_class` tag is
19928
- * an HA-specific input that is consumed at the discovery boundary
19929
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19930
- * stored after that point.
20651
+ * **Display unit and precision are NOT stored here.** They are a read-time
20652
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20653
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20654
+ * neither is repeated on each sample. The `device_class` tag is an
20655
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20656
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19931
20657
  *
19932
20658
  * The `raw` escape hatch carries provider-specific attributes that don't
19933
20659
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19941,6 +20667,14 @@ var SourceInfoSchema = object({
19941
20667
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19942
20668
  * Used to detect rename when `id` changes. */
19943
20669
  uniqueId: string().optional(),
20670
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20671
+ quantity: ReadingQuantitySchema.optional(),
20672
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20673
+ dateTimeShape: _enum([
20674
+ "date",
20675
+ "time",
20676
+ "datetime"
20677
+ ]).optional(),
19944
20678
  /** Provider-specific extras that don't fit the structured slots.
19945
20679
  * Whitelist what goes here per provider — do NOT dump entire HA
19946
20680
  * attribute blobs (would explode the meta JSON column). */
@@ -41045,7 +41779,8 @@ method(object({
41045
41779
  "enum"
41046
41780
  ]),
41047
41781
  enumValues: array(string()).optional(),
41048
- item: boolean().optional()
41782
+ item: boolean().optional(),
41783
+ quantity: ReadingQuantitySchema.optional()
41049
41784
  })).readonly(),
41050
41785
  itemArray: object({
41051
41786
  path: string(),
@@ -43406,7 +44141,7 @@ var NetworkAccessStatusSchema = object({
43406
44141
  });
43407
44142
  /**
43408
44143
  * Optional, richer endpoint shape returned by providers that expose
43409
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44144
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43410
44145
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43411
44146
  * the originating provider config (mode + sourcePort) so the
43412
44147
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44889,8 +45624,8 @@ var MaskPolygonShapeSchema = object({
44889
45624
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44890
45625
  var MaskGridShapeSchema = object({
44891
45626
  kind: literal("grid"),
44892
- gridWidth: number(),
44893
- gridHeight: number(),
45627
+ gridWidth: number().meta(readingMeta("none")),
45628
+ gridHeight: number().meta(readingMeta("none")),
44894
45629
  cells: array(boolean())
44895
45630
  });
44896
45631
  discriminatedUnion("kind", [
@@ -45002,6 +45737,8 @@ var NcSystemEventKindSchema = _enum([
45002
45737
  "detection-blind",
45003
45738
  "motion-noisy",
45004
45739
  "motion-calm",
45740
+ "package-llm-silent",
45741
+ "package-llm-answered",
45005
45742
  "recording-stalled",
45006
45743
  "recording-healthy",
45007
45744
  "recording-degraded",
@@ -46091,7 +46828,20 @@ var NcRuleInputSchema = object({
46091
46828
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46092
46829
  * path does not run Zod defaults.
46093
46830
  */
46094
- groupingSec: number().int().min(5).max(60).optional()
46831
+ groupingSec: number().int().min(5).max(60).optional(),
46832
+ /**
46833
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46834
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46835
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46836
+ * same mark the Library writes, and records whose notification it was and
46837
+ * when. A channel with no buttons gets the same two choices as links.
46838
+ *
46839
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46840
+ * cap path): absent and `false` both mean off, which is what every rule
46841
+ * authored before this field meant. An editor turns it off by writing
46842
+ * `false`, so it needs no `clear` key.
46843
+ */
46844
+ askVerdict: boolean().optional()
46095
46845
  });
46096
46846
  /**
46097
46847
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -56093,6 +56843,28 @@ var PackageVersionInfoSchema = object({
56093
56843
  deprecated: string().optional(),
56094
56844
  distTags: array(string()).readonly()
56095
56845
  });
56846
+ /**
56847
+ * One published document. `ok` is markdown from the registry. `missing` means
56848
+ * the registry answered and the file was not there. `error` is a registry or
56849
+ * tarball failure — never an empty string standing in for either.
56850
+ */
56851
+ var PackageDocBodySchema = discriminatedUnion("status", [
56852
+ object({
56853
+ status: literal("ok"),
56854
+ markdown: string()
56855
+ }),
56856
+ object({ status: literal("missing") }),
56857
+ object({
56858
+ status: literal("error"),
56859
+ message: string()
56860
+ })
56861
+ ]);
56862
+ var PackageDocsSchema = object({
56863
+ name: string(),
56864
+ version: string(),
56865
+ readme: PackageDocBodySchema,
56866
+ changelog: PackageDocBodySchema
56867
+ });
56096
56868
  var SearchResultSchema = object({
56097
56869
  name: string(),
56098
56870
  version: string(),
@@ -56311,7 +57083,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56311
57083
  }), object({ success: literal(true) }), {
56312
57084
  kind: "mutation",
56313
57085
  auth: "admin"
56314
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57086
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57087
+ name: string().min(1),
57088
+ version: string().min(1).optional()
57089
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56315
57090
  kind: "mutation",
56316
57091
  auth: "admin"
56317
57092
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56370,27 +57145,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56370
57145
  */
56371
57146
  var AirQualitySensorStatusSchema = object({
56372
57147
  /** Carbon dioxide concentration in ppm. */
56373
- co2Ppm: number().min(0).optional(),
57148
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56374
57149
  /** Total volatile organic compounds in ppb. */
56375
- vocPpb: number().min(0).optional(),
57150
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56376
57151
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56377
- pm25: number().min(0).optional(),
57152
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56378
57153
  /** Particulate matter ≤ 10 μm in µg/m³. */
56379
- pm10: number().min(0).optional(),
57154
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56380
57155
  /** Composite AQI value (typically 0..500). */
56381
- aqi: number().optional(),
57156
+ aqi: number().optional().meta(readingMeta("index")),
56382
57157
  /** Ms epoch when the slice was last updated. */
56383
- lastFetchedAt: number(),
56384
- /** Live display unit of the single metric this slice carries (e.g. HA
56385
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56386
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56387
- * per slice is unambiguous. */
56388
- unit: string().optional(),
56389
- /** Suggested decimal places for numeric display.
56390
- * Populated live from the upstream source when provided (e.g. HA
56391
- * `attributes.suggested_display_precision`). Falls back to
56392
- * auto-formatting when absent. */
56393
- precision: number().int().min(0).max(10).optional()
57158
+ lastFetchedAt: number()
56394
57159
  });
56395
57160
  var airQualitySensorCapability = {
56396
57161
  name: "air-quality-sensor",
@@ -56427,12 +57192,12 @@ var airQualitySensorCapability = {
56427
57192
  },
56428
57193
  pm25: {
56429
57194
  kind: "gauge",
56430
- unit: "ug/m3",
57195
+ unit: "µg/m³",
56431
57196
  deadband: 1
56432
57197
  },
56433
57198
  pm10: {
56434
57199
  kind: "gauge",
56435
- unit: "ug/m3",
57200
+ unit: "µg/m³",
56436
57201
  deadband: 1
56437
57202
  },
56438
57203
  aqi: {
@@ -56450,22 +57215,13 @@ var airQualitySensorCapability = {
56450
57215
  * entries with `device_class: illuminance`.
56451
57216
  */
56452
57217
  var AmbientLightSensorStatusSchema = object({
56453
- /** Current illuminance in lux (lx). */
56454
- lux: number().min(0).meta({
56455
- unit: "lx",
57218
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57219
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57220
+ precision: 0,
56456
57221
  label: "Illuminance"
56457
- }),
57222
+ })),
56458
57223
  /** Ms epoch when the slice was last updated. */
56459
- lastFetchedAt: number(),
56460
- /** Live display unit from the upstream source (e.g. HA
56461
- * `attributes.unit_of_measurement`). The UI prefers this over the
56462
- * role's canonical unit. Absent → fall back to the canonical unit. */
56463
- unit: string().optional(),
56464
- /** Suggested decimal places for numeric display.
56465
- * Populated live from the upstream source when provided (e.g. HA
56466
- * `attributes.suggested_display_precision`). Falls back to
56467
- * auto-formatting when absent. */
56468
- precision: number().int().min(0).max(10).optional()
57224
+ lastFetchedAt: number()
56469
57225
  });
56470
57226
  var ambientLightSensorCapability = {
56471
57227
  name: "ambient-light-sensor",
@@ -56497,12 +57253,7 @@ var ambientLightSensorCapability = {
56497
57253
  } } },
56498
57254
  /** Clock fields: written, but excluded from the compare that decides
56499
57255
  * whether persisting is worth a SQLite commit. */
56500
- volatileStateFields: ["lastFetchedAt"],
56501
- customize: {
56502
- version: 1,
56503
- advanced: "derive",
56504
- targets: []
56505
- }
57256
+ volatileStateFields: ["lastFetchedAt"]
56506
57257
  };
56507
57258
  /**
56508
57259
  * Per-class audio metrics aggregated over a sliding window.
@@ -56729,10 +57480,7 @@ var BatteryStatusSchema = object({
56729
57480
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56730
57481
  * declines to band a non-finite reading, which is the correct shape.
56731
57482
  */
56732
- percentage: number().min(0).max(100).nullable().meta({
56733
- unit: "%",
56734
- label: "Charge"
56735
- }),
57483
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56736
57484
  /**
56737
57485
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56738
57486
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56759,7 +57507,14 @@ var BatteryStatusSchema = object({
56759
57507
  */
56760
57508
  sleeping: boolean().meta({ label: "Sleeping" }),
56761
57509
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56762
- lastUpdated: number(),
57510
+ lastUpdated: number().meta(readingMeta("none")),
57511
+ /**
57512
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57513
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57514
+ * that only report charge. A second reading on this cap, not a second
57515
+ * `numeric-sensor` on the same device.
57516
+ */
57517
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56763
57518
  /**
56764
57519
  * Ms epoch of the last time the device PROVED it was reachable — a
56765
57520
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56806,12 +57561,12 @@ var BATTERY_DEVICE_TYPES = [
56806
57561
  DeviceType.Switch,
56807
57562
  DeviceType.PetFeeder,
56808
57563
  DeviceType.Container,
56809
- DeviceType.Thermostat
57564
+ DeviceType.Thermostat,
57565
+ DeviceType.EventEmitter
56810
57566
  ];
56811
57567
  /** Generic Customize target for the percentage reading (D936). */
56812
57568
  var batteryCustomize = {
56813
57569
  version: 1,
56814
- advanced: "derive",
56815
57570
  targets: [{
56816
57571
  id: "percentage",
56817
57572
  cardinality: "scalar",
@@ -56820,7 +57575,7 @@ var batteryCustomize = {
56820
57575
  stateField: "percentage"
56821
57576
  },
56822
57577
  deviceTypes: BATTERY_DEVICE_TYPES,
56823
- apply: "add-or-replace",
57578
+ apply: "add",
56824
57579
  path: "percentage",
56825
57580
  sources: [{
56826
57581
  kind: "field",
@@ -56941,10 +57696,16 @@ onStatusChanged: { data: object({
56941
57696
  /**
56942
57697
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
56943
57698
  */
56944
- history: { fields: { percentage: {
56945
- kind: "gauge",
56946
- unit: "%"
56947
- } } },
57699
+ history: { fields: {
57700
+ percentage: {
57701
+ kind: "gauge",
57702
+ unit: "%"
57703
+ },
57704
+ volts: {
57705
+ kind: "gauge",
57706
+ unit: "V"
57707
+ }
57708
+ } },
56948
57709
  customize: batteryCustomize,
56949
57710
  /** Clock fields: written, but excluded from the compare that decides
56950
57711
  * whether persisting is worth a SQLite commit. */
@@ -56991,12 +57752,7 @@ var binaryCapability = {
56991
57752
  history: { fields: { on: { kind: "boolean" } } },
56992
57753
  /** Clock fields: written, but excluded from the compare that decides
56993
57754
  * whether persisting is worth a SQLite commit. */
56994
- volatileStateFields: ["lastChangedAt"],
56995
- customize: {
56996
- version: 1,
56997
- advanced: "derive",
56998
- targets: []
56999
- }
57755
+ volatileStateFields: ["lastChangedAt"]
57000
57756
  };
57001
57757
  /**
57002
57758
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57012,7 +57768,7 @@ var binaryCapability = {
57012
57768
  */
57013
57769
  var BrightnessStatusSchema = object({
57014
57770
  /** Current level as 0..100 inclusive. Firmware-reported. */
57015
- percentage: number().min(0).max(100),
57771
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57016
57772
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57017
57773
  lastChangedAt: number()
57018
57774
  });
@@ -57466,21 +58222,21 @@ var ClimateControlStatusSchema = object({
57466
58222
  /** Single setpoint in Celsius. Used by single-target modes
57467
58223
  * (heat / cool / dry). Null when the device is in a range mode
57468
58224
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57469
- target: number().nullable(),
58225
+ target: number().nullable().meta(readingMeta("temperature")),
57470
58226
  /** Upper bound of the dual setpoint in Celsius. Populated only
57471
58227
  * in `heat_cool` mode. */
57472
- targetHigh: number().nullable(),
58228
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57473
58229
  /** Lower bound of the dual setpoint in Celsius. Populated only
57474
58230
  * in `heat_cool` mode. */
57475
- targetLow: number().nullable(),
58231
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57476
58232
  /** Target relative humidity (0..100). Null when the device has no
57477
58233
  * humidity control. */
57478
- targetHumidity: number().min(0).max(100).nullable(),
58234
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57479
58235
  /** Current measured temperature in Celsius. Read-only — pushed by
57480
58236
  * the device's internal sensor. */
57481
- currentTemp: number().nullable(),
58237
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57482
58238
  /** Current measured relative humidity (0..100). Read-only. */
57483
- currentHumidity: number().min(0).max(100).nullable(),
58239
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57484
58240
  /** Vertical louver swing on/off. Null when the device has no vertical
57485
58241
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57486
58242
  swingVertical: boolean().nullable(),
@@ -57488,11 +58244,11 @@ var ClimateControlStatusSchema = object({
57488
58244
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57489
58245
  swingHorizontal: boolean().nullable(),
57490
58246
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57491
- minTemp: number().nullable().optional(),
58247
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57492
58248
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57493
- maxTemp: number().nullable().optional(),
58249
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57494
58250
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57495
- tempStep: number().positive().nullable().optional(),
58251
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57496
58252
  /** Ms epoch when the slice was last updated (push or command). */
57497
58253
  lastFetchedAt: number()
57498
58254
  });
@@ -57634,17 +58390,17 @@ var climateControlCapability = {
57634
58390
  * modes to expose in the picker without a slice fetch.
57635
58391
  */
57636
58392
  var RgbTripletSchema = object({
57637
- r: number().int().min(0).max(255),
57638
- g: number().int().min(0).max(255),
57639
- b: number().int().min(0).max(255)
58393
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58394
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58395
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57640
58396
  });
57641
58397
  var HsvTripletSchema = object({
57642
58398
  /** Hue in degrees, 0..360. */
57643
- h: number().min(0).max(360),
58399
+ h: number().min(0).max(360).meta(readingMeta("none")),
57644
58400
  /** Saturation as 0..100 inclusive. */
57645
- s: number().min(0).max(100),
58401
+ s: number().min(0).max(100).meta(readingMeta("none")),
57646
58402
  /** Value/brightness as 0..100 inclusive. */
57647
- v: number().min(0).max(100)
58403
+ v: number().min(0).max(100).meta(readingMeta("none"))
57648
58404
  });
57649
58405
  var ColorInputSchema = discriminatedUnion("mode", [
57650
58406
  object({
@@ -57673,7 +58429,7 @@ var ColorStatusSchema = object({
57673
58429
  /** Populated when `mode === 'hsv'`. */
57674
58430
  hsv: HsvTripletSchema.optional(),
57675
58431
  /** Populated when `mode === 'mired'`. */
57676
- mireds: number().int().optional(),
58432
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57677
58433
  /** Ms epoch of the last operator-driven change. */
57678
58434
  lastChangedAt: number()
57679
58435
  });
@@ -57868,18 +58624,12 @@ _enum([
57868
58624
  "empty",
57869
58625
  "full"
57870
58626
  ]);
57871
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57872
- * each. */
57873
- var ConsumableRemainingUnitSchema = _enum([
57874
- "days",
57875
- "hours",
57876
- "cycles"
57877
- ]);
57878
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57879
- * days). `value` null = unknown (D393). */
58627
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58628
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58629
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57880
58630
  var ConsumableRemainingSchema = object({
57881
- value: number().min(0).nullable(),
57882
- unit: ConsumableRemainingUnitSchema
58631
+ value: number().min(0).nullable().meta(readingMeta("device")),
58632
+ kind: ConsumableRemainingKindSchema
57883
58633
  });
57884
58634
  /** A single consumable item. Either a continuous `level` (remaining
57885
58635
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57892,7 +58642,7 @@ var ConsumableItemSchema = object({
57892
58642
  /** Display name. */
57893
58643
  label: string().min(1),
57894
58644
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57895
- level: number().min(0).max(100).nullable(),
58645
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57896
58646
  /** The provider's discrete verdict when known (binary mode). */
57897
58647
  status: ConsumableStatusSchema.nullable(),
57898
58648
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57974,8 +58724,8 @@ var consumablesCustomize = {
57974
58724
  transforms: [{ kind: "identity" }]
57975
58725
  }],
57976
58726
  literals: [{
57977
- path: "remaining.unit",
57978
- values: [...ConsumableRemainingUnitSchema.options]
58727
+ path: "remaining.kind",
58728
+ values: [...ConsumableRemainingKindSchema.options]
57979
58729
  }]
57980
58730
  },
57981
58731
  {
@@ -58104,7 +58854,6 @@ reset: method(object({
58104
58854
  },
58105
58855
  "remaining.value": {
58106
58856
  kind: "gauge",
58107
- unitFrom: "remaining.unit",
58108
58857
  deadband: 1,
58109
58858
  displayName: "Remaining"
58110
58859
  }
@@ -58174,10 +58923,9 @@ var contactCapability = {
58174
58923
  * (empty array for the other kinds). It lives in the slice because
58175
58924
  * HA `select.*` entities may update their options at runtime.
58176
58925
  *
58177
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58178
- * `kind === 'numeric'` and are absent (undefined) for all other
58179
- * kinds. They are populated live from HA attributes on each state
58180
- * push — consistent with `options` and the sensor-unit approach.
58926
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58927
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58928
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58181
58929
  */
58182
58930
  var ControlStatusSchema = object({
58183
58931
  kind: _enum([
@@ -58189,39 +58937,17 @@ var ControlStatusSchema = object({
58189
58937
  /** Current value. Numeric kind → number; select/text/datetime → string.
58190
58938
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58191
58939
  * or time-only — HA accepts all three forms). */
58192
- value: union([number(), string()]),
58940
+ value: union([number().meta(readingMeta("device")), string()]),
58193
58941
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58194
58942
  options: array(string()),
58195
58943
  /** Ms epoch when the slice was last updated. */
58196
58944
  lastChangedAt: number(),
58197
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58198
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58199
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58200
- unit: string().optional(),
58201
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58202
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58203
- min: number().optional(),
58204
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58205
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58206
- max: number().optional(),
58207
- /** Step increment. Populated live from HA `attributes.step`.
58208
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58209
- step: number().optional(),
58210
- /** Suggested decimal places for numeric display. Populated live from HA
58211
- * `attributes.suggested_display_precision`, or derived from `step` when the
58212
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58213
- * other kinds. Falls back to auto-formatting when absent. */
58214
- precision: number().int().min(0).max(10).optional(),
58215
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58216
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58217
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58218
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58219
- * `has_date`/`has_time` attributes on each push. */
58220
- format: _enum([
58221
- "date",
58222
- "time",
58223
- "datetime"
58224
- ]).optional()
58945
+ /** Minimum allowed value, in the device quantity's base unit. */
58946
+ min: number().optional().meta(readingMeta("device")),
58947
+ /** Maximum allowed value, in the device quantity's base unit. */
58948
+ max: number().optional().meta(readingMeta("device")),
58949
+ /** Step increment, in the device quantity's base unit. */
58950
+ step: number().optional().meta(readingMeta("device"))
58225
58951
  });
58226
58952
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58227
58953
  object({
@@ -58287,9 +59013,9 @@ var CoverStatusSchema = object({
58287
59013
  ]),
58288
59014
  /** 0 = fully closed, 100 = fully open. Null when the device has
58289
59015
  * no intermediate position surface. */
58290
- position: number().min(0).max(100).nullable(),
59016
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58291
59017
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58292
- tiltPosition: number().min(0).max(100).nullable(),
59018
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58293
59019
  /** Ms epoch when the slice was last updated. */
58294
59020
  lastChangedAt: number()
58295
59021
  });
@@ -58392,9 +59118,9 @@ var NormalizedRangeSchema$1 = object({
58392
59118
  var DayNightStatusSchema = object({
58393
59119
  mode: DayNightModeSchema,
58394
59120
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58395
- sensitivity: number().optional(),
59121
+ sensitivity: number().optional().meta(readingMeta("none")),
58396
59122
  /** Delay before the IR-cut filter flips, in seconds. */
58397
- switchDelaySec: number().optional(),
59123
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58398
59124
  lastFetchedAt: number()
58399
59125
  });
58400
59126
  /**
@@ -58612,11 +59338,11 @@ var DishwasherStatusSchema = object({
58612
59338
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58613
59339
  availablePrograms: array(string()),
58614
59340
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58615
- remainingSeconds: number().int().nonnegative().nullable(),
59341
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58616
59342
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58617
- delayedStartSeconds: number().int().nonnegative().nullable(),
59343
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58618
59344
  /** Cycle completion 0..100, or null when not applicable. */
58619
- progressPercent: number().min(0).max(100).nullable(),
59345
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58620
59346
  /** True when the door is reported open; null when the door state is not known. */
58621
59347
  doorOpen: boolean().nullable(),
58622
59348
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58640,7 +59366,7 @@ var DishwasherStatusSchema = object({
58640
59366
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58641
59367
  machineCareDue: boolean().nullable(),
58642
59368
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58643
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59369
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58644
59370
  /** Option states: null when the appliance does not expose the option or has not said. */
58645
59371
  halfLoad: boolean().nullable(),
58646
59372
  hygienePlus: boolean().nullable(),
@@ -58655,7 +59381,7 @@ var DishwasherStatusSchema = object({
58655
59381
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58656
59382
  activeEvents: array(string()),
58657
59383
  /** Numeric error code when the appliance reports one; null otherwise. */
58658
- errorCode: number().nullable(),
59384
+ errorCode: number().nullable().meta(readingMeta("none")),
58659
59385
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58660
59386
  errorLabel: string().nullable(),
58661
59387
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58886,7 +59612,7 @@ var DoorbellStatusSchema = object({
58886
59612
  /** Ms epoch of the last press. null = never observed since this provider started. */
58887
59613
  lastPressedAt: number().nullable(),
58888
59614
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58889
- pressCountSinceStart: number()
59615
+ pressCountSinceStart: number().meta(readingMeta("count"))
58890
59616
  });
58891
59617
  var DoorbellPressEventSchema = object({
58892
59618
  deviceId: number(),
@@ -58951,17 +59677,7 @@ onPressed: {
58951
59677
  history: "none",
58952
59678
  customize: doorbellCustomize
58953
59679
  };
58954
- /**
58955
- * Enum-state sensor — a string value picked from a finite option set.
58956
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
58957
- * states, weather conditions, contact-source channels, mode strings).
58958
- * The option set is stable across observations; it is persisted in the
58959
- * device config blob (via the control slice at adoption) and is NOT
58960
- * stored in `sourceInfo`.
58961
- */
58962
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
58963
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
58964
- var EnumSensorDateTimeFormatSchema = _enum([
59680
+ _enum([
58965
59681
  "date",
58966
59682
  "time",
58967
59683
  "datetime"
@@ -58969,12 +59685,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
58969
59685
  var EnumSensorStatusSchema = object({
58970
59686
  value: string(),
58971
59687
  /**
58972
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
58973
- * as a locale date/time/datetime; absent for plain enum sensors. HA
58974
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
58975
- * a time-only sensor → `'time'`.
59688
+ * Set when `value` is a timestamp rather than an enum state, so history
59689
+ * does not learn ISO strings as states. The display shape (date / time /
59690
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
58976
59691
  */
58977
- format: EnumSensorDateTimeFormatSchema.optional(),
59692
+ timestamp: boolean().optional(),
58978
59693
  /** Ms epoch when the slice was last updated. */
58979
59694
  lastFetchedAt: number()
58980
59695
  });
@@ -59001,11 +59716,11 @@ var enumSensorCapability = {
59001
59716
  * History (D850): the value as an `enum` series (an appliance's program or
59002
59717
  * operation state, an HVAC action). The option set is per device, so the
59003
59718
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59004
- * `format` carries a timestamp, not a state, and is not recorded.
59719
+ * `timestamp` carries a clock, not a state, and is not recorded.
59005
59720
  */
59006
59721
  history: { fields: { value: {
59007
59722
  kind: "enum",
59008
- skipWhenSet: "format"
59723
+ skipWhenSet: "timestamp"
59009
59724
  } } },
59010
59725
  /** Clock fields: written, but excluded from the compare that decides
59011
59726
  * whether persisting is worth a SQLite commit. */
@@ -59025,13 +59740,13 @@ var EventFireSchema = object({
59025
59740
  deviceId: number(),
59026
59741
  eventType: string(),
59027
59742
  data: record(string(), unknown()).nullable(),
59028
- timestamp: number(),
59029
- seq: number()
59743
+ timestamp: number().meta(readingMeta("none")),
59744
+ seq: number().meta(readingMeta("count"))
59030
59745
  });
59031
59746
  var EventEmitterStatusSchema = object({
59032
59747
  eventTypes: array(string()),
59033
59748
  lastEvent: EventFireSchema.nullable(),
59034
- eventCountSinceStart: number()
59749
+ eventCountSinceStart: number().meta(readingMeta("count"))
59035
59750
  });
59036
59751
  var eventEmitterCapability = {
59037
59752
  name: "event-emitter",
@@ -59444,10 +60159,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59444
60159
  var FanControlStatusSchema = object({
59445
60160
  /** Active speed as 0..100 inclusive. Null when the device has no
59446
60161
  * speed surface (single-speed fan). */
59447
- percentage: number().min(0).max(100).nullable(),
60162
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59448
60163
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59449
60164
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59450
- percentageStep: number().positive().optional(),
60165
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59451
60166
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59452
60167
  * — empty string when no preset is active or supported. */
59453
60168
  preset: string(),
@@ -59568,7 +60283,7 @@ var FeatureProbeStatusSchema = object({
59568
60283
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59569
60284
  model: string().nullable(),
59570
60285
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59571
- channelCount: number().nullable(),
60286
+ channelCount: number().nullable().meta(readingMeta("count")),
59572
60287
  /**
59573
60288
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59574
60289
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59691,9 +60406,151 @@ var gasCapability = {
59691
60406
  * whether persisting is worth a SQLite commit. */
59692
60407
  volatileStateFields: ["lastChangedAt"]
59693
60408
  };
60409
+ var GroupCommandsSchema = object({
60410
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60411
+ results: array(object({
60412
+ deviceId: number().int().nonnegative(),
60413
+ cap: string().min(1),
60414
+ method: string().min(1),
60415
+ ok: boolean(),
60416
+ error: string().optional(),
60417
+ /** Ms epoch of the attempt. */
60418
+ at: number()
60419
+ })) });
60420
+ var groupCommandsCapability = {
60421
+ name: "group-commands",
60422
+ scope: "device",
60423
+ deviceNative: true,
60424
+ mode: "singleton",
60425
+ deviceTypes: [
60426
+ DeviceType.Switch,
60427
+ DeviceType.Light,
60428
+ DeviceType.Fan,
60429
+ DeviceType.Cover,
60430
+ DeviceType.Valve,
60431
+ DeviceType.Lock,
60432
+ DeviceType.MediaPlayer,
60433
+ DeviceType.Thermostat,
60434
+ DeviceType.Climate
60435
+ ],
60436
+ methods: {},
60437
+ status: {
60438
+ schema: GroupCommandsSchema,
60439
+ kind: "push"
60440
+ },
60441
+ runtimeState: GroupCommandsSchema,
60442
+ /**
60443
+ * Runtime-state durability: **session** — the last attempt. A restored
60444
+ * failure would outlive the command that caused it.
60445
+ *
60446
+ * See `RuntimeStateDurability`. Enforced by
60447
+ * `scripts/check-runtime-state-durability.ts`.
60448
+ */
60449
+ durability: "session",
60450
+ history: "none"
60451
+ };
60452
+ /**
60453
+ * Whether a helper actuator group counts as active (D944): at least one
60454
+ * included member is, or every included member is. What "active" means is the
60455
+ * type's: a cover is open, a lock is locked, a media player is playing, a
60456
+ * climate device is not off. A group of lights, switches and fans reports
60457
+ * `switch.on` instead; this cap is for the types whose own state is not a
60458
+ * yes/no. Unknown members are left out (`group-status` counts them) and an
60459
+ * empty included set takes the device offline, never `active: false`.
60460
+ */
60461
+ var GroupActiveSchema = object({
60462
+ active: boolean().meta({ label: "Active" }),
60463
+ /** Ms epoch when the slice was last updated. */
60464
+ lastChangedAt: number()
60465
+ });
60466
+ var groupActiveCapability = {
60467
+ name: "group-active",
60468
+ scope: "device",
60469
+ deviceNative: true,
60470
+ mode: "singleton",
60471
+ deviceTypes: [
60472
+ DeviceType.Cover,
60473
+ DeviceType.Valve,
60474
+ DeviceType.Lock,
60475
+ DeviceType.MediaPlayer,
60476
+ DeviceType.Climate,
60477
+ DeviceType.Thermostat
60478
+ ],
60479
+ methods: {},
60480
+ status: {
60481
+ schema: GroupActiveSchema,
60482
+ kind: "push"
60483
+ },
60484
+ runtimeState: GroupActiveSchema,
60485
+ /**
60486
+ * Runtime-state durability: **session** — recomputed from the members on the
60487
+ * next source event; a restored value would outlive the members it named.
60488
+ *
60489
+ * See `RuntimeStateDurability`. Enforced by
60490
+ * `scripts/check-runtime-state-durability.ts`.
60491
+ */
60492
+ durability: "session",
60493
+ history: "none"
60494
+ };
60495
+ /**
60496
+ * How many members of a helper group answered (D944). Counts are integers,
60497
+ * not measurements: zero included is a real count, never a stand-in reading.
60498
+ * The reading itself stays on the output capability. History is none — the
60499
+ * series that later statistics read is the output cap's own history block.
60500
+ */
60501
+ var GroupStatusSchema = object({
60502
+ /** Members whose current value entered the aggregate. */
60503
+ included: number().int().nonnegative().meta(readingMeta("count", {
60504
+ precision: 0,
60505
+ label: "Reporting"
60506
+ })),
60507
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60508
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60509
+ precision: 0,
60510
+ label: "Excluded"
60511
+ })),
60512
+ /** `included` + `excluded`. */
60513
+ total: number().int().nonnegative().meta(readingMeta("count", {
60514
+ precision: 0,
60515
+ label: "Members"
60516
+ }))
60517
+ });
60518
+ var groupStatusCapability = {
60519
+ name: "group-status",
60520
+ scope: "device",
60521
+ deviceNative: true,
60522
+ mode: "singleton",
60523
+ deviceTypes: [
60524
+ DeviceType.Sensor,
60525
+ DeviceType.Switch,
60526
+ DeviceType.Light,
60527
+ DeviceType.Fan,
60528
+ DeviceType.Cover,
60529
+ DeviceType.Valve,
60530
+ DeviceType.Lock,
60531
+ DeviceType.MediaPlayer,
60532
+ DeviceType.Thermostat,
60533
+ DeviceType.Climate
60534
+ ],
60535
+ methods: {},
60536
+ status: {
60537
+ schema: GroupStatusSchema,
60538
+ kind: "push"
60539
+ },
60540
+ runtimeState: GroupStatusSchema,
60541
+ /**
60542
+ * Runtime-state durability: **session** — recomputed from members on the
60543
+ * next source event; a restored count would outlive the readings it named.
60544
+ *
60545
+ * See `RuntimeStateDurability`. Enforced by
60546
+ * `scripts/check-runtime-state-durability.ts`.
60547
+ */
60548
+ durability: "session",
60549
+ history: "none"
60550
+ };
59694
60551
  var HeatingValveStatusSchema = object({
59695
60552
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59696
- position: number().min(0).max(100).nullable(),
60553
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59697
60554
  /** Ms epoch when the slice was last updated (push or poll). */
59698
60555
  lastFetchedAt: number()
59699
60556
  });
@@ -59736,9 +60593,9 @@ var HumidifierStatusSchema = object({
59736
60593
  /** Whether the humidifier is currently on. */
59737
60594
  on: boolean(),
59738
60595
  /** Current measured relative humidity (0..100). Null when not reported. */
59739
- currentHumidity: number().min(0).max(100).nullable(),
60596
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59740
60597
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59741
- targetHumidity: number().min(0).max(100).nullable(),
60598
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59742
60599
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59743
60600
  * device has no mode surface. */
59744
60601
  mode: string().nullable(),
@@ -59748,9 +60605,9 @@ var HumidifierStatusSchema = object({
59748
60605
  * `idle` / `off`). Null when the device doesn't report it. */
59749
60606
  action: string().nullable(),
59750
60607
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59751
- minHumidity: number().nullable(),
60608
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59752
60609
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59753
- maxHumidity: number().nullable(),
60610
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59754
60611
  /** Ms epoch when the slice was last updated. */
59755
60612
  lastChangedAt: number()
59756
60613
  });
@@ -59820,22 +60677,13 @@ var humidifierCapability = {
59820
60677
  * `sourceInfo`.
59821
60678
  */
59822
60679
  var HumiditySensorStatusSchema = object({
59823
- /** Current relative humidity, 0..100. */
59824
- percent: number().min(0).max(100).meta({
59825
- unit: "%",
60680
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60681
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60682
+ precision: 0,
59826
60683
  label: "Humidity"
59827
- }),
60684
+ })),
59828
60685
  /** Ms epoch when the slice was last updated. */
59829
- lastFetchedAt: number(),
59830
- /** Live display unit from the upstream source (e.g. HA
59831
- * `attributes.unit_of_measurement`). The UI prefers this over the
59832
- * role's canonical unit. Absent → fall back to the canonical unit. */
59833
- unit: string().optional(),
59834
- /** Suggested decimal places for numeric display.
59835
- * Populated live from the upstream source when provided (e.g. HA
59836
- * `attributes.suggested_display_precision`). Falls back to
59837
- * auto-formatting when absent. */
59838
- precision: number().int().min(0).max(10).optional()
60686
+ lastFetchedAt: number()
59839
60687
  });
59840
60688
  var humiditySensorCapability = {
59841
60689
  name: "humidity-sensor",
@@ -59866,12 +60714,7 @@ var humiditySensorCapability = {
59866
60714
  } } },
59867
60715
  /** Clock fields: written, but excluded from the compare that decides
59868
60716
  * whether persisting is worth a SQLite commit. */
59869
- volatileStateFields: ["lastFetchedAt"],
59870
- customize: {
59871
- version: 1,
59872
- advanced: "derive",
59873
- targets: []
59874
- }
60717
+ volatileStateFields: ["lastFetchedAt"]
59875
60718
  };
59876
60719
  /**
59877
60720
  * Image display cap. Models a single still image exposed by an integration —
@@ -59892,7 +60735,7 @@ var ImageStatusSchema = object({
59892
60735
  * entity exposes no `entity_picture` (yet). */
59893
60736
  url: string().nullable(),
59894
60737
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59895
- lastUpdated: number().nullable()
60738
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59896
60739
  });
59897
60740
  var imageCapability = {
59898
60741
  name: "image",
@@ -59980,19 +60823,19 @@ var NormalizedRangeSchema = object({
59980
60823
  */
59981
60824
  var ImageSettingsStatusSchema = object({
59982
60825
  /** Normalized 0–100. */
59983
- brightness: number().optional(),
60826
+ brightness: number().optional().meta(readingMeta("none")),
59984
60827
  /** Normalized 0–100. */
59985
- contrast: number().optional(),
60828
+ contrast: number().optional().meta(readingMeta("none")),
59986
60829
  /** Normalized 0–100. */
59987
- saturation: number().optional(),
60830
+ saturation: number().optional().meta(readingMeta("none")),
59988
60831
  /** Normalized 0–100. */
59989
- sharpness: number().optional(),
60832
+ sharpness: number().optional().meta(readingMeta("none")),
59990
60833
  mirror: boolean().optional(),
59991
60834
  flip: boolean().optional(),
59992
60835
  rotate: ImageRotateSchema.optional(),
59993
60836
  whiteBalance: WhiteBalanceModeSchema.optional(),
59994
60837
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
59995
- warmth: number().optional(),
60838
+ warmth: number().optional().meta(readingMeta("none")),
59996
60839
  exposureMode: ExposureModeSchema.optional(),
59997
60840
  backlightMode: BacklightModeSchema.optional(),
59998
60841
  lastFetchedAt: number()
@@ -60271,10 +61114,10 @@ var IntercomAbilitySchema = object({
60271
61114
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60272
61115
  codecs: array(string()),
60273
61116
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60274
- sampleRate: number(),
61117
+ sampleRate: number().meta(readingMeta("none")),
60275
61118
  duplex: _enum(["half", "full"]),
60276
61119
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60277
- maxBacklogMs: number()
61120
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60278
61121
  });
60279
61122
  var IntercomStatusSchema = object({
60280
61123
  /** True while a talk session is open. */
@@ -60501,13 +61344,13 @@ var LawnMowerControlStatusSchema = object({
60501
61344
  activity: LawnMowerActivitySchema,
60502
61345
  /** 0..100 battery percentage. Null when the device has no battery
60503
61346
  * reading. */
60504
- batteryLevel: number().min(0).max(100).nullable(),
61347
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60505
61348
  /** 0..100 mowing-completion percentage of the current task, or null when no
60506
61349
  * task is active / progress is unavailable. */
60507
- progressPercent: number().min(0).max(100).nullable(),
61350
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60508
61351
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60509
61352
  * or null when unknown. */
60510
- currentCode: number().nullable(),
61353
+ currentCode: number().nullable().meta(readingMeta("none")),
60511
61354
  /** Human label for {@link currentCode}, or null when undecodable. */
60512
61355
  currentCodeLabel: string().nullable(),
60513
61356
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -60960,7 +61803,14 @@ var lockControlCapability = {
60960
61803
  }),
60961
61804
  unlock: method(object({
60962
61805
  deviceId: number().int().nonnegative(),
60963
- code: string().min(1).optional()
61806
+ code: string().min(1).optional(),
61807
+ /**
61808
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
61809
+ * with one call is refused unless the caller says it was confirmed. A
61810
+ * single lock ignores it. The refusal lives in the callee, so no
61811
+ * automation or API caller reaches the doors by skipping a dialog.
61812
+ */
61813
+ confirm: boolean().optional()
60964
61814
  }), _void(), {
60965
61815
  kind: "mutation",
60966
61816
  auth: "protected"
@@ -60997,6 +61847,44 @@ var lockControlCapability = {
60997
61847
  volatileStateFields: ["lastChangedAt"]
60998
61848
  };
60999
61849
  /**
61850
+ * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
61851
+ * so dashboards keep one series; every other instantaneous reading lands here.
61852
+ * `value` is the time integral in the source quantity's accumulated base
61853
+ * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
61854
+ */
61855
+ var MeterStatusSchema = object({
61856
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
61857
+ lastFetchedAt: number()
61858
+ });
61859
+ var meterCapability = {
61860
+ name: "meter",
61861
+ scope: "device",
61862
+ deviceNative: true,
61863
+ mode: "singleton",
61864
+ deviceTypes: [DeviceType.Sensor],
61865
+ methods: {},
61866
+ status: {
61867
+ schema: MeterStatusSchema,
61868
+ kind: "push"
61869
+ },
61870
+ runtimeState: MeterStatusSchema,
61871
+ /**
61872
+ * Runtime-state durability: **restored** — the total is a cache of the
61873
+ * history integral. A restart shows it only until the history pass
61874
+ * recomputes the open period; it is not an in-memory buffer.
61875
+ *
61876
+ * See `RuntimeStateDurability`. Enforced by
61877
+ * `scripts/check-runtime-state-durability.ts`.
61878
+ */
61879
+ durability: "restored",
61880
+ history: { fields: { value: {
61881
+ kind: "counter",
61882
+ deadband: .001,
61883
+ minIntervalMs: 6e4
61884
+ } } },
61885
+ volatileStateFields: ["lastFetchedAt"]
61886
+ };
61887
+ /**
61000
61888
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
61001
61889
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
61002
61890
  * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
@@ -61040,7 +61928,7 @@ var MediaPlayerStatusSchema = object({
61040
61928
  state: MediaPlayerStateSchema,
61041
61929
  /** Volume as 0..100 inclusive. Null when the device has no volume
61042
61930
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61043
- volumeLevel: number().min(0).max(100).nullable(),
61931
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61044
61932
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61045
61933
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61046
61934
  isMuted: boolean().nullable(),
@@ -61053,10 +61941,10 @@ var MediaPlayerStatusSchema = object({
61053
61941
  currentMedia: MediaInfoSchema.nullable(),
61054
61942
  /** Current playback position in ms. Null when not seekable or
61055
61943
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61056
- positionMs: number().int().nonnegative().nullable(),
61944
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61057
61945
  /** Total duration of the current media in ms. Null when unknown
61058
61946
  * (live stream) or nothing is playing. */
61059
- durationMs: number().int().nonnegative().nullable(),
61947
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61060
61948
  /** Shuffle toggle. Null when no shuffle surface.
61061
61949
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61062
61950
  shuffle: boolean().nullable(),
@@ -61365,7 +62253,7 @@ var motionCustomize = {
61365
62253
  eventName: "onMotionChanged"
61366
62254
  },
61367
62255
  deviceTypes: [DeviceType.Camera],
61368
- apply: "replace",
62256
+ apply: "add",
61369
62257
  applyAuthority: {
61370
62258
  kind: "camera-sensors",
61371
62259
  timeoutField: "motionCooldownMs"
@@ -61437,7 +62325,7 @@ var MotionStatusSchema = object({
61437
62325
  * it, every provider seeded it at boot, and every session left it
61438
62326
  * `detected: true` after its detach.
61439
62327
  */
61440
- autoClearAfterMs: number().nullable()
62328
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61441
62329
  });
61442
62330
  /**
61443
62331
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61474,12 +62362,18 @@ var motionCapability = {
61474
62362
  methods: {
61475
62363
  /**
61476
62364
  * Pull the current motion state synchronously. Convenience shortcut
61477
- * for consumers that don't need the full status object; equivalent
61478
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61479
- * `getStatus` once the auto-injected status surface lands in every
61480
- * consumer.
62365
+ * for consumers that don't need the full status object. A composed
62366
+ * group answers from its slice (D944): unknown throws, it is not off.
61481
62367
  */
61482
62368
  isDetected: method(object({ deviceId: number() }), boolean()) },
62369
+ /**
62370
+ * A composed motion group serves `isDetected` from `detected` (D944).
62371
+ * Without this the cap stays uncomposable (`has-commands`).
62372
+ */
62373
+ composedMethods: { isDetected: {
62374
+ kind: "slice",
62375
+ field: "detected"
62376
+ } },
61483
62377
  events: {
61484
62378
  /**
61485
62379
  * Fires every time the runner transitions a camera between
@@ -61619,7 +62513,7 @@ var MotionZoneRegionSchema = object({
61619
62513
  * the grid region(s). */
61620
62514
  var MotionZoneStatusSchema = object({
61621
62515
  enabled: boolean(),
61622
- sensitivity: number(),
62516
+ sensitivity: number().meta(readingMeta("none")),
61623
62517
  /** Grid region(s). Today exactly one `grid` shape. */
61624
62518
  regions: array(MotionZoneRegionSchema),
61625
62519
  lastFetchedAt: number()
@@ -61989,7 +62883,7 @@ var NavigationStatusSchema = object({
61989
62883
  following: boolean(),
61990
62884
  flash: boolean(),
61991
62885
  lightMode: NavigationLightModeSchema,
61992
- lightLevel: number().min(40).max(100),
62886
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
61993
62887
  /** Ms epoch when the slice was last updated. */
61994
62888
  lastChangedAt: number()
61995
62889
  });
@@ -62123,13 +63017,13 @@ var NetworkLinkStatusSchema = object({
62123
63017
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62124
63018
  * SKIP a null rather than coerce it.
62125
63019
  */
62126
- signalPercent: number().min(0).max(100).nullable(),
63020
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62127
63021
  /** Raw received signal strength in dBm, when the firmware reports one. */
62128
- rssiDbm: number().optional(),
63022
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62129
63023
  /** Network name of a wireless link, when the firmware reports it. */
62130
63024
  ssid: string().optional(),
62131
63025
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62132
- lastUpdated: number()
63026
+ lastUpdated: number().meta(readingMeta("none"))
62133
63027
  });
62134
63028
  var networkLinkCapability = {
62135
63029
  name: "network-link",
@@ -62514,7 +63408,7 @@ var NotifierStatusSchema = object({
62514
63408
  lastError: string().nullable(),
62515
63409
  /** Number of deliveries currently buffered server-side (rate-limited
62516
63410
  * notifiers may queue). 0 when the provider sends synchronously. */
62517
- queueDepth: number().int().nonnegative(),
63411
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62518
63412
  /** Per-feature capability matrix — authoritative source for the
62519
63413
  * compose-form field gating. */
62520
63414
  supports: NotifierSupportsSchema
@@ -62606,11 +63500,9 @@ onSent: { data: object({
62606
63500
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62607
63501
  * for: water flow, distance, weight, frequency, signal strength, …).
62608
63502
  *
62609
- * The `unit` and `precision` fields carry the live display metadata that
62610
- * the upstream source provides with each state push (HA
62611
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62612
- * The UI reads them directly from the slice — they are the single source
62613
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63503
+ * The quantity is not on this slice. It is chosen when the device is
63504
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63505
+ * that quantity's base unit. `null` is a refused sample (D393).
62614
63506
  *
62615
63507
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62616
63508
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62619,15 +63511,8 @@ onSent: { data: object({
62619
63511
  * numeric values land here.
62620
63512
  */
62621
63513
  var NumericSensorStatusSchema = object({
62622
- value: number(),
62623
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62624
- * Populated live from the upstream source on each state push.
62625
- * Absent for unitless sensors. */
62626
- unit: string().optional(),
62627
- /** Suggested decimal places for numeric display.
62628
- * Populated live from the upstream source when provided.
62629
- * Falls back to auto-formatting when absent. */
62630
- precision: number().int().min(0).max(10).optional(),
63514
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63515
+ value: number().nullable().meta(readingMeta("device")),
62631
63516
  /** Ms epoch when the slice was last updated. */
62632
63517
  lastFetchedAt: number()
62633
63518
  });
@@ -62651,11 +63536,11 @@ var numericSensorCapability = {
62651
63536
  */
62652
63537
  durability: "restored",
62653
63538
  /**
62654
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63539
+ * History (D850): recorded series. The unit is the device quantity's base,
63540
+ * resolved when the series is read — the cap has no single unit.
62655
63541
  */
62656
63542
  history: { fields: { value: {
62657
63543
  kind: "gauge",
62658
- unitFrom: "unit",
62659
63544
  minIntervalMs: 3e4
62660
63545
  } } },
62661
63546
  /** Clock fields: written, but excluded from the compare that decides
@@ -62947,23 +63832,26 @@ var PetFeederStatusSchema = object({
62947
63832
  /** Food currently in the bowl (grams). Null when the device has not
62948
63833
  * reported a reading yet. On dual-hopper models this is the combined
62949
63834
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
62950
- foodLevel: number().nullable(),
63835
+ foodLevel: number().nullable().meta(readingMeta("none")),
62951
63836
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
62952
63837
  * single-hopper models. */
62953
- food1: number().nullable(),
63838
+ food1: number().nullable().meta(readingMeta("none")),
62954
63839
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
62955
63840
  * single-hopper models. */
62956
- food2: number().nullable(),
63841
+ food2: number().nullable().meta(readingMeta("none")),
62957
63842
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
62958
63843
  * (`device_class: problem`, on = low). True when the bowl is empty /
62959
63844
  * below the feeder's low threshold. */
62960
63845
  lowFood: boolean(),
62961
63846
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
62962
63847
  * device has no battery reading. */
62963
- batteryPower: number().min(0).max(100).nullable(),
62964
- /** Days of desiccant life remaining. Null when the model has no
62965
- * desiccant sensor. */
62966
- desiccantLeftDays: number().nullable(),
63848
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63849
+ /**
63850
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63851
+ * Providers convert from the days the feeder reports. Null when the
63852
+ * model has no desiccant sensor.
63853
+ */
63854
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
62967
63855
  /** True while a feed is in progress. */
62968
63856
  feeding: boolean(),
62969
63857
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -62974,7 +63862,7 @@ var PetFeederStatusSchema = object({
62974
63862
  * with `errorCode` for consumers that want the raw integer. */
62975
63863
  error: string().nullable(),
62976
63864
  /** Raw device error code (0 / null = no error). */
62977
- errorCode: number().nullable(),
63865
+ errorCode: number().nullable().meta(readingMeta("none")),
62978
63866
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
62979
63867
  isDualHopper: boolean(),
62980
63868
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -62984,7 +63872,7 @@ var PetFeederStatusSchema = object({
62984
63872
  /** Play a chime when dispensing. */
62985
63873
  feedSound: boolean(),
62986
63874
  /** Speaker / prompt volume level (device-scaled integer). */
62987
- volume: number(),
63875
+ volume: number().meta(readingMeta("none")),
62988
63876
  /** Ms epoch when the slice was last refreshed from the cloud. */
62989
63877
  lastFetchedAt: number()
62990
63878
  });
@@ -63103,7 +63991,7 @@ var petFeederCapability = {
63103
63991
  },
63104
63992
  desiccantLeftDays: {
63105
63993
  kind: "gauge",
63106
- unit: "days"
63994
+ unit: "s"
63107
63995
  }
63108
63996
  } }
63109
63997
  };
@@ -63504,43 +64392,88 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63504
64392
  */
63505
64393
  var PowerMeterStatusSchema = object({
63506
64394
  /** Instantaneous power draw in watts. */
63507
- watts: number().optional().meta({
63508
- unit: "W",
63509
- label: "Power"
63510
- }),
64395
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63511
64396
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63512
- kwhTotal: number().optional().meta({
63513
- unit: "kWh",
63514
- label: "Energy"
63515
- }),
64397
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63516
64398
  /** Voltage in volts. */
63517
- volts: number().optional(),
64399
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
63518
64400
  /** Current in amperes. */
63519
- amps: number().optional(),
63520
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63521
- va: number().optional(),
64401
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
64402
+ /** Apparent power in volt-amperes. */
64403
+ va: number().optional().meta(readingMeta("apparent-power")),
63522
64404
  /** Power factor (0..1) if reported. */
63523
- powerFactor: number().min(0).max(1).optional(),
64405
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63524
64406
  /** Ms epoch when the slice was last updated. */
63525
- lastFetchedAt: number(),
63526
- /** Live display unit of the single metric this slice carries (e.g. HA
63527
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63528
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63529
- * unit per slice is unambiguous. The UI prefers this over the role's
63530
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63531
- unit: string().optional(),
63532
- /** Suggested decimal places for numeric display.
63533
- * Populated live from the upstream source when provided (e.g. HA
63534
- * `attributes.suggested_display_precision`). Falls back to
63535
- * auto-formatting when absent. */
63536
- precision: number().int().min(0).max(10).optional()
64407
+ lastFetchedAt: number()
63537
64408
  });
64409
+ /**
64410
+ * Loads that draw metered mains power (D951). The reading is a feature of the
64411
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
64412
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
64413
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
64414
+ * camera and the like are left out: their draw is not what anyone meters.
64415
+ */
64416
+ var POWER_METER_DEVICE_TYPES = [
64417
+ DeviceType.Sensor,
64418
+ DeviceType.Switch,
64419
+ DeviceType.Light,
64420
+ DeviceType.Fan,
64421
+ DeviceType.Climate,
64422
+ DeviceType.Thermostat,
64423
+ DeviceType.Humidifier,
64424
+ DeviceType.WaterHeater,
64425
+ DeviceType.MediaPlayer,
64426
+ DeviceType.Washer,
64427
+ DeviceType.Dryer,
64428
+ DeviceType.Dishwasher,
64429
+ DeviceType.Vacuum,
64430
+ DeviceType.LawnMower,
64431
+ DeviceType.PetFeeder
64432
+ ];
64433
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
64434
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
64435
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
64436
+ function powerTarget(id, unit) {
64437
+ return {
64438
+ id,
64439
+ cardinality: "scalar",
64440
+ vocabulary: {
64441
+ cap: "power-meter",
64442
+ stateField: id
64443
+ },
64444
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
64445
+ apply: "add",
64446
+ path: id,
64447
+ sources: [{
64448
+ kind: "numeric",
64449
+ requiredUnit: unit,
64450
+ transforms: [{ kind: "identity" }]
64451
+ }],
64452
+ companions: [{
64453
+ kind: "clock",
64454
+ path: "lastFetchedAt",
64455
+ when: "always"
64456
+ }],
64457
+ sourceReport: { status: "assignment-status" },
64458
+ advancedExpression: true
64459
+ };
64460
+ }
64461
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
64462
+ var powerMeterCustomize = {
64463
+ version: 1,
64464
+ targets: [
64465
+ powerTarget("watts", "W"),
64466
+ powerTarget("kwhTotal", "kWh"),
64467
+ powerTarget("volts", "V"),
64468
+ powerTarget("amps", "A")
64469
+ ]
64470
+ };
63538
64471
  var powerMeterCapability = {
63539
64472
  name: "power-meter",
63540
64473
  scope: "device",
63541
64474
  deviceNative: true,
63542
64475
  mode: "singleton",
63543
- deviceTypes: [DeviceType.Sensor],
64476
+ deviceTypes: POWER_METER_DEVICE_TYPES,
63544
64477
  methods: {},
63545
64478
  status: {
63546
64479
  schema: PowerMeterStatusSchema,
@@ -63598,11 +64531,7 @@ var powerMeterCapability = {
63598
64531
  /** Clock fields: written, but excluded from the compare that decides
63599
64532
  * whether persisting is worth a SQLite commit. */
63600
64533
  volatileStateFields: ["lastFetchedAt"],
63601
- customize: {
63602
- version: 1,
63603
- advanced: "derive",
63604
- targets: []
63605
- }
64534
+ customize: powerMeterCustomize
63606
64535
  };
63607
64536
  /**
63608
64537
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63622,11 +64551,11 @@ var powerMeterCapability = {
63622
64551
  */
63623
64552
  var GpsLocationSchema = object({
63624
64553
  /** Latitude in decimal degrees, -90..90. */
63625
- latitude: number().min(-90).max(90),
64554
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63626
64555
  /** Longitude in decimal degrees, -180..180. */
63627
- longitude: number().min(-180).max(180),
64556
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63628
64557
  /** Reported accuracy in meters (lower = better). */
63629
- accuracyMeters: number().nonnegative()
64558
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63630
64559
  });
63631
64560
  var PresenceStatusSchema = object({
63632
64561
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63638,7 +64567,7 @@ var PresenceStatusSchema = object({
63638
64567
  gps: GpsLocationSchema.nullable(),
63639
64568
  /** Optional battery level of the tracking device (0..100). Null
63640
64569
  * when not reported. */
63641
- batteryPercent: number().min(0).max(100).nullable(),
64570
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63642
64571
  /** Ms epoch when the slice was last updated. */
63643
64572
  lastChangedAt: number()
63644
64573
  });
@@ -63688,19 +64617,10 @@ var presenceCapability = {
63688
64617
  * `sourceInfo`.
63689
64618
  */
63690
64619
  var PressureSensorStatusSchema = object({
63691
- /** Current pressure in hPa. */
63692
- hpa: number(),
64620
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64621
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63693
64622
  /** Ms epoch when the slice was last updated. */
63694
- lastFetchedAt: number(),
63695
- /** Live display unit from the upstream source (e.g. HA
63696
- * `attributes.unit_of_measurement`). The UI prefers this over the
63697
- * role's canonical unit. Absent → fall back to the canonical unit. */
63698
- unit: string().optional(),
63699
- /** Suggested decimal places for numeric display.
63700
- * Populated live from the upstream source when provided (e.g. HA
63701
- * `attributes.suggested_display_precision`). Falls back to
63702
- * auto-formatting when absent. */
63703
- precision: number().int().min(0).max(10).optional()
64623
+ lastFetchedAt: number()
63704
64624
  });
63705
64625
  var pressureSensorCapability = {
63706
64626
  name: "pressure-sensor",
@@ -64114,8 +65034,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64114
65034
  }), _void(), { kind: "mutation" });
64115
65035
  var PtzAutotrackSettingsSchema = object({
64116
65036
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64117
- stopDelaySeconds: number().int().min(0).max(300),
64118
- disappearDelaySeconds: number().int().min(0).max(300)
65037
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65038
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64119
65039
  });
64120
65040
  /**
64121
65041
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64269,7 +65189,7 @@ var RunningStateStatusSchema = object({
64269
65189
  /** null = unknown — never coerced to false (D393). */
64270
65190
  running: boolean().nullable(),
64271
65191
  /** Epoch ms the current boolean began; null while `running` is null. */
64272
- since: number().nullable(),
65192
+ since: number().nullable().meta(readingMeta("none")),
64273
65193
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64274
65194
  phase: string().nullable(),
64275
65195
  /** null until a reading has been published (D393). */
@@ -64284,7 +65204,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64284
65204
  /** Generic Customize target for the running boolean (D936). */
64285
65205
  var runningCustomize = {
64286
65206
  version: 1,
64287
- advanced: "derive",
64288
65207
  targets: [{
64289
65208
  id: "running",
64290
65209
  cardinality: "scalar",
@@ -64293,6 +65212,8 @@ var runningCustomize = {
64293
65212
  stateField: "running"
64294
65213
  },
64295
65214
  deviceTypes: [
65215
+ DeviceType.Washer,
65216
+ DeviceType.Dryer,
64296
65217
  DeviceType.Dishwasher,
64297
65218
  DeviceType.Vacuum,
64298
65219
  DeviceType.LawnMower,
@@ -64304,7 +65225,7 @@ var runningCustomize = {
64304
65225
  DeviceType.Thermostat,
64305
65226
  DeviceType.PetFeeder
64306
65227
  ],
64307
- apply: "add-or-replace",
65228
+ apply: "add",
64308
65229
  path: "running",
64309
65230
  sources: [
64310
65231
  {
@@ -65568,9 +66489,9 @@ var RecordTriggerSchema = _enum([
65568
66489
  */
65569
66490
  var RecordWindowSchema = object({
65570
66491
  trigger: RecordTriggerSchema,
65571
- day: number().int().min(0).max(6),
65572
- startMinute: number().int().min(0).max(1439),
65573
- endMinute: number().int().min(1).max(1440)
66492
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66493
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66494
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65574
66495
  });
65575
66496
  /** Status of one physical volume, as the camera itself describes it. */
65576
66497
  var OnboardStorageVolumeSchema = object({
@@ -65646,10 +66567,10 @@ var RecordingOnboardStatusSchema = object({
65646
66567
  /** Master "record to the card at all" switch. */
65647
66568
  enabled: boolean().nullable(),
65648
66569
  overwriteWhenFull: boolean().nullable(),
65649
- preRecordSec: number().nullable(),
65650
- postRecordSec: number().nullable(),
66570
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66571
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65651
66572
  /** Length of one recorded file, in minutes. */
65652
- segmentMinutes: number().nullable(),
66573
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65653
66574
  /** The primary track's weekly windows, flattened. */
65654
66575
  windows: array(RecordWindowSchema),
65655
66576
  /**
@@ -65665,7 +66586,7 @@ var RecordingOnboardStatusSchema = object({
65665
66586
  * provider reporting a non-zero count also reports the schedule as not
65666
66587
  * writable.
65667
66588
  */
65668
- unreadableWindows: number(),
66589
+ unreadableWindows: number().meta(readingMeta("count")),
65669
66590
  /**
65670
66591
  * The camera is scheduled to record and has NO usable storage.
65671
66592
  *
@@ -66510,7 +67431,7 @@ var CamStreamDescriptorSchema = object({
66510
67431
  url: string().optional(),
66511
67432
  codec: string().optional(),
66512
67433
  resolution: CamStreamResolutionSchema.optional(),
66513
- fps: number().positive().optional(),
67434
+ fps: number().positive().optional().meta(readingMeta("rate")),
66514
67435
  label: string().optional(),
66515
67436
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66516
67437
  deviceFeatures: array(string()).optional(),
@@ -66597,18 +67518,18 @@ var StreamProfileSchema = _enum([
66597
67518
  ]);
66598
67519
  /** The current encoding config of one stream profile. */
66599
67520
  var StreamProfileConfigSchema = object({
66600
- width: number(),
66601
- height: number(),
67521
+ width: number().meta(readingMeta("none")),
67522
+ height: number().meta(readingMeta("none")),
66602
67523
  codec: _enum(["h264", "h265"]),
66603
- framerate: number(),
66604
- bitrate: number(),
67524
+ framerate: number().meta(readingMeta("rate")),
67525
+ bitrate: number().meta(readingMeta("bitrate")),
66605
67526
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66606
67527
  encoderProfile: _enum([
66607
67528
  "high",
66608
67529
  "main",
66609
67530
  "baseline"
66610
67531
  ]).optional(),
66611
- gop: number().optional(),
67532
+ gop: number().optional().meta(readingMeta("none")),
66612
67533
  /**
66613
67534
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66614
67535
  *
@@ -66782,7 +67703,8 @@ var switchCapability = {
66782
67703
  deviceTypes: [
66783
67704
  DeviceType.Switch,
66784
67705
  DeviceType.Siren,
66785
- DeviceType.Light
67706
+ DeviceType.Light,
67707
+ DeviceType.Fan
66786
67708
  ],
66787
67709
  methods: { setState: method(object({
66788
67710
  deviceId: number(),
@@ -66829,12 +67751,7 @@ var switchCapability = {
66829
67751
  label: "On",
66830
67752
  kind: "boolean"
66831
67753
  }
66832
- }] },
66833
- customize: {
66834
- version: 1,
66835
- advanced: "derive",
66836
- targets: []
66837
- }
67754
+ }] }
66838
67755
  };
66839
67756
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66840
67757
  function hasControlCharacter(value) {
@@ -67417,35 +68334,21 @@ var tamperCapability = {
67417
68334
  * entries with `device_class: temperature` and any future native
67418
68335
  * thermometer.
67419
68336
  *
67420
- * Unit normalisation: providers convert to Celsius before storing.
67421
- * The slice value is always Celsius so cross-cap aggregators
67422
- * (climate-control's `currentTemp`, energy analytics) can compose
67423
- * without per-source unit fixups. The canonical display unit (`°C`) is
67424
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67425
- * `sourceInfo`.
68337
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68338
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67426
68339
  *
67427
68340
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67428
68341
  * frozen feed (provider hung) distinct from a "temperature hasn't
67429
68342
  * changed" steady state.
67430
68343
  */
67431
68344
  var TemperatureSensorStatusSchema = object({
67432
- /** Current temperature in Celsius. */
67433
- celsius: number().meta({
67434
- unit: "°C",
68345
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68346
+ celsius: number().nullable().meta(readingMeta("temperature", {
68347
+ precision: 1,
67435
68348
  label: "Temperature"
67436
- }),
68349
+ })),
67437
68350
  /** Ms epoch when the slice was last updated (push or poll). */
67438
- lastFetchedAt: number(),
67439
- /** Live display unit from the upstream source (e.g. HA
67440
- * `attributes.unit_of_measurement`). The UI prefers this over the
67441
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67442
- * Absent → the UI falls back to the role's canonical unit. */
67443
- unit: string().optional(),
67444
- /** Suggested decimal places for numeric display.
67445
- * Populated live from the upstream source when provided (e.g. HA
67446
- * `attributes.suggested_display_precision`). Falls back to
67447
- * auto-formatting when absent. */
67448
- precision: number().int().min(0).max(10).optional()
68351
+ lastFetchedAt: number()
67449
68352
  });
67450
68353
  var temperatureSensorCapability = {
67451
68354
  name: "temperature-sensor",
@@ -67476,12 +68379,7 @@ var temperatureSensorCapability = {
67476
68379
  } } },
67477
68380
  /** Clock fields: written, but excluded from the compare that decides
67478
68381
  * whether persisting is worth a SQLite commit. */
67479
- volatileStateFields: ["lastFetchedAt"],
67480
- customize: {
67481
- version: 1,
67482
- advanced: "derive",
67483
- targets: []
67484
- }
68382
+ volatileStateFields: ["lastFetchedAt"]
67485
68383
  };
67486
68384
  /**
67487
68385
  * toast — system-scoped singleton capability that streams toast
@@ -67764,16 +68662,16 @@ var VacuumControlStatusSchema = object({
67764
68662
  ]),
67765
68663
  /** 0..100 battery percentage. Null when the device has no battery
67766
68664
  * reading. */
67767
- batteryLevel: number().min(0).max(100).nullable(),
68665
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67768
68666
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67769
68667
  * the vacuum has no speed control. */
67770
68668
  fanSpeed: string().nullable(),
67771
68669
  /** Speed tokens the hardware accepts — drives the UI selector. */
67772
68670
  availableFanSpeeds: array(string()),
67773
68671
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67774
- progressPercent: number().min(0).max(100).nullable(),
68672
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67775
68673
  /** Current error code (0 / null = no error). */
67776
- errorCode: number().nullable(),
68674
+ errorCode: number().nullable().meta(readingMeta("none")),
67777
68675
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67778
68676
  errorLabel: string().nullable(),
67779
68677
  /** Ms epoch when the slice was last updated. */
@@ -67851,7 +68749,7 @@ var ValveStatusSchema = object({
67851
68749
  ]),
67852
68750
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67853
68751
  * intermediate position surface. */
67854
- position: number().min(0).max(100).nullable(),
68752
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67855
68753
  /** Ms epoch when the slice was last updated. */
67856
68754
  lastChangedAt: number()
67857
68755
  });
@@ -67959,9 +68857,9 @@ var vibrationCapability = {
67959
68857
  */
67960
68858
  var WaterHeaterStatusSchema = object({
67961
68859
  /** Current measured temperature. Null when not reported. */
67962
- currentTemp: number().nullable(),
68860
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
67963
68861
  /** Target temperature setpoint. Null when no setpoint surface. */
67964
- targetTemp: number().nullable(),
68862
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
67965
68863
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
67966
68864
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
67967
68865
  * device reports an unknown state. */
@@ -67972,9 +68870,9 @@ var WaterHeaterStatusSchema = object({
67972
68870
  * has no away surface. */
67973
68871
  away: boolean().nullable(),
67974
68872
  /** HA `min_temp` attribute. Null when not reported. */
67975
- minTemp: number().nullable(),
68873
+ minTemp: number().nullable().meta(readingMeta("temperature")),
67976
68874
  /** HA `max_temp` attribute. Null when not reported. */
67977
- maxTemp: number().nullable(),
68875
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
67978
68876
  /** Ms epoch when the slice was last updated. */
67979
68877
  lastChangedAt: number()
67980
68878
  });
@@ -68059,25 +68957,18 @@ var waterHeaterCapability = {
68059
68957
  * Forecast deferred — current conditions only for v1.
68060
68958
  */
68061
68959
  var WeatherStatusSchema = object({
68062
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68063
- * when no condition has been reported yet. */
68960
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68064
68961
  condition: string().nullable(),
68065
- /** Current temperature in the reported unit. Null when not provided. */
68066
- temperature: number().nullable(),
68067
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68068
- temperatureUnit: string().nullable(),
68962
+ /** Current temperature in Celsius. Null when not provided or refused. */
68963
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68069
68964
  /** Relative humidity (0..100). Null when not provided. */
68070
- humidity: number().min(0).max(100).nullable(),
68071
- /** Barometric pressure in the reported unit. Null when not provided. */
68072
- pressure: number().nullable(),
68073
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68074
- pressureUnit: string().nullable(),
68075
- /** Wind speed in the reported unit. Null when not provided. */
68076
- windSpeed: number().nullable(),
68077
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68078
- windSpeedUnit: string().nullable(),
68965
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68966
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68967
+ pressure: number().nullable().meta(readingMeta("pressure")),
68968
+ /** Wind speed in metres per second. Null when not provided or refused. */
68969
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68079
68970
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68080
- windBearing: number().nullable(),
68971
+ windBearing: number().nullable().meta(readingMeta("angle")),
68081
68972
  /** Ms epoch when the slice was last updated. */
68082
68973
  lastFetchedAt: number()
68083
68974
  });
@@ -68105,12 +68996,12 @@ var weatherCapability = {
68105
68996
  */
68106
68997
  durability: "session",
68107
68998
  /**
68108
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68999
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68109
69000
  */
68110
69001
  history: { fields: {
68111
69002
  temperature: {
68112
69003
  kind: "gauge",
68113
- unitFrom: "temperatureUnit",
69004
+ unit: "°C",
68114
69005
  deadband: .2
68115
69006
  },
68116
69007
  humidity: {
@@ -68120,12 +69011,12 @@ var weatherCapability = {
68120
69011
  },
68121
69012
  pressure: {
68122
69013
  kind: "gauge",
68123
- unitFrom: "pressureUnit",
69014
+ unit: "hPa",
68124
69015
  deadband: .5
68125
69016
  },
68126
69017
  windSpeed: {
68127
69018
  kind: "gauge",
68128
- unitFrom: "windSpeedUnit",
69019
+ unit: "m/s",
68129
69020
  deadband: .5
68130
69021
  }
68131
69022
  } }
@@ -69026,8 +69917,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
69026
69917
  kind: "mutation",
69027
69918
  auth: "admin"
69028
69919
  });
69920
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
69029
69921
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69030
- 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;
69922
+ 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;
69031
69923
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
69032
69924
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
69033
69925
  new Set(CAP_NAMES);
@@ -69356,6 +70248,9 @@ var DEVICE_LOCAL_STATE_CAPS = {
69356
70248
  featureProbe: featureProbeCapability,
69357
70249
  flood: floodCapability,
69358
70250
  gas: gasCapability,
70251
+ groupActive: groupActiveCapability,
70252
+ groupCommands: groupCommandsCapability,
70253
+ groupStatus: groupStatusCapability,
69359
70254
  heatingValve: heatingValveCapability,
69360
70255
  humidifier: humidifierCapability,
69361
70256
  humiditySensor: humiditySensorCapability,
@@ -69365,6 +70260,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
69365
70260
  lawnMowerControl: lawnMowerControlCapability,
69366
70261
  lockControl: lockControlCapability,
69367
70262
  mediaPlayer: mediaPlayerCapability,
70263
+ meter: meterCapability,
69368
70264
  motion: motionCapability,
69369
70265
  motionTrigger: motionTriggerCapability,
69370
70266
  motionZones: motionZonesCapability,
@@ -70624,7 +71520,7 @@ var BaseDeviceProvider = class extends BaseAddon {
70624
71520
  return toDeviceSummary(device, this.addonId);
70625
71521
  }
70626
71522
  };
70627
- 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;
71523
+ 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;
70628
71524
  new Set(Object.values(DeviceType));
70629
71525
  Object.freeze({
70630
71526
  "accessories.getStatus": {
@@ -70732,6 +71628,13 @@ Object.freeze({
70732
71628
  access: "view",
70733
71629
  role: "admin"
70734
71630
  },
71631
+ "addons.getPackageDocs": {
71632
+ capName: "addons",
71633
+ capScope: "system",
71634
+ addonId: null,
71635
+ access: "view",
71636
+ role: "admin"
71637
+ },
70735
71638
  "addons.getVersions": {
70736
71639
  capName: "addons",
70737
71640
  capScope: "system",
@@ -73840,6 +74743,27 @@ Object.freeze({
73840
74743
  access: "view",
73841
74744
  role: "viewer"
73842
74745
  },
74746
+ "groupActive.getStatus": {
74747
+ capName: "group-active",
74748
+ capScope: "device",
74749
+ addonId: null,
74750
+ access: "view",
74751
+ role: "viewer"
74752
+ },
74753
+ "groupCommands.getStatus": {
74754
+ capName: "group-commands",
74755
+ capScope: "device",
74756
+ addonId: null,
74757
+ access: "view",
74758
+ role: "viewer"
74759
+ },
74760
+ "groupStatus.getStatus": {
74761
+ capName: "group-status",
74762
+ capScope: "device",
74763
+ addonId: null,
74764
+ access: "view",
74765
+ role: "viewer"
74766
+ },
73843
74767
  "heatingValve.getStatus": {
73844
74768
  capName: "heating-valve",
73845
74769
  capScope: "device",
@@ -74792,6 +75716,13 @@ Object.freeze({
74792
75716
  access: "create",
74793
75717
  role: "admin"
74794
75718
  },
75719
+ "meter.getStatus": {
75720
+ capName: "meter",
75721
+ capScope: "device",
75722
+ addonId: null,
75723
+ access: "view",
75724
+ role: "viewer"
75725
+ },
74795
75726
  "metricsProvider.collectSnapshot": {
74796
75727
  capName: "metrics-provider",
74797
75728
  capScope: "system",
@@ -82890,7 +83821,39 @@ var APPLIANCE_CAP_VOCABULARY = {
82890
83821
  phrase: t("When {device} finishes", "Quando {device} finisce"),
82891
83822
  icon: "square"
82892
83823
  }
82893
- }
83824
+ },
83825
+ overrides: { byType: {
83826
+ [DeviceType.Washer]: {
83827
+ state: { running: {
83828
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
83829
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
83830
+ } },
83831
+ events: {
83832
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
83833
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
83834
+ }
83835
+ },
83836
+ [DeviceType.Dryer]: {
83837
+ state: { running: {
83838
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
83839
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
83840
+ } },
83841
+ events: {
83842
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
83843
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
83844
+ }
83845
+ },
83846
+ [DeviceType.Dishwasher]: {
83847
+ state: { running: {
83848
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
83849
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
83850
+ } },
83851
+ events: {
83852
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
83853
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
83854
+ }
83855
+ }
83856
+ } }
82894
83857
  },
82895
83858
  update: {
82896
83859
  scope: "device",
@@ -83662,6 +84625,41 @@ var SENSOR_CAP_VOCABULARY = {
83662
84625
  icon: "flame"
83663
84626
  }]
83664
84627
  },
84628
+ "group-active": {
84629
+ scope: "device",
84630
+ noun: t("Group state", "Stato del gruppo")
84631
+ },
84632
+ "group-commands": {
84633
+ scope: "device",
84634
+ noun: t("Group command", "Comando di gruppo")
84635
+ },
84636
+ "group-status": {
84637
+ scope: "device",
84638
+ noun: t("Group", "Gruppo"),
84639
+ state: [
84640
+ {
84641
+ kind: "numeric",
84642
+ field: "included",
84643
+ quantity: t("the members reporting", "i membri che rispondono"),
84644
+ label: t("Reporting", "Rispondono"),
84645
+ icon: "users"
84646
+ },
84647
+ {
84648
+ kind: "numeric",
84649
+ field: "excluded",
84650
+ quantity: t("the members left out", "i membri esclusi"),
84651
+ label: t("Excluded", "Esclusi"),
84652
+ icon: "users"
84653
+ },
84654
+ {
84655
+ kind: "numeric",
84656
+ field: "total",
84657
+ quantity: t("the member count", "il numero di membri"),
84658
+ label: t("Members", "Membri"),
84659
+ icon: "sigma"
84660
+ }
84661
+ ]
84662
+ },
83665
84663
  "humidity-sensor": {
83666
84664
  scope: "device",
83667
84665
  noun: t("Humidity", "Umidità"),
@@ -83687,6 +84685,17 @@ var SENSOR_CAP_VOCABULARY = {
83687
84685
  }],
83688
84686
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
83689
84687
  },
84688
+ meter: {
84689
+ scope: "device",
84690
+ noun: t("Meter", "Contatore"),
84691
+ state: [{
84692
+ kind: "numeric",
84693
+ field: "value",
84694
+ quantity: t("the total", "il totale"),
84695
+ label: t("Total", "Totale"),
84696
+ icon: "gauge"
84697
+ }]
84698
+ },
83690
84699
  "numeric-sensor": {
83691
84700
  scope: "device",
83692
84701
  noun: t("Sensor", "Sensore"),
@@ -83695,7 +84704,6 @@ var SENSOR_CAP_VOCABULARY = {
83695
84704
  field: "value",
83696
84705
  quantity: t("the reading", "il valore"),
83697
84706
  label: t("Reading", "Valore"),
83698
- unitField: "unit",
83699
84707
  icon: "gauge"
83700
84708
  }]
83701
84709
  },
@@ -83831,7 +84839,7 @@ var SENSOR_CAP_VOCABULARY = {
83831
84839
  field: "temperature",
83832
84840
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83833
84841
  label: t("Temperature", "Temperatura"),
83834
- unitField: "temperatureUnit",
84842
+ unit: "°C",
83835
84843
  icon: "thermometer"
83836
84844
  },
83837
84845
  {
@@ -83847,7 +84855,7 @@ var SENSOR_CAP_VOCABULARY = {
83847
84855
  field: "windSpeed",
83848
84856
  quantity: t("the wind speed", "la velocità del vento"),
83849
84857
  label: t("Wind", "Vento"),
83850
- unitField: "windSpeedUnit",
84858
+ unit: "m/s",
83851
84859
  icon: "wind"
83852
84860
  },
83853
84861
  {
@@ -83855,7 +84863,7 @@ var SENSOR_CAP_VOCABULARY = {
83855
84863
  field: "pressure",
83856
84864
  quantity: t("the air pressure", "la pressione atmosferica"),
83857
84865
  label: t("Pressure", "Pressione"),
83858
- unitField: "pressureUnit",
84866
+ unit: "hPa",
83859
84867
  icon: "gauge"
83860
84868
  }
83861
84869
  ]
@@ -84160,8 +85168,47 @@ function projectAction(cap, action) {
84160
85168
  };
84161
85169
  }
84162
85170
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
84163
- 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;
85171
+ 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;
84164
85172
  DeviceFeature.BatteryOperated;
85173
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
85174
+ 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.";
85175
+ 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.";
85176
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
85177
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
85178
+ 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.";
85179
+ function withheld(reason) {
85180
+ return {
85181
+ [DeviceType.Washer]: {
85182
+ exported: false,
85183
+ reason
85184
+ },
85185
+ [DeviceType.Dryer]: {
85186
+ exported: false,
85187
+ reason
85188
+ },
85189
+ [DeviceType.Dishwasher]: {
85190
+ exported: false,
85191
+ reason
85192
+ }
85193
+ };
85194
+ }
85195
+ function offered(received) {
85196
+ return {
85197
+ [DeviceType.Washer]: {
85198
+ exported: true,
85199
+ as: received
85200
+ },
85201
+ [DeviceType.Dryer]: {
85202
+ exported: true,
85203
+ as: received
85204
+ },
85205
+ [DeviceType.Dishwasher]: {
85206
+ exported: true,
85207
+ as: received
85208
+ }
85209
+ };
85210
+ }
85211
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
84165
85212
  Object.freeze({
84166
85213
  "accessories.getStatus": [{
84167
85214
  name: "deviceId",
@@ -85128,6 +86175,21 @@ Object.freeze({
85128
86175
  form: "single",
85129
86176
  optional: false
85130
86177
  }],
86178
+ "groupActive.getStatus": [{
86179
+ name: "deviceId",
86180
+ form: "single",
86181
+ optional: false
86182
+ }],
86183
+ "groupCommands.getStatus": [{
86184
+ name: "deviceId",
86185
+ form: "single",
86186
+ optional: false
86187
+ }],
86188
+ "groupStatus.getStatus": [{
86189
+ name: "deviceId",
86190
+ form: "single",
86191
+ optional: false
86192
+ }],
85131
86193
  "heatingValve.getStatus": [{
85132
86194
  name: "deviceId",
85133
86195
  form: "single",
@@ -85328,6 +86390,11 @@ Object.freeze({
85328
86390
  form: "single",
85329
86391
  optional: false
85330
86392
  }],
86393
+ "meter.getStatus": [{
86394
+ name: "deviceId",
86395
+ form: "single",
86396
+ optional: false
86397
+ }],
85331
86398
  "motion.getStatus": [{
85332
86399
  name: "deviceId",
85333
86400
  form: "single",
@@ -86935,7 +88002,6 @@ Object.freeze({
86935
88002
  "network-access": "ingress",
86936
88003
  "smtp-provider": "email"
86937
88004
  });
86938
- DeviceType.Camera;
86939
88005
  /**
86940
88006
  * A saved Customize assignment (D936). The schema is the type: callers use
86941
88007
  * `z.infer`, they do not restate the shape.
@@ -87112,6 +88178,8 @@ new Map(Object.entries({
87112
88178
  "detection-blind": "device-events",
87113
88179
  "motion-noisy": "device-events",
87114
88180
  "motion-calm": "device-events",
88181
+ "package-llm-silent": "device-events",
88182
+ "package-llm-answered": "device-events",
87115
88183
  "recording-stalled": "device-events",
87116
88184
  "recording-healthy": "device-events",
87117
88185
  "recording-degraded": "device-events",
@@ -87195,6 +88263,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87195
88263
  label: "Motion without a subject",
87196
88264
  kinds: ["motion-noisy", "motion-calm"]
87197
88265
  },
88266
+ {
88267
+ id: "package-llm",
88268
+ family: "device-events",
88269
+ label: "Package confirm",
88270
+ kinds: ["package-llm-silent", "package-llm-answered"]
88271
+ },
87198
88272
  {
87199
88273
  id: "recording",
87200
88274
  family: "device-events",
@@ -87376,6 +88450,8 @@ var DEVICE_EVENTS = [
87376
88450
  "detection-blind",
87377
88451
  "motion-noisy",
87378
88452
  "motion-calm",
88453
+ "package-llm-silent",
88454
+ "package-llm-answered",
87379
88455
  "recording-stalled",
87380
88456
  "recording-healthy",
87381
88457
  "recording-degraded",
@@ -87644,6 +88720,15 @@ new Map([
87644
88720
  snapshotLabel: "Cameras with constant motion now",
87645
88721
  snapshotSection: null
87646
88722
  },
88723
+ {
88724
+ id: "package-llm-confirm",
88725
+ family: "device-events",
88726
+ label: "Package confirm (not answering / answering again)",
88727
+ bad: ["package-llm-silent"],
88728
+ recovery: ["package-llm-answered"],
88729
+ snapshotLabel: "Cameras whose package confirm is not answering",
88730
+ snapshotSection: null
88731
+ },
87647
88732
  {
87648
88733
  id: "recording-health",
87649
88734
  family: "device-events",