@camstack/addon-provider-amcrest 0.2.160 → 0.2.162

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -35,7 +35,7 @@ let node_stream_promises = require("node:stream/promises");
35
35
  let node_child_process = require("node:child_process");
36
36
  require("node:zlib");
37
37
  let node_os = require("node:os");
38
- //#region ../types/dist/event-category-DHXxB8t9.mjs
38
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
39
39
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
40
40
  EventCategory["SystemBoot"] = "system.boot";
41
41
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -361,6 +361,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
361
361
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
362
362
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
363
363
  /**
364
+ * A camera's package LLM confirm has failed closed three times in a row
365
+ * (D931, 2026-10-26). One edge per camera until
366
+ * {@link PipelinePackageLlmAnswered}.
367
+ */
368
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
369
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
370
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
371
+ /**
364
372
  * Per-camera pipeline config was mutated by the orchestrator
365
373
  * (a settings change via `setAgentInferenceDevices` /
366
374
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5470,7 +5478,7 @@ var ZodIssueCode = {
5470
5478
  var ZodFirstPartyTypeKind;
5471
5479
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5472
5480
  //#endregion
5473
- //#region ../types/dist/sleep-BErAxhul.mjs
5481
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5474
5482
  /**
5475
5483
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5476
5484
  * window to float samples (D455).
@@ -8024,6 +8032,99 @@ var RecordKindDeclarationSchema = object({
8024
8032
  }).strict();
8025
8033
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8026
8034
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8035
+ var READING_QUANTITIES = [
8036
+ "temperature",
8037
+ "pressure",
8038
+ "speed",
8039
+ "precipitation",
8040
+ "precipitation-rate",
8041
+ "length",
8042
+ "area",
8043
+ "illuminance",
8044
+ "irradiance",
8045
+ "power",
8046
+ "energy",
8047
+ "voltage",
8048
+ "current",
8049
+ "apparent-power",
8050
+ "humidity",
8051
+ "percentage",
8052
+ "concentration-volume",
8053
+ "concentration-mass",
8054
+ "concentration-ppb",
8055
+ "volume",
8056
+ "duration",
8057
+ "duration-ms",
8058
+ "information",
8059
+ "sound-level",
8060
+ "signal-level",
8061
+ "colour-temperature",
8062
+ "angle",
8063
+ "bitrate",
8064
+ "rate",
8065
+ "mass",
8066
+ "index",
8067
+ "ratio",
8068
+ "count",
8069
+ "none",
8070
+ "device"
8071
+ ];
8072
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8073
+ new Set(READING_QUANTITIES);
8074
+ /**
8075
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8076
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8077
+ */
8078
+ var QUANTITY_BASE = {
8079
+ temperature: "°C",
8080
+ pressure: "hPa",
8081
+ speed: "m/s",
8082
+ precipitation: "mm",
8083
+ "precipitation-rate": "mm/h",
8084
+ length: "m",
8085
+ area: "m²",
8086
+ illuminance: "lx",
8087
+ irradiance: "W/m²",
8088
+ power: "W",
8089
+ energy: "kWh",
8090
+ voltage: "V",
8091
+ current: "A",
8092
+ "apparent-power": "VA",
8093
+ humidity: "%",
8094
+ percentage: "%",
8095
+ "concentration-volume": "ppm",
8096
+ "concentration-mass": "µg/m³",
8097
+ "concentration-ppb": "ppb",
8098
+ volume: "L",
8099
+ duration: "s",
8100
+ "duration-ms": "ms",
8101
+ information: "B",
8102
+ "sound-level": "dBFS",
8103
+ "signal-level": "dBm",
8104
+ "colour-temperature": "mired",
8105
+ angle: "°",
8106
+ bitrate: "kbps",
8107
+ rate: "fps",
8108
+ mass: "kg",
8109
+ index: "none",
8110
+ ratio: "none",
8111
+ count: "none",
8112
+ none: "none",
8113
+ device: "device"
8114
+ };
8115
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8116
+ function baseUnitOf(quantity) {
8117
+ const symbol = QUANTITY_BASE[quantity];
8118
+ if (symbol === "none" || symbol === "device") return void 0;
8119
+ return symbol;
8120
+ }
8121
+ function readingMeta(quantity, extras) {
8122
+ return {
8123
+ quantity,
8124
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8125
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8126
+ };
8127
+ }
8027
8128
  /** Shared Zod schemas used across streaming capabilities. */
8028
8129
  var CamProfileSchema = _enum([
8029
8130
  "high",
@@ -8051,8 +8152,8 @@ var CamStreamKindSchema = _enum([
8051
8152
  "derived"
8052
8153
  ]);
8053
8154
  var CamStreamResolutionSchema = object({
8054
- width: number().int().positive(),
8055
- height: number().int().positive()
8155
+ width: number().int().positive().meta(readingMeta("none")),
8156
+ height: number().int().positive().meta(readingMeta("none"))
8056
8157
  });
8057
8158
  var CameraStreamSchema = object({
8058
8159
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8570,6 +8671,9 @@ var CAP_NAMES = Object.freeze([
8570
8671
  "filesystem-browse",
8571
8672
  "flood",
8572
8673
  "gas",
8674
+ "group-active",
8675
+ "group-commands",
8676
+ "group-status",
8573
8677
  "heating-valve",
8574
8678
  "humidifier",
8575
8679
  "humidity-sensor",
@@ -8590,6 +8694,7 @@ var CAP_NAMES = Object.freeze([
8590
8694
  "login-method",
8591
8695
  "media-player",
8592
8696
  "mesh-network",
8697
+ "meter",
8593
8698
  "metrics-provider",
8594
8699
  "model-convert",
8595
8700
  "model-distributor",
@@ -8699,14 +8804,6 @@ Object.freeze({
8699
8804
  {
8700
8805
  "path": "aqi",
8701
8806
  "valueType": "number"
8702
- },
8703
- {
8704
- "path": "unit",
8705
- "valueType": "string"
8706
- },
8707
- {
8708
- "path": "precision",
8709
- "valueType": "number"
8710
8807
  }
8711
8808
  ],
8712
8809
  "alarm-panel": [
@@ -8742,20 +8839,10 @@ Object.freeze({
8742
8839
  "valueType": "boolean"
8743
8840
  }
8744
8841
  ],
8745
- "ambient-light-sensor": [
8746
- {
8747
- "path": "lux",
8748
- "valueType": "number"
8749
- },
8750
- {
8751
- "path": "unit",
8752
- "valueType": "string"
8753
- },
8754
- {
8755
- "path": "precision",
8756
- "valueType": "number"
8757
- }
8758
- ],
8842
+ "ambient-light-sensor": [{
8843
+ "path": "lux",
8844
+ "valueType": "number"
8845
+ }],
8759
8846
  "audio-metrics": [
8760
8847
  {
8761
8848
  "path": "ts",
@@ -8835,6 +8922,10 @@ Object.freeze({
8835
8922
  "path": "lastUpdated",
8836
8923
  "valueType": "number"
8837
8924
  },
8925
+ {
8926
+ "path": "volts",
8927
+ "valueType": "number"
8928
+ },
8838
8929
  {
8839
8930
  "path": "lastContactAt",
8840
8931
  "valueType": "number"
@@ -9077,10 +9168,6 @@ Object.freeze({
9077
9168
  "datetime"
9078
9169
  ]
9079
9170
  },
9080
- {
9081
- "path": "unit",
9082
- "valueType": "string"
9083
- },
9084
9171
  {
9085
9172
  "path": "min",
9086
9173
  "valueType": "number"
@@ -9092,19 +9179,6 @@ Object.freeze({
9092
9179
  {
9093
9180
  "path": "step",
9094
9181
  "valueType": "number"
9095
- },
9096
- {
9097
- "path": "precision",
9098
- "valueType": "number"
9099
- },
9100
- {
9101
- "path": "format",
9102
- "valueType": "enum",
9103
- "enumValues": [
9104
- "date",
9105
- "time",
9106
- "datetime"
9107
- ]
9108
9182
  }
9109
9183
  ],
9110
9184
  "cover": [
@@ -9345,13 +9419,8 @@ Object.freeze({
9345
9419
  "path": "value",
9346
9420
  "valueType": "string"
9347
9421
  }, {
9348
- "path": "format",
9349
- "valueType": "enum",
9350
- "enumValues": [
9351
- "date",
9352
- "time",
9353
- "datetime"
9354
- ]
9422
+ "path": "timestamp",
9423
+ "valueType": "boolean"
9355
9424
  }],
9356
9425
  "event-emitter": [
9357
9426
  {
@@ -9424,6 +9493,24 @@ Object.freeze({
9424
9493
  "path": "detected",
9425
9494
  "valueType": "boolean"
9426
9495
  }],
9496
+ "group-active": [{
9497
+ "path": "active",
9498
+ "valueType": "boolean"
9499
+ }],
9500
+ "group-status": [
9501
+ {
9502
+ "path": "included",
9503
+ "valueType": "number"
9504
+ },
9505
+ {
9506
+ "path": "excluded",
9507
+ "valueType": "number"
9508
+ },
9509
+ {
9510
+ "path": "total",
9511
+ "valueType": "number"
9512
+ }
9513
+ ],
9427
9514
  "heating-valve": [{
9428
9515
  "path": "position",
9429
9516
  "valueType": "number"
@@ -9458,20 +9545,10 @@ Object.freeze({
9458
9545
  "valueType": "number"
9459
9546
  }
9460
9547
  ],
9461
- "humidity-sensor": [
9462
- {
9463
- "path": "percent",
9464
- "valueType": "number"
9465
- },
9466
- {
9467
- "path": "unit",
9468
- "valueType": "string"
9469
- },
9470
- {
9471
- "path": "precision",
9472
- "valueType": "number"
9473
- }
9474
- ],
9548
+ "humidity-sensor": [{
9549
+ "path": "percent",
9550
+ "valueType": "number"
9551
+ }],
9475
9552
  "image": [{
9476
9553
  "path": "url",
9477
9554
  "valueType": "string"
@@ -9679,6 +9756,10 @@ Object.freeze({
9679
9756
  ]
9680
9757
  }
9681
9758
  ],
9759
+ "meter": [{
9760
+ "path": "value",
9761
+ "valueType": "number"
9762
+ }],
9682
9763
  "motion": [
9683
9764
  {
9684
9765
  "path": "detected",
@@ -9810,20 +9891,10 @@ Object.freeze({
9810
9891
  "valueType": "boolean"
9811
9892
  }
9812
9893
  ],
9813
- "numeric-sensor": [
9814
- {
9815
- "path": "value",
9816
- "valueType": "number"
9817
- },
9818
- {
9819
- "path": "unit",
9820
- "valueType": "string"
9821
- },
9822
- {
9823
- "path": "precision",
9824
- "valueType": "number"
9825
- }
9826
- ],
9894
+ "numeric-sensor": [{
9895
+ "path": "value",
9896
+ "valueType": "number"
9897
+ }],
9827
9898
  "pet-feeder": [
9828
9899
  {
9829
9900
  "path": "foodLevel",
@@ -9915,14 +9986,6 @@ Object.freeze({
9915
9986
  {
9916
9987
  "path": "powerFactor",
9917
9988
  "valueType": "number"
9918
- },
9919
- {
9920
- "path": "unit",
9921
- "valueType": "string"
9922
- },
9923
- {
9924
- "path": "precision",
9925
- "valueType": "number"
9926
9989
  }
9927
9990
  ],
9928
9991
  "presence": [
@@ -9951,20 +10014,10 @@ Object.freeze({
9951
10014
  "valueType": "number"
9952
10015
  }
9953
10016
  ],
9954
- "pressure-sensor": [
9955
- {
9956
- "path": "hpa",
9957
- "valueType": "number"
9958
- },
9959
- {
9960
- "path": "unit",
9961
- "valueType": "string"
9962
- },
9963
- {
9964
- "path": "precision",
9965
- "valueType": "number"
9966
- }
9967
- ],
10017
+ "pressure-sensor": [{
10018
+ "path": "hpa",
10019
+ "valueType": "number"
10020
+ }],
9968
10021
  "privacy-mask": [{
9969
10022
  "path": "enabled",
9970
10023
  "valueType": "boolean"
@@ -10275,20 +10328,10 @@ Object.freeze({
10275
10328
  "path": "tampered",
10276
10329
  "valueType": "boolean"
10277
10330
  }],
10278
- "temperature-sensor": [
10279
- {
10280
- "path": "celsius",
10281
- "valueType": "number"
10282
- },
10283
- {
10284
- "path": "unit",
10285
- "valueType": "string"
10286
- },
10287
- {
10288
- "path": "precision",
10289
- "valueType": "number"
10290
- }
10291
- ],
10331
+ "temperature-sensor": [{
10332
+ "path": "celsius",
10333
+ "valueType": "number"
10334
+ }],
10292
10335
  "update": [
10293
10336
  {
10294
10337
  "path": "currentVersion",
@@ -10399,10 +10442,6 @@ Object.freeze({
10399
10442
  "path": "temperature",
10400
10443
  "valueType": "number"
10401
10444
  },
10402
- {
10403
- "path": "temperatureUnit",
10404
- "valueType": "string"
10405
- },
10406
10445
  {
10407
10446
  "path": "humidity",
10408
10447
  "valueType": "number"
@@ -10411,18 +10450,10 @@ Object.freeze({
10411
10450
  "path": "pressure",
10412
10451
  "valueType": "number"
10413
10452
  },
10414
- {
10415
- "path": "pressureUnit",
10416
- "valueType": "string"
10417
- },
10418
10453
  {
10419
10454
  "path": "windSpeed",
10420
10455
  "valueType": "number"
10421
10456
  },
10422
- {
10423
- "path": "windSpeedUnit",
10424
- "valueType": "string"
10425
- },
10426
10457
  {
10427
10458
  "path": "windBearing",
10428
10459
  "valueType": "number"
@@ -10486,7 +10517,7 @@ function userError(code, params, englishFallback) {
10486
10517
  return new CamstackError(code, params, englishFallback);
10487
10518
  }
10488
10519
  //#endregion
10489
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10520
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10490
10521
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10491
10522
  DeviceType["Camera"] = "camera";
10492
10523
  DeviceType["Hub"] = "hub";
@@ -10580,8 +10611,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10580
10611
  DeviceType["PetFeeder"] = "pet-feeder";
10581
10612
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10582
10613
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10583
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10614
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10615
+ * A plug-built dishwasher carries running-state and power instead. */
10584
10616
  DeviceType["Dishwasher"] = "dishwasher";
10617
+ /** Washing machine. A plug-built one is running while its power stays
10618
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10619
+ DeviceType["Washer"] = "washer";
10620
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10621
+ DeviceType["Dryer"] = "dryer";
10585
10622
  return DeviceType;
10586
10623
  }({});
10587
10624
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -10829,7 +10866,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
10829
10866
  return DeviceRole;
10830
10867
  }({});
10831
10868
  //#endregion
10832
- //#region ../types/dist/composition-kEl9mevu.mjs
10869
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
10833
10870
  /**
10834
10871
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10835
10872
  *
@@ -11144,6 +11181,13 @@ var table = {
11144
11181
  apply: () => {
11145
11182
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11146
11183
  }
11184
+ },
11185
+ schmitt: {
11186
+ minArgs: 3,
11187
+ maxArgs: 5,
11188
+ apply: () => {
11189
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11190
+ }
11147
11191
  }
11148
11192
  };
11149
11193
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11584,15 +11628,232 @@ function validateExpressionSource(src) {
11584
11628
  }
11585
11629
  return null;
11586
11630
  }
11587
- /** A composed device's stableId is this prefix plus the block id. */
11588
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11589
- /** Longest snippet a `code` source may carry (slice 4). */
11590
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11631
+ /**
11632
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11633
+ *
11634
+ * Standby of the control board is under 5 W; a motor or a heater is far
11635
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11636
+ * a cycle, so running starts only after 30 s above the line.
11637
+ *
11638
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11639
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11640
+ * report "finished" in the middle of the program, so the stop dwell is
11641
+ * 10 minutes on every type. The number is stored per type so one machine
11642
+ * can move later without moving the others.
11643
+ */
11644
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11645
+ var APPLIANCE_KINDS = [
11646
+ "washer",
11647
+ "dryer",
11648
+ "dishwasher"
11649
+ ];
11650
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11651
+ /**
11652
+ * Statistics and integrals over a history read (D944).
11653
+ *
11654
+ * Pure: the same points always give the same answer, so a restart recomputes
11655
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11656
+ * never the mean of the samples. A hole with no held value is not integrated
11657
+ * as zero. A truncated read is not published.
11658
+ */
11659
+ var STATISTIC_STATS = [
11660
+ "min",
11661
+ "max",
11662
+ "mean",
11663
+ "sum",
11664
+ "count",
11665
+ "change"
11666
+ ];
11667
+ /**
11668
+ * The range a statistics or integral helper asks history for (D944).
11669
+ *
11670
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11671
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11672
+ * is strictly longer than the longest that range can be. Forever qualifies.
11673
+ * Equal to a tier's retention is a refusal, not a short answer.
11674
+ */
11675
+ var MINUTE_MS = 6e4;
11676
+ var HOUR_MS = 60 * MINUTE_MS;
11677
+ var DAY_MS = 24 * HOUR_MS;
11678
+ var ROLLING_SPANS = [
11679
+ "1h",
11680
+ "24h",
11681
+ "2d",
11682
+ "7d",
11683
+ "30d",
11684
+ "custom"
11685
+ ];
11686
+ var CALENDAR_PERIODS = [
11687
+ "today",
11688
+ "yesterday",
11689
+ "this-week",
11690
+ "this-month"
11691
+ ];
11692
+ var RESET_PERIODS = [
11693
+ "never",
11694
+ "daily",
11695
+ "weekly",
11696
+ "monthly"
11697
+ ];
11698
+ 5 * MINUTE_MS;
11699
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11700
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11701
+ var rollingWindowObject = object({
11702
+ kind: literal("rolling"),
11703
+ span: _enum(ROLLING_SPANS),
11704
+ customMinutes: number().int().min(5).optional()
11705
+ }).check((payload) => {
11706
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11707
+ code: "custom",
11708
+ message: "a custom range needs a duration in minutes",
11709
+ path: ["customMinutes"],
11710
+ input: payload.value
11711
+ });
11712
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11713
+ code: "custom",
11714
+ message: "only a custom range carries a duration",
11715
+ path: ["customMinutes"],
11716
+ input: payload.value
11717
+ });
11718
+ });
11719
+ var RollingWindowSchema = rollingWindowObject;
11720
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
11721
+ kind: literal("calendar"),
11722
+ period: _enum(CALENDAR_PERIODS)
11723
+ })]);
11724
+ /**
11725
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11726
+ * id. Split out so helper schemas can name it without a cycle through
11727
+ * `composition.ts` (D659).
11728
+ */
11591
11729
  var CompositionSourceRefSchema = object({
11592
11730
  /** The owning addon; `stableId` is unique only within it. */
11593
11731
  addonId: string().min(1),
11594
11732
  stableId: string().min(1)
11595
11733
  });
11734
+ var GROUP_TEMPLATE_IDS = [
11735
+ "temperature",
11736
+ "humidity",
11737
+ "pressure",
11738
+ "illuminance",
11739
+ "power",
11740
+ "energy",
11741
+ "battery",
11742
+ "numeric",
11743
+ "motion",
11744
+ "contact",
11745
+ "leak",
11746
+ "smoke",
11747
+ "occupancy",
11748
+ "binary",
11749
+ "switch",
11750
+ "light",
11751
+ "fan",
11752
+ "cover",
11753
+ "valve",
11754
+ "lock",
11755
+ "media-player",
11756
+ "climate"
11757
+ ];
11758
+ var NUMERIC_AGGREGATES = [
11759
+ "mean",
11760
+ "median",
11761
+ "min",
11762
+ "max",
11763
+ "sum"
11764
+ ];
11765
+ var BINARY_AGGREGATES = ["any", "all"];
11766
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11767
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11768
+ var AGGREGATE_REDUCERS = [
11769
+ "mean",
11770
+ "median",
11771
+ "min",
11772
+ "max",
11773
+ "sum",
11774
+ "any",
11775
+ "all"
11776
+ ];
11777
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11778
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11779
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11780
+ /** One member. The field is stored so occupancy can name motion or binary. */
11781
+ var HelperMemberSchema = object({
11782
+ source: CompositionSourceRefSchema,
11783
+ cap: string().min(1),
11784
+ fieldPath: string().min(1)
11785
+ });
11786
+ var HelperSpecSchema = discriminatedUnion("kind", [
11787
+ object({
11788
+ kind: literal("group"),
11789
+ templateId: GroupTemplateIdSchema,
11790
+ aggregate: AggregateReducerSchema,
11791
+ members: array(HelperMemberSchema).min(1).max(64),
11792
+ /** Actuator groups only. Readings leave this unset. */
11793
+ active: ActiveCriterionSchema.optional()
11794
+ }),
11795
+ object({
11796
+ kind: literal("appliance"),
11797
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
11798
+ appliance: _enum(APPLIANCE_KINDS),
11799
+ thresholdWatts: number().positive(),
11800
+ startDwellMs: number().int().nonnegative(),
11801
+ stopDwellMs: number().int().nonnegative(),
11802
+ member: HelperMemberSchema
11803
+ }),
11804
+ object({
11805
+ kind: literal("statistic"),
11806
+ stat: _enum(STATISTIC_STATS),
11807
+ window: StatisticWindowSchema,
11808
+ source: HelperMemberSchema
11809
+ }),
11810
+ object({
11811
+ kind: literal("integral"),
11812
+ reset: _enum(RESET_PERIODS),
11813
+ output: _enum(["energy", "meter"]),
11814
+ source: HelperMemberSchema
11815
+ }),
11816
+ object({
11817
+ kind: literal("threshold"),
11818
+ source: HelperMemberSchema,
11819
+ high: number(),
11820
+ low: number(),
11821
+ onForMs: number().int().nonnegative(),
11822
+ offForMs: number().int().nonnegative()
11823
+ }),
11824
+ object({
11825
+ kind: literal("arithmetic"),
11826
+ op: _enum([
11827
+ "sum",
11828
+ "difference",
11829
+ "ratio"
11830
+ ]),
11831
+ inputs: array(HelperMemberSchema).min(2).max(64)
11832
+ }),
11833
+ object({
11834
+ kind: literal("expression"),
11835
+ expr: string().min(1),
11836
+ inputs: array(object({
11837
+ name: string().min(1),
11838
+ member: HelperMemberSchema
11839
+ })).min(1).max(64),
11840
+ output: _enum(["number", "boolean"])
11841
+ }),
11842
+ object({
11843
+ kind: literal("derivative"),
11844
+ window: RollingWindowSchema,
11845
+ per: _enum([
11846
+ "second",
11847
+ "minute",
11848
+ "hour"
11849
+ ]),
11850
+ source: HelperMemberSchema
11851
+ })
11852
+ ]);
11853
+ /** A composed device's stableId is this prefix plus the block id. */
11854
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11855
+ /** Longest snippet a `code` source may carry (slice 4). */
11856
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11596
11857
  /** One field of one capability of one source device. */
11597
11858
  var CompositionFieldReadSchema = object({
11598
11859
  source: CompositionSourceRefSchema,
@@ -11636,7 +11897,59 @@ var CompositionCodeSourceSchema = object({
11636
11897
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11637
11898
  CompositionFromSourceSchema,
11638
11899
  CompositionExpressionSourceSchema,
11639
- CompositionCodeSourceSchema
11900
+ CompositionCodeSourceSchema,
11901
+ object({
11902
+ kind: literal("aggregate"),
11903
+ reducer: _enum([
11904
+ "mean",
11905
+ "median",
11906
+ "min",
11907
+ "max",
11908
+ "sum",
11909
+ "any",
11910
+ "all"
11911
+ ]),
11912
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11913
+ /**
11914
+ * Set when the reads are an enum state, not a yes/no: the listed states
11915
+ * count as active (a cover is `open`). A state outside the list is inactive;
11916
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
11917
+ */
11918
+ activeWhen: array(string().min(1)).min(1).optional(),
11919
+ /** `value` is the reading; the others are the member counts. */
11920
+ publish: _enum([
11921
+ "value",
11922
+ "included",
11923
+ "excluded",
11924
+ "total"
11925
+ ]),
11926
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11927
+ empty: _enum([
11928
+ "null",
11929
+ "omit",
11930
+ "offline"
11931
+ ])
11932
+ }),
11933
+ object({
11934
+ kind: literal("series"),
11935
+ read: CompositionFieldReadSchema,
11936
+ /** `value` is the statistic or the integral. The others are the gap counts. */
11937
+ publish: _enum([
11938
+ "value",
11939
+ "included",
11940
+ "excluded",
11941
+ "total"
11942
+ ]),
11943
+ /**
11944
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
11945
+ * device unseeded until history answers, so a restart cannot publish 0.
11946
+ */
11947
+ empty: _enum([
11948
+ "null",
11949
+ "omit",
11950
+ "offline"
11951
+ ])
11952
+ })
11640
11953
  ]);
11641
11954
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11642
11955
  kind: literal("forward"),
@@ -11682,7 +11995,13 @@ var CompositionSchema = object({
11682
11995
  kind: literal("existing"),
11683
11996
  device: CompositionSourceRefSchema
11684
11997
  })]),
11685
- features: array(CompositionFeatureSchema).min(1).max(16)
11998
+ features: array(CompositionFeatureSchema).min(1).max(16),
11999
+ /**
12000
+ * Present when the block is a helper template. Absent on a free
12001
+ * composition (the room example, device 4603). The editor reads this,
12002
+ * never the compiled features.
12003
+ */
12004
+ helper: HelperSpecSchema.optional()
11686
12005
  }).superRefine((composition, ctx) => {
11687
12006
  const seen = /* @__PURE__ */ new Set();
11688
12007
  composition.features.forEach((feature, index) => {
@@ -16827,7 +17146,22 @@ var BrokerProviderInfoSchema = object({
16827
17146
  */
16828
17147
  interactiveConnectPath: string().min(1).optional(),
16829
17148
  /** Relative hub path to disconnect the linked account (same popup pattern). */
16830
- interactiveDisconnectPath: string().min(1).optional()
17149
+ interactiveDisconnectPath: string().min(1).optional(),
17150
+ /**
17151
+ * The kind signs in through a server-held interactive session instead of
17152
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17153
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17154
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17155
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17156
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17157
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17158
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17159
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17160
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17161
+ * offers it as the default path; the fields the session fills are
17162
+ * marked `filledBySignIn` in the kind's settings schema.
17163
+ */
17164
+ signIn: object({ basePath: string().min(1) }).optional()
16831
17165
  }))
16832
17166
  });
16833
17167
  var ListInputSchema = object({
@@ -17928,7 +18262,12 @@ var CompositionProblemSchema = object({
17928
18262
  "item-field-reserved",
17929
18263
  "target-missing",
17930
18264
  "target-unreadable",
17931
- "not-replaceable"
18265
+ "not-replaceable",
18266
+ "native-capability-replace",
18267
+ "mixed-quantity",
18268
+ "helper-mismatch",
18269
+ "member-constraint",
18270
+ "retention"
17932
18271
  ]),
17933
18272
  path: string(),
17934
18273
  message: string()
@@ -17949,7 +18288,8 @@ var CompositionUnavailableModeSchema = _enum([
17949
18288
  "null",
17950
18289
  "offline",
17951
18290
  "unknown",
17952
- "release"
18291
+ "release",
18292
+ "omit"
17953
18293
  ]);
17954
18294
  /**
17955
18295
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17986,9 +18326,11 @@ var CompositionFieldPlanSchema = object({
17986
18326
  reads: array(CompositionFieldReadSchema)
17987
18327
  });
17988
18328
  /**
17989
- * What a feature does to its target (D663): `new` builds a capability on a new
17990
- * device; on an existing device it `add`s a capability the device lacks, or
17991
- * `replace`s named fields of one it has natively.
18329
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18330
+ * new device; on an existing device it `add`s a capability the device lacks.
18331
+ * Replacing a capability the device already serves is refused
18332
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18333
+ * wire for a claim the hub already holds.
17992
18334
  */
17993
18335
  var CompositionFeatureModeSchema = _enum([
17994
18336
  "new",
@@ -18159,8 +18501,392 @@ Object.freeze({
18159
18501
  latchedAt: {
18160
18502
  kind: "fixed",
18161
18503
  result: ["number", "null"]
18504
+ },
18505
+ schmitt: {
18506
+ kind: "fixed",
18507
+ result: ["boolean", "null"]
18162
18508
  }
18163
18509
  });
18510
+ /**
18511
+ * One catalog row per base reading a group helper can build (D944 §3).
18512
+ * Data, not a form default written into the block.
18513
+ */
18514
+ var NOW$2 = {
18515
+ fieldPath: "lastFetchedAt",
18516
+ expr: "now"
18517
+ };
18518
+ var CHANGED = {
18519
+ fieldPath: "lastChangedAt",
18520
+ expr: "now"
18521
+ };
18522
+ function numeric(input) {
18523
+ return {
18524
+ ...input,
18525
+ family: "numeric",
18526
+ deviceType: DeviceType.Sensor,
18527
+ memberDeviceType: DeviceType.Sensor,
18528
+ memberDeviceTypes: [DeviceType.Sensor],
18529
+ activeWhen: void 0,
18530
+ commandCaps: [],
18531
+ baseUnit: baseUnitOf(input.quantity),
18532
+ aggregates: NUMERIC_AGGREGATES,
18533
+ accept: [{
18534
+ cap: input.outputCap,
18535
+ fieldPath: input.outputField
18536
+ }]
18537
+ };
18538
+ }
18539
+ function binary(input) {
18540
+ return {
18541
+ ...input,
18542
+ family: "binary",
18543
+ deviceType: DeviceType.Sensor,
18544
+ memberDeviceType: DeviceType.Sensor,
18545
+ memberDeviceTypes: [DeviceType.Sensor],
18546
+ activeWhen: void 0,
18547
+ commandCaps: [],
18548
+ quantity: "none",
18549
+ baseUnit: void 0,
18550
+ aggregates: BINARY_AGGREGATES,
18551
+ empty: "offline"
18552
+ };
18553
+ }
18554
+ var GROUP_TEMPLATES = [
18555
+ numeric({
18556
+ id: "temperature",
18557
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18558
+ role: DeviceRole.TemperatureSensor,
18559
+ outputCap: "temperature-sensor",
18560
+ outputField: "celsius",
18561
+ quantity: "temperature",
18562
+ defaultAggregate: "mean",
18563
+ empty: "null",
18564
+ companions: [NOW$2]
18565
+ }),
18566
+ numeric({
18567
+ id: "humidity",
18568
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18569
+ role: DeviceRole.HumiditySensor,
18570
+ outputCap: "humidity-sensor",
18571
+ outputField: "percent",
18572
+ quantity: "humidity",
18573
+ defaultAggregate: "mean",
18574
+ empty: "null",
18575
+ companions: [NOW$2]
18576
+ }),
18577
+ numeric({
18578
+ id: "pressure",
18579
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18580
+ role: DeviceRole.PressureSensor,
18581
+ outputCap: "pressure-sensor",
18582
+ outputField: "hpa",
18583
+ quantity: "pressure",
18584
+ defaultAggregate: "mean",
18585
+ empty: "null",
18586
+ companions: [NOW$2]
18587
+ }),
18588
+ numeric({
18589
+ id: "illuminance",
18590
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18591
+ role: DeviceRole.AmbientLightSensor,
18592
+ outputCap: "ambient-light-sensor",
18593
+ outputField: "lux",
18594
+ quantity: "illuminance",
18595
+ defaultAggregate: "mean",
18596
+ empty: "null",
18597
+ companions: [NOW$2]
18598
+ }),
18599
+ numeric({
18600
+ id: "power",
18601
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18602
+ role: DeviceRole.PowerSensor,
18603
+ outputCap: "power-meter",
18604
+ outputField: "watts",
18605
+ quantity: "power",
18606
+ defaultAggregate: "sum",
18607
+ empty: "omit",
18608
+ companions: [NOW$2]
18609
+ }),
18610
+ numeric({
18611
+ id: "energy",
18612
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18613
+ role: DeviceRole.EnergySensor,
18614
+ outputCap: "power-meter",
18615
+ outputField: "kwhTotal",
18616
+ quantity: "energy",
18617
+ defaultAggregate: "sum",
18618
+ empty: "omit",
18619
+ companions: [NOW$2]
18620
+ }),
18621
+ numeric({
18622
+ id: "battery",
18623
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18624
+ role: DeviceRole.BatterySensor,
18625
+ outputCap: "battery",
18626
+ outputField: "percentage",
18627
+ quantity: "percentage",
18628
+ defaultAggregate: "mean",
18629
+ empty: "null",
18630
+ companions: [
18631
+ {
18632
+ fieldPath: "charging",
18633
+ expr: "\"unknown\""
18634
+ },
18635
+ {
18636
+ fieldPath: "sleeping",
18637
+ expr: "false"
18638
+ },
18639
+ {
18640
+ fieldPath: "lastUpdated",
18641
+ expr: "now"
18642
+ }
18643
+ ]
18644
+ }),
18645
+ numeric({
18646
+ id: "numeric",
18647
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18648
+ role: DeviceRole.NumericSensor,
18649
+ outputCap: "numeric-sensor",
18650
+ outputField: "value",
18651
+ quantity: "device",
18652
+ defaultAggregate: "mean",
18653
+ empty: "null",
18654
+ companions: [NOW$2]
18655
+ }),
18656
+ binary({
18657
+ id: "motion",
18658
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18659
+ role: DeviceRole.MotionSensor,
18660
+ outputCap: "motion",
18661
+ outputField: "detected",
18662
+ defaultAggregate: "any",
18663
+ accept: [{
18664
+ cap: "motion",
18665
+ fieldPath: "detected"
18666
+ }],
18667
+ companions: [{
18668
+ fieldPath: "lastDetectedAt",
18669
+ expr: "null"
18670
+ }, {
18671
+ fieldPath: "autoClearAfterMs",
18672
+ expr: "null"
18673
+ }]
18674
+ }),
18675
+ binary({
18676
+ id: "contact",
18677
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18678
+ role: DeviceRole.ContactSensor,
18679
+ outputCap: "contact",
18680
+ outputField: "entryOpen",
18681
+ defaultAggregate: "any",
18682
+ accept: [{
18683
+ cap: "contact",
18684
+ fieldPath: "entryOpen"
18685
+ }],
18686
+ companions: [CHANGED]
18687
+ }),
18688
+ binary({
18689
+ id: "leak",
18690
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18691
+ role: DeviceRole.LeakSensor,
18692
+ outputCap: "flood",
18693
+ outputField: "flooded",
18694
+ defaultAggregate: "any",
18695
+ accept: [{
18696
+ cap: "flood",
18697
+ fieldPath: "flooded"
18698
+ }],
18699
+ companions: [CHANGED]
18700
+ }),
18701
+ binary({
18702
+ id: "smoke",
18703
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18704
+ role: DeviceRole.SmokeSensor,
18705
+ outputCap: "smoke",
18706
+ outputField: "detected",
18707
+ defaultAggregate: "any",
18708
+ accept: [{
18709
+ cap: "smoke",
18710
+ fieldPath: "detected"
18711
+ }],
18712
+ companions: [CHANGED]
18713
+ }),
18714
+ binary({
18715
+ id: "occupancy",
18716
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18717
+ role: DeviceRole.BinarySensor,
18718
+ outputCap: "binary",
18719
+ outputField: "on",
18720
+ defaultAggregate: "any",
18721
+ accept: [{
18722
+ cap: "motion",
18723
+ fieldPath: "detected"
18724
+ }, {
18725
+ cap: "binary",
18726
+ fieldPath: "on"
18727
+ }],
18728
+ companions: [CHANGED]
18729
+ }),
18730
+ binary({
18731
+ id: "binary",
18732
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18733
+ role: DeviceRole.BinarySensor,
18734
+ outputCap: "binary",
18735
+ outputField: "on",
18736
+ defaultAggregate: "any",
18737
+ accept: [{
18738
+ cap: "binary",
18739
+ fieldPath: "on"
18740
+ }],
18741
+ companions: [CHANGED]
18742
+ }),
18743
+ actuator({
18744
+ id: "switch",
18745
+ 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.",
18746
+ deviceType: DeviceType.Switch,
18747
+ outputCap: "switch",
18748
+ outputField: "on",
18749
+ member: {
18750
+ cap: "switch",
18751
+ fieldPath: "on"
18752
+ },
18753
+ commandCaps: ["switch"]
18754
+ }),
18755
+ actuator({
18756
+ id: "light",
18757
+ 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.",
18758
+ deviceType: DeviceType.Light,
18759
+ outputCap: "switch",
18760
+ outputField: "on",
18761
+ member: {
18762
+ cap: "switch",
18763
+ fieldPath: "on"
18764
+ },
18765
+ commandCaps: [
18766
+ "switch",
18767
+ "brightness",
18768
+ "color"
18769
+ ]
18770
+ }),
18771
+ actuator({
18772
+ id: "fan",
18773
+ 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.",
18774
+ deviceType: DeviceType.Fan,
18775
+ outputCap: "switch",
18776
+ outputField: "on",
18777
+ member: {
18778
+ cap: "switch",
18779
+ fieldPath: "on"
18780
+ },
18781
+ commandCaps: ["switch", "fan-control"]
18782
+ }),
18783
+ actuator({
18784
+ id: "cover",
18785
+ 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.",
18786
+ deviceType: DeviceType.Cover,
18787
+ outputCap: "group-active",
18788
+ outputField: "active",
18789
+ member: {
18790
+ cap: "cover",
18791
+ fieldPath: "state"
18792
+ },
18793
+ activeWhen: [
18794
+ "open",
18795
+ "opening",
18796
+ "stopped"
18797
+ ],
18798
+ commandCaps: ["cover"]
18799
+ }),
18800
+ actuator({
18801
+ id: "valve",
18802
+ 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.",
18803
+ deviceType: DeviceType.Valve,
18804
+ outputCap: "group-active",
18805
+ outputField: "active",
18806
+ member: {
18807
+ cap: "valve",
18808
+ fieldPath: "state"
18809
+ },
18810
+ activeWhen: [
18811
+ "open",
18812
+ "opening",
18813
+ "stopped"
18814
+ ],
18815
+ commandCaps: ["valve"]
18816
+ }),
18817
+ actuator({
18818
+ id: "lock",
18819
+ 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.",
18820
+ deviceType: DeviceType.Lock,
18821
+ outputCap: "group-active",
18822
+ outputField: "active",
18823
+ member: {
18824
+ cap: "lock-control",
18825
+ fieldPath: "state"
18826
+ },
18827
+ activeWhen: ["locked"],
18828
+ commandCaps: ["lock-control"]
18829
+ }),
18830
+ actuator({
18831
+ id: "media-player",
18832
+ 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.",
18833
+ deviceType: DeviceType.MediaPlayer,
18834
+ outputCap: "group-active",
18835
+ outputField: "active",
18836
+ member: {
18837
+ cap: "media-player",
18838
+ fieldPath: "state"
18839
+ },
18840
+ activeWhen: ["playing", "buffering"],
18841
+ commandCaps: ["media-player"]
18842
+ }),
18843
+ actuator({
18844
+ id: "climate",
18845
+ 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.",
18846
+ deviceType: DeviceType.Thermostat,
18847
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
18848
+ outputCap: "group-active",
18849
+ outputField: "active",
18850
+ member: {
18851
+ cap: "climate-control",
18852
+ fieldPath: "mode"
18853
+ },
18854
+ activeWhen: [
18855
+ "heat",
18856
+ "cool",
18857
+ "auto",
18858
+ "heat_cool",
18859
+ "fan_only",
18860
+ "dry"
18861
+ ],
18862
+ commandCaps: ["climate-control"]
18863
+ })
18864
+ ];
18865
+ function actuator(input) {
18866
+ return {
18867
+ id: input.id,
18868
+ summary: input.summary,
18869
+ deviceType: input.deviceType,
18870
+ outputCap: input.outputCap,
18871
+ outputField: input.outputField,
18872
+ commandCaps: input.commandCaps,
18873
+ family: "actuator",
18874
+ role: void 0,
18875
+ memberDeviceType: input.deviceType,
18876
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
18877
+ activeWhen: input.activeWhen,
18878
+ quantity: "none",
18879
+ baseUnit: void 0,
18880
+ aggregates: BINARY_AGGREGATES,
18881
+ defaultAggregate: "any",
18882
+ empty: "offline",
18883
+ accept: [input.member],
18884
+ companions: [CHANGED]
18885
+ };
18886
+ }
18887
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18888
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18889
+ DeviceType.Camera;
18164
18890
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18165
18891
  /**
18166
18892
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19987,12 +20713,12 @@ function buildDeviceAdminUrl(parts) {
19987
20713
  * providers detect renames by matching `uniqueId` and patch `id`
19988
20714
  * in-place so dispatch keeps working.
19989
20715
  *
19990
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19991
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19992
- * state push and are the single source of truth. The `device_class` tag is
19993
- * an HA-specific input that is consumed at the discovery boundary
19994
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19995
- * stored after that point.
20716
+ * **Display unit and precision are NOT stored here.** They are a read-time
20717
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20718
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20719
+ * neither is repeated on each sample. The `device_class` tag is an
20720
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20721
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19996
20722
  *
19997
20723
  * The `raw` escape hatch carries provider-specific attributes that don't
19998
20724
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20006,6 +20732,14 @@ var SourceInfoSchema = object({
20006
20732
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20007
20733
  * Used to detect rename when `id` changes. */
20008
20734
  uniqueId: string().optional(),
20735
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20736
+ quantity: ReadingQuantitySchema.optional(),
20737
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20738
+ dateTimeShape: _enum([
20739
+ "date",
20740
+ "time",
20741
+ "datetime"
20742
+ ]).optional(),
20009
20743
  /** Provider-specific extras that don't fit the structured slots.
20010
20744
  * Whitelist what goes here per provider — do NOT dump entire HA
20011
20745
  * attribute blobs (would explode the meta JSON column). */
@@ -41110,7 +41844,8 @@ method(object({
41110
41844
  "enum"
41111
41845
  ]),
41112
41846
  enumValues: array(string()).optional(),
41113
- item: boolean().optional()
41847
+ item: boolean().optional(),
41848
+ quantity: ReadingQuantitySchema.optional()
41114
41849
  })).readonly(),
41115
41850
  itemArray: object({
41116
41851
  path: string(),
@@ -43471,7 +44206,7 @@ var NetworkAccessStatusSchema = object({
43471
44206
  });
43472
44207
  /**
43473
44208
  * Optional, richer endpoint shape returned by providers that expose
43474
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44209
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43475
44210
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43476
44211
  * the originating provider config (mode + sourcePort) so the
43477
44212
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44954,8 +45689,8 @@ var MaskPolygonShapeSchema = object({
44954
45689
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44955
45690
  var MaskGridShapeSchema = object({
44956
45691
  kind: literal("grid"),
44957
- gridWidth: number(),
44958
- gridHeight: number(),
45692
+ gridWidth: number().meta(readingMeta("none")),
45693
+ gridHeight: number().meta(readingMeta("none")),
44959
45694
  cells: array(boolean())
44960
45695
  });
44961
45696
  discriminatedUnion("kind", [
@@ -45067,6 +45802,8 @@ var NcSystemEventKindSchema = _enum([
45067
45802
  "detection-blind",
45068
45803
  "motion-noisy",
45069
45804
  "motion-calm",
45805
+ "package-llm-silent",
45806
+ "package-llm-answered",
45070
45807
  "recording-stalled",
45071
45808
  "recording-healthy",
45072
45809
  "recording-degraded",
@@ -46156,7 +46893,20 @@ var NcRuleInputSchema = object({
46156
46893
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46157
46894
  * path does not run Zod defaults.
46158
46895
  */
46159
- groupingSec: number().int().min(5).max(60).optional()
46896
+ groupingSec: number().int().min(5).max(60).optional(),
46897
+ /**
46898
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46899
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46900
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46901
+ * same mark the Library writes, and records whose notification it was and
46902
+ * when. A channel with no buttons gets the same two choices as links.
46903
+ *
46904
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46905
+ * cap path): absent and `false` both mean off, which is what every rule
46906
+ * authored before this field meant. An editor turns it off by writing
46907
+ * `false`, so it needs no `clear` key.
46908
+ */
46909
+ askVerdict: boolean().optional()
46160
46910
  });
46161
46911
  /**
46162
46912
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -56671,6 +57421,28 @@ var PackageVersionInfoSchema = object({
56671
57421
  deprecated: string().optional(),
56672
57422
  distTags: array(string()).readonly()
56673
57423
  });
57424
+ /**
57425
+ * One published document. `ok` is markdown from the registry. `missing` means
57426
+ * the registry answered and the file was not there. `error` is a registry or
57427
+ * tarball failure — never an empty string standing in for either.
57428
+ */
57429
+ var PackageDocBodySchema = discriminatedUnion("status", [
57430
+ object({
57431
+ status: literal("ok"),
57432
+ markdown: string()
57433
+ }),
57434
+ object({ status: literal("missing") }),
57435
+ object({
57436
+ status: literal("error"),
57437
+ message: string()
57438
+ })
57439
+ ]);
57440
+ var PackageDocsSchema = object({
57441
+ name: string(),
57442
+ version: string(),
57443
+ readme: PackageDocBodySchema,
57444
+ changelog: PackageDocBodySchema
57445
+ });
56674
57446
  var SearchResultSchema = object({
56675
57447
  name: string(),
56676
57448
  version: string(),
@@ -56889,7 +57661,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56889
57661
  }), object({ success: literal(true) }), {
56890
57662
  kind: "mutation",
56891
57663
  auth: "admin"
56892
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57664
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57665
+ name: string().min(1),
57666
+ version: string().min(1).optional()
57667
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56893
57668
  kind: "mutation",
56894
57669
  auth: "admin"
56895
57670
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56948,27 +57723,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56948
57723
  */
56949
57724
  var AirQualitySensorStatusSchema = object({
56950
57725
  /** Carbon dioxide concentration in ppm. */
56951
- co2Ppm: number().min(0).optional(),
57726
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56952
57727
  /** Total volatile organic compounds in ppb. */
56953
- vocPpb: number().min(0).optional(),
57728
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56954
57729
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56955
- pm25: number().min(0).optional(),
57730
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56956
57731
  /** Particulate matter ≤ 10 μm in µg/m³. */
56957
- pm10: number().min(0).optional(),
57732
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56958
57733
  /** Composite AQI value (typically 0..500). */
56959
- aqi: number().optional(),
57734
+ aqi: number().optional().meta(readingMeta("index")),
56960
57735
  /** Ms epoch when the slice was last updated. */
56961
- lastFetchedAt: number(),
56962
- /** Live display unit of the single metric this slice carries (e.g. HA
56963
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56964
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56965
- * per slice is unambiguous. */
56966
- unit: string().optional(),
56967
- /** Suggested decimal places for numeric display.
56968
- * Populated live from the upstream source when provided (e.g. HA
56969
- * `attributes.suggested_display_precision`). Falls back to
56970
- * auto-formatting when absent. */
56971
- precision: number().int().min(0).max(10).optional()
57736
+ lastFetchedAt: number()
56972
57737
  });
56973
57738
  var airQualitySensorCapability = {
56974
57739
  name: "air-quality-sensor",
@@ -57005,12 +57770,12 @@ var airQualitySensorCapability = {
57005
57770
  },
57006
57771
  pm25: {
57007
57772
  kind: "gauge",
57008
- unit: "ug/m3",
57773
+ unit: "µg/m³",
57009
57774
  deadband: 1
57010
57775
  },
57011
57776
  pm10: {
57012
57777
  kind: "gauge",
57013
- unit: "ug/m3",
57778
+ unit: "µg/m³",
57014
57779
  deadband: 1
57015
57780
  },
57016
57781
  aqi: {
@@ -57028,22 +57793,13 @@ var airQualitySensorCapability = {
57028
57793
  * entries with `device_class: illuminance`.
57029
57794
  */
57030
57795
  var AmbientLightSensorStatusSchema = object({
57031
- /** Current illuminance in lux (lx). */
57032
- lux: number().min(0).meta({
57033
- unit: "lx",
57796
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57797
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57798
+ precision: 0,
57034
57799
  label: "Illuminance"
57035
- }),
57800
+ })),
57036
57801
  /** Ms epoch when the slice was last updated. */
57037
- lastFetchedAt: number(),
57038
- /** Live display unit from the upstream source (e.g. HA
57039
- * `attributes.unit_of_measurement`). The UI prefers this over the
57040
- * role's canonical unit. Absent → fall back to the canonical unit. */
57041
- unit: string().optional(),
57042
- /** Suggested decimal places for numeric display.
57043
- * Populated live from the upstream source when provided (e.g. HA
57044
- * `attributes.suggested_display_precision`). Falls back to
57045
- * auto-formatting when absent. */
57046
- precision: number().int().min(0).max(10).optional()
57802
+ lastFetchedAt: number()
57047
57803
  });
57048
57804
  var ambientLightSensorCapability = {
57049
57805
  name: "ambient-light-sensor",
@@ -57075,12 +57831,7 @@ var ambientLightSensorCapability = {
57075
57831
  } } },
57076
57832
  /** Clock fields: written, but excluded from the compare that decides
57077
57833
  * whether persisting is worth a SQLite commit. */
57078
- volatileStateFields: ["lastFetchedAt"],
57079
- customize: {
57080
- version: 1,
57081
- advanced: "derive",
57082
- targets: []
57083
- }
57834
+ volatileStateFields: ["lastFetchedAt"]
57084
57835
  };
57085
57836
  /**
57086
57837
  * Per-class audio metrics aggregated over a sliding window.
@@ -57307,10 +58058,7 @@ var BatteryStatusSchema = object({
57307
58058
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57308
58059
  * declines to band a non-finite reading, which is the correct shape.
57309
58060
  */
57310
- percentage: number().min(0).max(100).nullable().meta({
57311
- unit: "%",
57312
- label: "Charge"
57313
- }),
58061
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57314
58062
  /**
57315
58063
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57316
58064
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57337,7 +58085,14 @@ var BatteryStatusSchema = object({
57337
58085
  */
57338
58086
  sleeping: boolean().meta({ label: "Sleeping" }),
57339
58087
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57340
- lastUpdated: number(),
58088
+ lastUpdated: number().meta(readingMeta("none")),
58089
+ /**
58090
+ * Battery voltage in volts, for a cell the firmware reports as voltage
58091
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
58092
+ * that only report charge. A second reading on this cap, not a second
58093
+ * `numeric-sensor` on the same device.
58094
+ */
58095
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57341
58096
  /**
57342
58097
  * Ms epoch of the last time the device PROVED it was reachable — a
57343
58098
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57384,12 +58139,12 @@ var BATTERY_DEVICE_TYPES = [
57384
58139
  DeviceType.Switch,
57385
58140
  DeviceType.PetFeeder,
57386
58141
  DeviceType.Container,
57387
- DeviceType.Thermostat
58142
+ DeviceType.Thermostat,
58143
+ DeviceType.EventEmitter
57388
58144
  ];
57389
58145
  /** Generic Customize target for the percentage reading (D936). */
57390
58146
  var batteryCustomize = {
57391
58147
  version: 1,
57392
- advanced: "derive",
57393
58148
  targets: [{
57394
58149
  id: "percentage",
57395
58150
  cardinality: "scalar",
@@ -57398,7 +58153,7 @@ var batteryCustomize = {
57398
58153
  stateField: "percentage"
57399
58154
  },
57400
58155
  deviceTypes: BATTERY_DEVICE_TYPES,
57401
- apply: "add-or-replace",
58156
+ apply: "add",
57402
58157
  path: "percentage",
57403
58158
  sources: [{
57404
58159
  kind: "field",
@@ -57519,10 +58274,16 @@ onStatusChanged: { data: object({
57519
58274
  /**
57520
58275
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57521
58276
  */
57522
- history: { fields: { percentage: {
57523
- kind: "gauge",
57524
- unit: "%"
57525
- } } },
58277
+ history: { fields: {
58278
+ percentage: {
58279
+ kind: "gauge",
58280
+ unit: "%"
58281
+ },
58282
+ volts: {
58283
+ kind: "gauge",
58284
+ unit: "V"
58285
+ }
58286
+ } },
57526
58287
  customize: batteryCustomize,
57527
58288
  /** Clock fields: written, but excluded from the compare that decides
57528
58289
  * whether persisting is worth a SQLite commit. */
@@ -57569,12 +58330,7 @@ var binaryCapability = {
57569
58330
  history: { fields: { on: { kind: "boolean" } } },
57570
58331
  /** Clock fields: written, but excluded from the compare that decides
57571
58332
  * whether persisting is worth a SQLite commit. */
57572
- volatileStateFields: ["lastChangedAt"],
57573
- customize: {
57574
- version: 1,
57575
- advanced: "derive",
57576
- targets: []
57577
- }
58333
+ volatileStateFields: ["lastChangedAt"]
57578
58334
  };
57579
58335
  /**
57580
58336
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57590,7 +58346,7 @@ var binaryCapability = {
57590
58346
  */
57591
58347
  var BrightnessStatusSchema = object({
57592
58348
  /** Current level as 0..100 inclusive. Firmware-reported. */
57593
- percentage: number().min(0).max(100),
58349
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57594
58350
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57595
58351
  lastChangedAt: number()
57596
58352
  });
@@ -58044,21 +58800,21 @@ var ClimateControlStatusSchema = object({
58044
58800
  /** Single setpoint in Celsius. Used by single-target modes
58045
58801
  * (heat / cool / dry). Null when the device is in a range mode
58046
58802
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58047
- target: number().nullable(),
58803
+ target: number().nullable().meta(readingMeta("temperature")),
58048
58804
  /** Upper bound of the dual setpoint in Celsius. Populated only
58049
58805
  * in `heat_cool` mode. */
58050
- targetHigh: number().nullable(),
58806
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58051
58807
  /** Lower bound of the dual setpoint in Celsius. Populated only
58052
58808
  * in `heat_cool` mode. */
58053
- targetLow: number().nullable(),
58809
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58054
58810
  /** Target relative humidity (0..100). Null when the device has no
58055
58811
  * humidity control. */
58056
- targetHumidity: number().min(0).max(100).nullable(),
58812
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58057
58813
  /** Current measured temperature in Celsius. Read-only — pushed by
58058
58814
  * the device's internal sensor. */
58059
- currentTemp: number().nullable(),
58815
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58060
58816
  /** Current measured relative humidity (0..100). Read-only. */
58061
- currentHumidity: number().min(0).max(100).nullable(),
58817
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58062
58818
  /** Vertical louver swing on/off. Null when the device has no vertical
58063
58819
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58064
58820
  swingVertical: boolean().nullable(),
@@ -58066,11 +58822,11 @@ var ClimateControlStatusSchema = object({
58066
58822
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58067
58823
  swingHorizontal: boolean().nullable(),
58068
58824
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58069
- minTemp: number().nullable().optional(),
58825
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58070
58826
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58071
- maxTemp: number().nullable().optional(),
58827
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58072
58828
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58073
- tempStep: number().positive().nullable().optional(),
58829
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58074
58830
  /** Ms epoch when the slice was last updated (push or command). */
58075
58831
  lastFetchedAt: number()
58076
58832
  });
@@ -58212,17 +58968,17 @@ var climateControlCapability = {
58212
58968
  * modes to expose in the picker without a slice fetch.
58213
58969
  */
58214
58970
  var RgbTripletSchema = object({
58215
- r: number().int().min(0).max(255),
58216
- g: number().int().min(0).max(255),
58217
- b: number().int().min(0).max(255)
58971
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58972
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58973
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58218
58974
  });
58219
58975
  var HsvTripletSchema = object({
58220
58976
  /** Hue in degrees, 0..360. */
58221
- h: number().min(0).max(360),
58977
+ h: number().min(0).max(360).meta(readingMeta("none")),
58222
58978
  /** Saturation as 0..100 inclusive. */
58223
- s: number().min(0).max(100),
58979
+ s: number().min(0).max(100).meta(readingMeta("none")),
58224
58980
  /** Value/brightness as 0..100 inclusive. */
58225
- v: number().min(0).max(100)
58981
+ v: number().min(0).max(100).meta(readingMeta("none"))
58226
58982
  });
58227
58983
  var ColorInputSchema = discriminatedUnion("mode", [
58228
58984
  object({
@@ -58251,7 +59007,7 @@ var ColorStatusSchema = object({
58251
59007
  /** Populated when `mode === 'hsv'`. */
58252
59008
  hsv: HsvTripletSchema.optional(),
58253
59009
  /** Populated when `mode === 'mired'`. */
58254
- mireds: number().int().optional(),
59010
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58255
59011
  /** Ms epoch of the last operator-driven change. */
58256
59012
  lastChangedAt: number()
58257
59013
  });
@@ -58446,18 +59202,12 @@ _enum([
58446
59202
  "empty",
58447
59203
  "full"
58448
59204
  ]);
58449
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58450
- * each. */
58451
- var ConsumableRemainingUnitSchema = _enum([
58452
- "days",
58453
- "hours",
58454
- "cycles"
58455
- ]);
58456
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58457
- * days). `value` null = unknown (D393). */
59205
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
59206
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
59207
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58458
59208
  var ConsumableRemainingSchema = object({
58459
- value: number().min(0).nullable(),
58460
- unit: ConsumableRemainingUnitSchema
59209
+ value: number().min(0).nullable().meta(readingMeta("device")),
59210
+ kind: ConsumableRemainingKindSchema
58461
59211
  });
58462
59212
  /** A single consumable item. Either a continuous `level` (remaining
58463
59213
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58470,7 +59220,7 @@ var ConsumableItemSchema = object({
58470
59220
  /** Display name. */
58471
59221
  label: string().min(1),
58472
59222
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58473
- level: number().min(0).max(100).nullable(),
59223
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58474
59224
  /** The provider's discrete verdict when known (binary mode). */
58475
59225
  status: ConsumableStatusSchema.nullable(),
58476
59226
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58552,8 +59302,8 @@ var consumablesCustomize = {
58552
59302
  transforms: [{ kind: "identity" }]
58553
59303
  }],
58554
59304
  literals: [{
58555
- path: "remaining.unit",
58556
- values: [...ConsumableRemainingUnitSchema.options]
59305
+ path: "remaining.kind",
59306
+ values: [...ConsumableRemainingKindSchema.options]
58557
59307
  }]
58558
59308
  },
58559
59309
  {
@@ -58682,7 +59432,6 @@ reset: method(object({
58682
59432
  },
58683
59433
  "remaining.value": {
58684
59434
  kind: "gauge",
58685
- unitFrom: "remaining.unit",
58686
59435
  deadband: 1,
58687
59436
  displayName: "Remaining"
58688
59437
  }
@@ -58752,10 +59501,9 @@ var contactCapability = {
58752
59501
  * (empty array for the other kinds). It lives in the slice because
58753
59502
  * HA `select.*` entities may update their options at runtime.
58754
59503
  *
58755
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58756
- * `kind === 'numeric'` and are absent (undefined) for all other
58757
- * kinds. They are populated live from HA attributes on each state
58758
- * push — consistent with `options` and the sensor-unit approach.
59504
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59505
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59506
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58759
59507
  */
58760
59508
  var ControlStatusSchema = object({
58761
59509
  kind: _enum([
@@ -58767,39 +59515,17 @@ var ControlStatusSchema = object({
58767
59515
  /** Current value. Numeric kind → number; select/text/datetime → string.
58768
59516
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58769
59517
  * or time-only — HA accepts all three forms). */
58770
- value: union([number(), string()]),
59518
+ value: union([number().meta(readingMeta("device")), string()]),
58771
59519
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58772
59520
  options: array(string()),
58773
59521
  /** Ms epoch when the slice was last updated. */
58774
59522
  lastChangedAt: number(),
58775
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58776
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58777
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58778
- unit: string().optional(),
58779
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58780
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58781
- min: number().optional(),
58782
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58783
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58784
- max: number().optional(),
58785
- /** Step increment. Populated live from HA `attributes.step`.
58786
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58787
- step: number().optional(),
58788
- /** Suggested decimal places for numeric display. Populated live from HA
58789
- * `attributes.suggested_display_precision`, or derived from `step` when the
58790
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58791
- * other kinds. Falls back to auto-formatting when absent. */
58792
- precision: number().int().min(0).max(10).optional(),
58793
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58794
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58795
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58796
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58797
- * `has_date`/`has_time` attributes on each push. */
58798
- format: _enum([
58799
- "date",
58800
- "time",
58801
- "datetime"
58802
- ]).optional()
59523
+ /** Minimum allowed value, in the device quantity's base unit. */
59524
+ min: number().optional().meta(readingMeta("device")),
59525
+ /** Maximum allowed value, in the device quantity's base unit. */
59526
+ max: number().optional().meta(readingMeta("device")),
59527
+ /** Step increment, in the device quantity's base unit. */
59528
+ step: number().optional().meta(readingMeta("device"))
58803
59529
  });
58804
59530
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58805
59531
  object({
@@ -58865,9 +59591,9 @@ var CoverStatusSchema = object({
58865
59591
  ]),
58866
59592
  /** 0 = fully closed, 100 = fully open. Null when the device has
58867
59593
  * no intermediate position surface. */
58868
- position: number().min(0).max(100).nullable(),
59594
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58869
59595
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58870
- tiltPosition: number().min(0).max(100).nullable(),
59596
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58871
59597
  /** Ms epoch when the slice was last updated. */
58872
59598
  lastChangedAt: number()
58873
59599
  });
@@ -58970,9 +59696,9 @@ var NormalizedRangeSchema$1 = object({
58970
59696
  var DayNightStatusSchema = object({
58971
59697
  mode: DayNightModeSchema,
58972
59698
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58973
- sensitivity: number().optional(),
59699
+ sensitivity: number().optional().meta(readingMeta("none")),
58974
59700
  /** Delay before the IR-cut filter flips, in seconds. */
58975
- switchDelaySec: number().optional(),
59701
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58976
59702
  lastFetchedAt: number()
58977
59703
  });
58978
59704
  /**
@@ -59190,11 +59916,11 @@ var DishwasherStatusSchema = object({
59190
59916
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59191
59917
  availablePrograms: array(string()),
59192
59918
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59193
- remainingSeconds: number().int().nonnegative().nullable(),
59919
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59194
59920
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59195
- delayedStartSeconds: number().int().nonnegative().nullable(),
59921
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59196
59922
  /** Cycle completion 0..100, or null when not applicable. */
59197
- progressPercent: number().min(0).max(100).nullable(),
59923
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59198
59924
  /** True when the door is reported open; null when the door state is not known. */
59199
59925
  doorOpen: boolean().nullable(),
59200
59926
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59218,7 +59944,7 @@ var DishwasherStatusSchema = object({
59218
59944
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59219
59945
  machineCareDue: boolean().nullable(),
59220
59946
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59221
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59947
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59222
59948
  /** Option states: null when the appliance does not expose the option or has not said. */
59223
59949
  halfLoad: boolean().nullable(),
59224
59950
  hygienePlus: boolean().nullable(),
@@ -59233,7 +59959,7 @@ var DishwasherStatusSchema = object({
59233
59959
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59234
59960
  activeEvents: array(string()),
59235
59961
  /** Numeric error code when the appliance reports one; null otherwise. */
59236
- errorCode: number().nullable(),
59962
+ errorCode: number().nullable().meta(readingMeta("none")),
59237
59963
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59238
59964
  errorLabel: string().nullable(),
59239
59965
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59464,7 +60190,7 @@ var DoorbellStatusSchema = object({
59464
60190
  /** Ms epoch of the last press. null = never observed since this provider started. */
59465
60191
  lastPressedAt: number().nullable(),
59466
60192
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59467
- pressCountSinceStart: number()
60193
+ pressCountSinceStart: number().meta(readingMeta("count"))
59468
60194
  });
59469
60195
  var DoorbellPressEventSchema = object({
59470
60196
  deviceId: number(),
@@ -59529,17 +60255,7 @@ onPressed: {
59529
60255
  history: "none",
59530
60256
  customize: doorbellCustomize
59531
60257
  };
59532
- /**
59533
- * Enum-state sensor — a string value picked from a finite option set.
59534
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59535
- * states, weather conditions, contact-source channels, mode strings).
59536
- * The option set is stable across observations; it is persisted in the
59537
- * device config blob (via the control slice at adoption) and is NOT
59538
- * stored in `sourceInfo`.
59539
- */
59540
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59541
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59542
- var EnumSensorDateTimeFormatSchema = _enum([
60258
+ _enum([
59543
60259
  "date",
59544
60260
  "time",
59545
60261
  "datetime"
@@ -59547,12 +60263,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59547
60263
  var EnumSensorStatusSchema = object({
59548
60264
  value: string(),
59549
60265
  /**
59550
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59551
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59552
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59553
- * a time-only sensor → `'time'`.
60266
+ * Set when `value` is a timestamp rather than an enum state, so history
60267
+ * does not learn ISO strings as states. The display shape (date / time /
60268
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59554
60269
  */
59555
- format: EnumSensorDateTimeFormatSchema.optional(),
60270
+ timestamp: boolean().optional(),
59556
60271
  /** Ms epoch when the slice was last updated. */
59557
60272
  lastFetchedAt: number()
59558
60273
  });
@@ -59579,11 +60294,11 @@ var enumSensorCapability = {
59579
60294
  * History (D850): the value as an `enum` series (an appliance's program or
59580
60295
  * operation state, an HVAC action). The option set is per device, so the
59581
60296
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59582
- * `format` carries a timestamp, not a state, and is not recorded.
60297
+ * `timestamp` carries a clock, not a state, and is not recorded.
59583
60298
  */
59584
60299
  history: { fields: { value: {
59585
60300
  kind: "enum",
59586
- skipWhenSet: "format"
60301
+ skipWhenSet: "timestamp"
59587
60302
  } } },
59588
60303
  /** Clock fields: written, but excluded from the compare that decides
59589
60304
  * whether persisting is worth a SQLite commit. */
@@ -59603,13 +60318,13 @@ var EventFireSchema = object({
59603
60318
  deviceId: number(),
59604
60319
  eventType: string(),
59605
60320
  data: record(string(), unknown()).nullable(),
59606
- timestamp: number(),
59607
- seq: number()
60321
+ timestamp: number().meta(readingMeta("none")),
60322
+ seq: number().meta(readingMeta("count"))
59608
60323
  });
59609
60324
  var EventEmitterStatusSchema = object({
59610
60325
  eventTypes: array(string()),
59611
60326
  lastEvent: EventFireSchema.nullable(),
59612
- eventCountSinceStart: number()
60327
+ eventCountSinceStart: number().meta(readingMeta("count"))
59613
60328
  });
59614
60329
  var eventEmitterCapability = {
59615
60330
  name: "event-emitter",
@@ -60022,10 +60737,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60022
60737
  var FanControlStatusSchema = object({
60023
60738
  /** Active speed as 0..100 inclusive. Null when the device has no
60024
60739
  * speed surface (single-speed fan). */
60025
- percentage: number().min(0).max(100).nullable(),
60740
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60026
60741
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60027
60742
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60028
- percentageStep: number().positive().optional(),
60743
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60029
60744
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60030
60745
  * — empty string when no preset is active or supported. */
60031
60746
  preset: string(),
@@ -60146,7 +60861,7 @@ var FeatureProbeStatusSchema = object({
60146
60861
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60147
60862
  model: string().nullable(),
60148
60863
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60149
- channelCount: number().nullable(),
60864
+ channelCount: number().nullable().meta(readingMeta("count")),
60150
60865
  /**
60151
60866
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60152
60867
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60269,9 +60984,151 @@ var gasCapability = {
60269
60984
  * whether persisting is worth a SQLite commit. */
60270
60985
  volatileStateFields: ["lastChangedAt"]
60271
60986
  };
60987
+ var GroupCommandsSchema = object({
60988
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60989
+ results: array(object({
60990
+ deviceId: number().int().nonnegative(),
60991
+ cap: string().min(1),
60992
+ method: string().min(1),
60993
+ ok: boolean(),
60994
+ error: string().optional(),
60995
+ /** Ms epoch of the attempt. */
60996
+ at: number()
60997
+ })) });
60998
+ var groupCommandsCapability = {
60999
+ name: "group-commands",
61000
+ scope: "device",
61001
+ deviceNative: true,
61002
+ mode: "singleton",
61003
+ deviceTypes: [
61004
+ DeviceType.Switch,
61005
+ DeviceType.Light,
61006
+ DeviceType.Fan,
61007
+ DeviceType.Cover,
61008
+ DeviceType.Valve,
61009
+ DeviceType.Lock,
61010
+ DeviceType.MediaPlayer,
61011
+ DeviceType.Thermostat,
61012
+ DeviceType.Climate
61013
+ ],
61014
+ methods: {},
61015
+ status: {
61016
+ schema: GroupCommandsSchema,
61017
+ kind: "push"
61018
+ },
61019
+ runtimeState: GroupCommandsSchema,
61020
+ /**
61021
+ * Runtime-state durability: **session** — the last attempt. A restored
61022
+ * failure would outlive the command that caused it.
61023
+ *
61024
+ * See `RuntimeStateDurability`. Enforced by
61025
+ * `scripts/check-runtime-state-durability.ts`.
61026
+ */
61027
+ durability: "session",
61028
+ history: "none"
61029
+ };
61030
+ /**
61031
+ * Whether a helper actuator group counts as active (D944): at least one
61032
+ * included member is, or every included member is. What "active" means is the
61033
+ * type's: a cover is open, a lock is locked, a media player is playing, a
61034
+ * climate device is not off. A group of lights, switches and fans reports
61035
+ * `switch.on` instead; this cap is for the types whose own state is not a
61036
+ * yes/no. Unknown members are left out (`group-status` counts them) and an
61037
+ * empty included set takes the device offline, never `active: false`.
61038
+ */
61039
+ var GroupActiveSchema = object({
61040
+ active: boolean().meta({ label: "Active" }),
61041
+ /** Ms epoch when the slice was last updated. */
61042
+ lastChangedAt: number()
61043
+ });
61044
+ var groupActiveCapability = {
61045
+ name: "group-active",
61046
+ scope: "device",
61047
+ deviceNative: true,
61048
+ mode: "singleton",
61049
+ deviceTypes: [
61050
+ DeviceType.Cover,
61051
+ DeviceType.Valve,
61052
+ DeviceType.Lock,
61053
+ DeviceType.MediaPlayer,
61054
+ DeviceType.Climate,
61055
+ DeviceType.Thermostat
61056
+ ],
61057
+ methods: {},
61058
+ status: {
61059
+ schema: GroupActiveSchema,
61060
+ kind: "push"
61061
+ },
61062
+ runtimeState: GroupActiveSchema,
61063
+ /**
61064
+ * Runtime-state durability: **session** — recomputed from the members on the
61065
+ * next source event; a restored value would outlive the members it named.
61066
+ *
61067
+ * See `RuntimeStateDurability`. Enforced by
61068
+ * `scripts/check-runtime-state-durability.ts`.
61069
+ */
61070
+ durability: "session",
61071
+ history: "none"
61072
+ };
61073
+ /**
61074
+ * How many members of a helper group answered (D944). Counts are integers,
61075
+ * not measurements: zero included is a real count, never a stand-in reading.
61076
+ * The reading itself stays on the output capability. History is none — the
61077
+ * series that later statistics read is the output cap's own history block.
61078
+ */
61079
+ var GroupStatusSchema = object({
61080
+ /** Members whose current value entered the aggregate. */
61081
+ included: number().int().nonnegative().meta(readingMeta("count", {
61082
+ precision: 0,
61083
+ label: "Reporting"
61084
+ })),
61085
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
61086
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
61087
+ precision: 0,
61088
+ label: "Excluded"
61089
+ })),
61090
+ /** `included` + `excluded`. */
61091
+ total: number().int().nonnegative().meta(readingMeta("count", {
61092
+ precision: 0,
61093
+ label: "Members"
61094
+ }))
61095
+ });
61096
+ var groupStatusCapability = {
61097
+ name: "group-status",
61098
+ scope: "device",
61099
+ deviceNative: true,
61100
+ mode: "singleton",
61101
+ deviceTypes: [
61102
+ DeviceType.Sensor,
61103
+ DeviceType.Switch,
61104
+ DeviceType.Light,
61105
+ DeviceType.Fan,
61106
+ DeviceType.Cover,
61107
+ DeviceType.Valve,
61108
+ DeviceType.Lock,
61109
+ DeviceType.MediaPlayer,
61110
+ DeviceType.Thermostat,
61111
+ DeviceType.Climate
61112
+ ],
61113
+ methods: {},
61114
+ status: {
61115
+ schema: GroupStatusSchema,
61116
+ kind: "push"
61117
+ },
61118
+ runtimeState: GroupStatusSchema,
61119
+ /**
61120
+ * Runtime-state durability: **session** — recomputed from members on the
61121
+ * next source event; a restored count would outlive the readings it named.
61122
+ *
61123
+ * See `RuntimeStateDurability`. Enforced by
61124
+ * `scripts/check-runtime-state-durability.ts`.
61125
+ */
61126
+ durability: "session",
61127
+ history: "none"
61128
+ };
60272
61129
  var HeatingValveStatusSchema = object({
60273
61130
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60274
- position: number().min(0).max(100).nullable(),
61131
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60275
61132
  /** Ms epoch when the slice was last updated (push or poll). */
60276
61133
  lastFetchedAt: number()
60277
61134
  });
@@ -60314,9 +61171,9 @@ var HumidifierStatusSchema = object({
60314
61171
  /** Whether the humidifier is currently on. */
60315
61172
  on: boolean(),
60316
61173
  /** Current measured relative humidity (0..100). Null when not reported. */
60317
- currentHumidity: number().min(0).max(100).nullable(),
61174
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60318
61175
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60319
- targetHumidity: number().min(0).max(100).nullable(),
61176
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60320
61177
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60321
61178
  * device has no mode surface. */
60322
61179
  mode: string().nullable(),
@@ -60326,9 +61183,9 @@ var HumidifierStatusSchema = object({
60326
61183
  * `idle` / `off`). Null when the device doesn't report it. */
60327
61184
  action: string().nullable(),
60328
61185
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60329
- minHumidity: number().nullable(),
61186
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60330
61187
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60331
- maxHumidity: number().nullable(),
61188
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60332
61189
  /** Ms epoch when the slice was last updated. */
60333
61190
  lastChangedAt: number()
60334
61191
  });
@@ -60398,22 +61255,13 @@ var humidifierCapability = {
60398
61255
  * `sourceInfo`.
60399
61256
  */
60400
61257
  var HumiditySensorStatusSchema = object({
60401
- /** Current relative humidity, 0..100. */
60402
- percent: number().min(0).max(100).meta({
60403
- unit: "%",
61258
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
61259
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
61260
+ precision: 0,
60404
61261
  label: "Humidity"
60405
- }),
61262
+ })),
60406
61263
  /** Ms epoch when the slice was last updated. */
60407
- lastFetchedAt: number(),
60408
- /** Live display unit from the upstream source (e.g. HA
60409
- * `attributes.unit_of_measurement`). The UI prefers this over the
60410
- * role's canonical unit. Absent → fall back to the canonical unit. */
60411
- unit: string().optional(),
60412
- /** Suggested decimal places for numeric display.
60413
- * Populated live from the upstream source when provided (e.g. HA
60414
- * `attributes.suggested_display_precision`). Falls back to
60415
- * auto-formatting when absent. */
60416
- precision: number().int().min(0).max(10).optional()
61264
+ lastFetchedAt: number()
60417
61265
  });
60418
61266
  var humiditySensorCapability = {
60419
61267
  name: "humidity-sensor",
@@ -60444,12 +61292,7 @@ var humiditySensorCapability = {
60444
61292
  } } },
60445
61293
  /** Clock fields: written, but excluded from the compare that decides
60446
61294
  * whether persisting is worth a SQLite commit. */
60447
- volatileStateFields: ["lastFetchedAt"],
60448
- customize: {
60449
- version: 1,
60450
- advanced: "derive",
60451
- targets: []
60452
- }
61295
+ volatileStateFields: ["lastFetchedAt"]
60453
61296
  };
60454
61297
  /**
60455
61298
  * Image display cap. Models a single still image exposed by an integration —
@@ -60470,7 +61313,7 @@ var ImageStatusSchema = object({
60470
61313
  * entity exposes no `entity_picture` (yet). */
60471
61314
  url: string().nullable(),
60472
61315
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60473
- lastUpdated: number().nullable()
61316
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60474
61317
  });
60475
61318
  var imageCapability = {
60476
61319
  name: "image",
@@ -60558,19 +61401,19 @@ var NormalizedRangeSchema = object({
60558
61401
  */
60559
61402
  var ImageSettingsStatusSchema = object({
60560
61403
  /** Normalized 0–100. */
60561
- brightness: number().optional(),
61404
+ brightness: number().optional().meta(readingMeta("none")),
60562
61405
  /** Normalized 0–100. */
60563
- contrast: number().optional(),
61406
+ contrast: number().optional().meta(readingMeta("none")),
60564
61407
  /** Normalized 0–100. */
60565
- saturation: number().optional(),
61408
+ saturation: number().optional().meta(readingMeta("none")),
60566
61409
  /** Normalized 0–100. */
60567
- sharpness: number().optional(),
61410
+ sharpness: number().optional().meta(readingMeta("none")),
60568
61411
  mirror: boolean().optional(),
60569
61412
  flip: boolean().optional(),
60570
61413
  rotate: ImageRotateSchema.optional(),
60571
61414
  whiteBalance: WhiteBalanceModeSchema.optional(),
60572
61415
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60573
- warmth: number().optional(),
61416
+ warmth: number().optional().meta(readingMeta("none")),
60574
61417
  exposureMode: ExposureModeSchema.optional(),
60575
61418
  backlightMode: BacklightModeSchema.optional(),
60576
61419
  lastFetchedAt: number()
@@ -60849,10 +61692,10 @@ var IntercomAbilitySchema = object({
60849
61692
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60850
61693
  codecs: array(string()),
60851
61694
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60852
- sampleRate: number(),
61695
+ sampleRate: number().meta(readingMeta("none")),
60853
61696
  duplex: _enum(["half", "full"]),
60854
61697
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60855
- maxBacklogMs: number()
61698
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60856
61699
  });
60857
61700
  var IntercomStatusSchema = object({
60858
61701
  /** True while a talk session is open. */
@@ -61079,13 +61922,13 @@ var LawnMowerControlStatusSchema = object({
61079
61922
  activity: LawnMowerActivitySchema,
61080
61923
  /** 0..100 battery percentage. Null when the device has no battery
61081
61924
  * reading. */
61082
- batteryLevel: number().min(0).max(100).nullable(),
61925
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61083
61926
  /** 0..100 mowing-completion percentage of the current task, or null when no
61084
61927
  * task is active / progress is unavailable. */
61085
- progressPercent: number().min(0).max(100).nullable(),
61928
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61086
61929
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61087
61930
  * or null when unknown. */
61088
- currentCode: number().nullable(),
61931
+ currentCode: number().nullable().meta(readingMeta("none")),
61089
61932
  /** Human label for {@link currentCode}, or null when undecodable. */
61090
61933
  currentCodeLabel: string().nullable(),
61091
61934
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61538,7 +62381,14 @@ var lockControlCapability = {
61538
62381
  }),
61539
62382
  unlock: method(object({
61540
62383
  deviceId: number().int().nonnegative(),
61541
- code: string().min(1).optional()
62384
+ code: string().min(1).optional(),
62385
+ /**
62386
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
62387
+ * with one call is refused unless the caller says it was confirmed. A
62388
+ * single lock ignores it. The refusal lives in the callee, so no
62389
+ * automation or API caller reaches the doors by skipping a dialog.
62390
+ */
62391
+ confirm: boolean().optional()
61542
62392
  }), _void(), {
61543
62393
  kind: "mutation",
61544
62394
  auth: "protected"
@@ -61575,6 +62425,44 @@ var lockControlCapability = {
61575
62425
  volatileStateFields: ["lastChangedAt"]
61576
62426
  };
61577
62427
  /**
62428
+ * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
62429
+ * so dashboards keep one series; every other instantaneous reading lands here.
62430
+ * `value` is the time integral in the source quantity's accumulated base
62431
+ * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
62432
+ */
62433
+ var MeterStatusSchema = object({
62434
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
62435
+ lastFetchedAt: number()
62436
+ });
62437
+ var meterCapability = {
62438
+ name: "meter",
62439
+ scope: "device",
62440
+ deviceNative: true,
62441
+ mode: "singleton",
62442
+ deviceTypes: [DeviceType.Sensor],
62443
+ methods: {},
62444
+ status: {
62445
+ schema: MeterStatusSchema,
62446
+ kind: "push"
62447
+ },
62448
+ runtimeState: MeterStatusSchema,
62449
+ /**
62450
+ * Runtime-state durability: **restored** — the total is a cache of the
62451
+ * history integral. A restart shows it only until the history pass
62452
+ * recomputes the open period; it is not an in-memory buffer.
62453
+ *
62454
+ * See `RuntimeStateDurability`. Enforced by
62455
+ * `scripts/check-runtime-state-durability.ts`.
62456
+ */
62457
+ durability: "restored",
62458
+ history: { fields: { value: {
62459
+ kind: "counter",
62460
+ deadband: .001,
62461
+ minIntervalMs: 6e4
62462
+ } } },
62463
+ volatileStateFields: ["lastFetchedAt"]
62464
+ };
62465
+ /**
61578
62466
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
61579
62467
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
61580
62468
  * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
@@ -61618,7 +62506,7 @@ var MediaPlayerStatusSchema = object({
61618
62506
  state: MediaPlayerStateSchema,
61619
62507
  /** Volume as 0..100 inclusive. Null when the device has no volume
61620
62508
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61621
- volumeLevel: number().min(0).max(100).nullable(),
62509
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61622
62510
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61623
62511
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61624
62512
  isMuted: boolean().nullable(),
@@ -61631,10 +62519,10 @@ var MediaPlayerStatusSchema = object({
61631
62519
  currentMedia: MediaInfoSchema.nullable(),
61632
62520
  /** Current playback position in ms. Null when not seekable or
61633
62521
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61634
- positionMs: number().int().nonnegative().nullable(),
62522
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61635
62523
  /** Total duration of the current media in ms. Null when unknown
61636
62524
  * (live stream) or nothing is playing. */
61637
- durationMs: number().int().nonnegative().nullable(),
62525
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61638
62526
  /** Shuffle toggle. Null when no shuffle surface.
61639
62527
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61640
62528
  shuffle: boolean().nullable(),
@@ -61943,7 +62831,7 @@ var motionCustomize = {
61943
62831
  eventName: "onMotionChanged"
61944
62832
  },
61945
62833
  deviceTypes: [DeviceType.Camera],
61946
- apply: "replace",
62834
+ apply: "add",
61947
62835
  applyAuthority: {
61948
62836
  kind: "camera-sensors",
61949
62837
  timeoutField: "motionCooldownMs"
@@ -62015,7 +62903,7 @@ var MotionStatusSchema = object({
62015
62903
  * it, every provider seeded it at boot, and every session left it
62016
62904
  * `detected: true` after its detach.
62017
62905
  */
62018
- autoClearAfterMs: number().nullable()
62906
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62019
62907
  });
62020
62908
  /**
62021
62909
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62052,12 +62940,18 @@ var motionCapability = {
62052
62940
  methods: {
62053
62941
  /**
62054
62942
  * Pull the current motion state synchronously. Convenience shortcut
62055
- * for consumers that don't need the full status object; equivalent
62056
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62057
- * `getStatus` once the auto-injected status surface lands in every
62058
- * consumer.
62943
+ * for consumers that don't need the full status object. A composed
62944
+ * group answers from its slice (D944): unknown throws, it is not off.
62059
62945
  */
62060
62946
  isDetected: method(object({ deviceId: number() }), boolean()) },
62947
+ /**
62948
+ * A composed motion group serves `isDetected` from `detected` (D944).
62949
+ * Without this the cap stays uncomposable (`has-commands`).
62950
+ */
62951
+ composedMethods: { isDetected: {
62952
+ kind: "slice",
62953
+ field: "detected"
62954
+ } },
62061
62955
  events: {
62062
62956
  /**
62063
62957
  * Fires every time the runner transitions a camera between
@@ -62197,7 +63091,7 @@ var MotionZoneRegionSchema = object({
62197
63091
  * the grid region(s). */
62198
63092
  var MotionZoneStatusSchema = object({
62199
63093
  enabled: boolean(),
62200
- sensitivity: number(),
63094
+ sensitivity: number().meta(readingMeta("none")),
62201
63095
  /** Grid region(s). Today exactly one `grid` shape. */
62202
63096
  regions: array(MotionZoneRegionSchema),
62203
63097
  lastFetchedAt: number()
@@ -62567,7 +63461,7 @@ var NavigationStatusSchema = object({
62567
63461
  following: boolean(),
62568
63462
  flash: boolean(),
62569
63463
  lightMode: NavigationLightModeSchema,
62570
- lightLevel: number().min(40).max(100),
63464
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62571
63465
  /** Ms epoch when the slice was last updated. */
62572
63466
  lastChangedAt: number()
62573
63467
  });
@@ -62701,13 +63595,13 @@ var NetworkLinkStatusSchema = object({
62701
63595
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62702
63596
  * SKIP a null rather than coerce it.
62703
63597
  */
62704
- signalPercent: number().min(0).max(100).nullable(),
63598
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62705
63599
  /** Raw received signal strength in dBm, when the firmware reports one. */
62706
- rssiDbm: number().optional(),
63600
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62707
63601
  /** Network name of a wireless link, when the firmware reports it. */
62708
63602
  ssid: string().optional(),
62709
63603
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62710
- lastUpdated: number()
63604
+ lastUpdated: number().meta(readingMeta("none"))
62711
63605
  });
62712
63606
  var networkLinkCapability = {
62713
63607
  name: "network-link",
@@ -63092,7 +63986,7 @@ var NotifierStatusSchema = object({
63092
63986
  lastError: string().nullable(),
63093
63987
  /** Number of deliveries currently buffered server-side (rate-limited
63094
63988
  * notifiers may queue). 0 when the provider sends synchronously. */
63095
- queueDepth: number().int().nonnegative(),
63989
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63096
63990
  /** Per-feature capability matrix — authoritative source for the
63097
63991
  * compose-form field gating. */
63098
63992
  supports: NotifierSupportsSchema
@@ -63184,11 +64078,9 @@ onSent: { data: object({
63184
64078
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63185
64079
  * for: water flow, distance, weight, frequency, signal strength, …).
63186
64080
  *
63187
- * The `unit` and `precision` fields carry the live display metadata that
63188
- * the upstream source provides with each state push (HA
63189
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63190
- * The UI reads them directly from the slice — they are the single source
63191
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
64081
+ * The quantity is not on this slice. It is chosen when the device is
64082
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
64083
+ * that quantity's base unit. `null` is a refused sample (D393).
63192
64084
  *
63193
64085
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63194
64086
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63197,15 +64089,8 @@ onSent: { data: object({
63197
64089
  * numeric values land here.
63198
64090
  */
63199
64091
  var NumericSensorStatusSchema = object({
63200
- value: number(),
63201
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63202
- * Populated live from the upstream source on each state push.
63203
- * Absent for unitless sensors. */
63204
- unit: string().optional(),
63205
- /** Suggested decimal places for numeric display.
63206
- * Populated live from the upstream source when provided.
63207
- * Falls back to auto-formatting when absent. */
63208
- precision: number().int().min(0).max(10).optional(),
64092
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
64093
+ value: number().nullable().meta(readingMeta("device")),
63209
64094
  /** Ms epoch when the slice was last updated. */
63210
64095
  lastFetchedAt: number()
63211
64096
  });
@@ -63229,11 +64114,11 @@ var numericSensorCapability = {
63229
64114
  */
63230
64115
  durability: "restored",
63231
64116
  /**
63232
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
64117
+ * History (D850): recorded series. The unit is the device quantity's base,
64118
+ * resolved when the series is read — the cap has no single unit.
63233
64119
  */
63234
64120
  history: { fields: { value: {
63235
64121
  kind: "gauge",
63236
- unitFrom: "unit",
63237
64122
  minIntervalMs: 3e4
63238
64123
  } } },
63239
64124
  /** Clock fields: written, but excluded from the compare that decides
@@ -63571,23 +64456,26 @@ var PetFeederStatusSchema = object({
63571
64456
  /** Food currently in the bowl (grams). Null when the device has not
63572
64457
  * reported a reading yet. On dual-hopper models this is the combined
63573
64458
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63574
- foodLevel: number().nullable(),
64459
+ foodLevel: number().nullable().meta(readingMeta("none")),
63575
64460
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63576
64461
  * single-hopper models. */
63577
- food1: number().nullable(),
64462
+ food1: number().nullable().meta(readingMeta("none")),
63578
64463
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63579
64464
  * single-hopper models. */
63580
- food2: number().nullable(),
64465
+ food2: number().nullable().meta(readingMeta("none")),
63581
64466
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63582
64467
  * (`device_class: problem`, on = low). True when the bowl is empty /
63583
64468
  * below the feeder's low threshold. */
63584
64469
  lowFood: boolean(),
63585
64470
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63586
64471
  * device has no battery reading. */
63587
- batteryPower: number().min(0).max(100).nullable(),
63588
- /** Days of desiccant life remaining. Null when the model has no
63589
- * desiccant sensor. */
63590
- desiccantLeftDays: number().nullable(),
64472
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64473
+ /**
64474
+ * Desiccant life remaining, stored in seconds (duration base, D944).
64475
+ * Providers convert from the days the feeder reports. Null when the
64476
+ * model has no desiccant sensor.
64477
+ */
64478
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63591
64479
  /** True while a feed is in progress. */
63592
64480
  feeding: boolean(),
63593
64481
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63598,7 +64486,7 @@ var PetFeederStatusSchema = object({
63598
64486
  * with `errorCode` for consumers that want the raw integer. */
63599
64487
  error: string().nullable(),
63600
64488
  /** Raw device error code (0 / null = no error). */
63601
- errorCode: number().nullable(),
64489
+ errorCode: number().nullable().meta(readingMeta("none")),
63602
64490
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63603
64491
  isDualHopper: boolean(),
63604
64492
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63608,7 +64496,7 @@ var PetFeederStatusSchema = object({
63608
64496
  /** Play a chime when dispensing. */
63609
64497
  feedSound: boolean(),
63610
64498
  /** Speaker / prompt volume level (device-scaled integer). */
63611
- volume: number(),
64499
+ volume: number().meta(readingMeta("none")),
63612
64500
  /** Ms epoch when the slice was last refreshed from the cloud. */
63613
64501
  lastFetchedAt: number()
63614
64502
  });
@@ -63727,7 +64615,7 @@ var petFeederCapability = {
63727
64615
  },
63728
64616
  desiccantLeftDays: {
63729
64617
  kind: "gauge",
63730
- unit: "days"
64618
+ unit: "s"
63731
64619
  }
63732
64620
  } }
63733
64621
  };
@@ -64128,43 +65016,88 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64128
65016
  */
64129
65017
  var PowerMeterStatusSchema = object({
64130
65018
  /** Instantaneous power draw in watts. */
64131
- watts: number().optional().meta({
64132
- unit: "W",
64133
- label: "Power"
64134
- }),
65019
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64135
65020
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64136
- kwhTotal: number().optional().meta({
64137
- unit: "kWh",
64138
- label: "Energy"
64139
- }),
65021
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64140
65022
  /** Voltage in volts. */
64141
- volts: number().optional(),
65023
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
64142
65024
  /** Current in amperes. */
64143
- amps: number().optional(),
64144
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64145
- va: number().optional(),
65025
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
65026
+ /** Apparent power in volt-amperes. */
65027
+ va: number().optional().meta(readingMeta("apparent-power")),
64146
65028
  /** Power factor (0..1) if reported. */
64147
- powerFactor: number().min(0).max(1).optional(),
65029
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64148
65030
  /** Ms epoch when the slice was last updated. */
64149
- lastFetchedAt: number(),
64150
- /** Live display unit of the single metric this slice carries (e.g. HA
64151
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64152
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64153
- * unit per slice is unambiguous. The UI prefers this over the role's
64154
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64155
- unit: string().optional(),
64156
- /** Suggested decimal places for numeric display.
64157
- * Populated live from the upstream source when provided (e.g. HA
64158
- * `attributes.suggested_display_precision`). Falls back to
64159
- * auto-formatting when absent. */
64160
- precision: number().int().min(0).max(10).optional()
65031
+ lastFetchedAt: number()
64161
65032
  });
65033
+ /**
65034
+ * Loads that draw metered mains power (D951). The reading is a feature of the
65035
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
65036
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
65037
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
65038
+ * camera and the like are left out: their draw is not what anyone meters.
65039
+ */
65040
+ var POWER_METER_DEVICE_TYPES = [
65041
+ DeviceType.Sensor,
65042
+ DeviceType.Switch,
65043
+ DeviceType.Light,
65044
+ DeviceType.Fan,
65045
+ DeviceType.Climate,
65046
+ DeviceType.Thermostat,
65047
+ DeviceType.Humidifier,
65048
+ DeviceType.WaterHeater,
65049
+ DeviceType.MediaPlayer,
65050
+ DeviceType.Washer,
65051
+ DeviceType.Dryer,
65052
+ DeviceType.Dishwasher,
65053
+ DeviceType.Vacuum,
65054
+ DeviceType.LawnMower,
65055
+ DeviceType.PetFeeder
65056
+ ];
65057
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
65058
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
65059
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
65060
+ function powerTarget(id, unit) {
65061
+ return {
65062
+ id,
65063
+ cardinality: "scalar",
65064
+ vocabulary: {
65065
+ cap: "power-meter",
65066
+ stateField: id
65067
+ },
65068
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
65069
+ apply: "add",
65070
+ path: id,
65071
+ sources: [{
65072
+ kind: "numeric",
65073
+ requiredUnit: unit,
65074
+ transforms: [{ kind: "identity" }]
65075
+ }],
65076
+ companions: [{
65077
+ kind: "clock",
65078
+ path: "lastFetchedAt",
65079
+ when: "always"
65080
+ }],
65081
+ sourceReport: { status: "assignment-status" },
65082
+ advancedExpression: true
65083
+ };
65084
+ }
65085
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
65086
+ var powerMeterCustomize = {
65087
+ version: 1,
65088
+ targets: [
65089
+ powerTarget("watts", "W"),
65090
+ powerTarget("kwhTotal", "kWh"),
65091
+ powerTarget("volts", "V"),
65092
+ powerTarget("amps", "A")
65093
+ ]
65094
+ };
64162
65095
  var powerMeterCapability = {
64163
65096
  name: "power-meter",
64164
65097
  scope: "device",
64165
65098
  deviceNative: true,
64166
65099
  mode: "singleton",
64167
- deviceTypes: [DeviceType.Sensor],
65100
+ deviceTypes: POWER_METER_DEVICE_TYPES,
64168
65101
  methods: {},
64169
65102
  status: {
64170
65103
  schema: PowerMeterStatusSchema,
@@ -64222,11 +65155,7 @@ var powerMeterCapability = {
64222
65155
  /** Clock fields: written, but excluded from the compare that decides
64223
65156
  * whether persisting is worth a SQLite commit. */
64224
65157
  volatileStateFields: ["lastFetchedAt"],
64225
- customize: {
64226
- version: 1,
64227
- advanced: "derive",
64228
- targets: []
64229
- }
65158
+ customize: powerMeterCustomize
64230
65159
  };
64231
65160
  /**
64232
65161
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64246,11 +65175,11 @@ var powerMeterCapability = {
64246
65175
  */
64247
65176
  var GpsLocationSchema = object({
64248
65177
  /** Latitude in decimal degrees, -90..90. */
64249
- latitude: number().min(-90).max(90),
65178
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64250
65179
  /** Longitude in decimal degrees, -180..180. */
64251
- longitude: number().min(-180).max(180),
65180
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64252
65181
  /** Reported accuracy in meters (lower = better). */
64253
- accuracyMeters: number().nonnegative()
65182
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64254
65183
  });
64255
65184
  var PresenceStatusSchema = object({
64256
65185
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64262,7 +65191,7 @@ var PresenceStatusSchema = object({
64262
65191
  gps: GpsLocationSchema.nullable(),
64263
65192
  /** Optional battery level of the tracking device (0..100). Null
64264
65193
  * when not reported. */
64265
- batteryPercent: number().min(0).max(100).nullable(),
65194
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64266
65195
  /** Ms epoch when the slice was last updated. */
64267
65196
  lastChangedAt: number()
64268
65197
  });
@@ -64312,19 +65241,10 @@ var presenceCapability = {
64312
65241
  * `sourceInfo`.
64313
65242
  */
64314
65243
  var PressureSensorStatusSchema = object({
64315
- /** Current pressure in hPa. */
64316
- hpa: number(),
65244
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
65245
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64317
65246
  /** Ms epoch when the slice was last updated. */
64318
- lastFetchedAt: number(),
64319
- /** Live display unit from the upstream source (e.g. HA
64320
- * `attributes.unit_of_measurement`). The UI prefers this over the
64321
- * role's canonical unit. Absent → fall back to the canonical unit. */
64322
- unit: string().optional(),
64323
- /** Suggested decimal places for numeric display.
64324
- * Populated live from the upstream source when provided (e.g. HA
64325
- * `attributes.suggested_display_precision`). Falls back to
64326
- * auto-formatting when absent. */
64327
- precision: number().int().min(0).max(10).optional()
65247
+ lastFetchedAt: number()
64328
65248
  });
64329
65249
  var pressureSensorCapability = {
64330
65250
  name: "pressure-sensor",
@@ -64850,8 +65770,8 @@ var ptzCapability = {
64850
65770
  };
64851
65771
  var PtzAutotrackSettingsSchema = object({
64852
65772
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64853
- stopDelaySeconds: number().int().min(0).max(300),
64854
- disappearDelaySeconds: number().int().min(0).max(300)
65773
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65774
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64855
65775
  });
64856
65776
  /**
64857
65777
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65005,7 +65925,7 @@ var RunningStateStatusSchema = object({
65005
65925
  /** null = unknown — never coerced to false (D393). */
65006
65926
  running: boolean().nullable(),
65007
65927
  /** Epoch ms the current boolean began; null while `running` is null. */
65008
- since: number().nullable(),
65928
+ since: number().nullable().meta(readingMeta("none")),
65009
65929
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65010
65930
  phase: string().nullable(),
65011
65931
  /** null until a reading has been published (D393). */
@@ -65020,7 +65940,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65020
65940
  /** Generic Customize target for the running boolean (D936). */
65021
65941
  var runningCustomize = {
65022
65942
  version: 1,
65023
- advanced: "derive",
65024
65943
  targets: [{
65025
65944
  id: "running",
65026
65945
  cardinality: "scalar",
@@ -65029,6 +65948,8 @@ var runningCustomize = {
65029
65948
  stateField: "running"
65030
65949
  },
65031
65950
  deviceTypes: [
65951
+ DeviceType.Washer,
65952
+ DeviceType.Dryer,
65032
65953
  DeviceType.Dishwasher,
65033
65954
  DeviceType.Vacuum,
65034
65955
  DeviceType.LawnMower,
@@ -65040,7 +65961,7 @@ var runningCustomize = {
65040
65961
  DeviceType.Thermostat,
65041
65962
  DeviceType.PetFeeder
65042
65963
  ],
65043
- apply: "add-or-replace",
65964
+ apply: "add",
65044
65965
  path: "running",
65045
65966
  sources: [
65046
65967
  {
@@ -66308,9 +67229,9 @@ var RecordTriggerSchema = _enum([
66308
67229
  */
66309
67230
  var RecordWindowSchema = object({
66310
67231
  trigger: RecordTriggerSchema,
66311
- day: number().int().min(0).max(6),
66312
- startMinute: number().int().min(0).max(1439),
66313
- endMinute: number().int().min(1).max(1440)
67232
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
67233
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
67234
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
66314
67235
  });
66315
67236
  /** Status of one physical volume, as the camera itself describes it. */
66316
67237
  var OnboardStorageVolumeSchema = object({
@@ -66386,10 +67307,10 @@ var RecordingOnboardStatusSchema = object({
66386
67307
  /** Master "record to the card at all" switch. */
66387
67308
  enabled: boolean().nullable(),
66388
67309
  overwriteWhenFull: boolean().nullable(),
66389
- preRecordSec: number().nullable(),
66390
- postRecordSec: number().nullable(),
67310
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
67311
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66391
67312
  /** Length of one recorded file, in minutes. */
66392
- segmentMinutes: number().nullable(),
67313
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66393
67314
  /** The primary track's weekly windows, flattened. */
66394
67315
  windows: array(RecordWindowSchema),
66395
67316
  /**
@@ -66405,7 +67326,7 @@ var RecordingOnboardStatusSchema = object({
66405
67326
  * provider reporting a non-zero count also reports the schedule as not
66406
67327
  * writable.
66407
67328
  */
66408
- unreadableWindows: number(),
67329
+ unreadableWindows: number().meta(readingMeta("count")),
66409
67330
  /**
66410
67331
  * The camera is scheduled to record and has NO usable storage.
66411
67332
  *
@@ -67352,7 +68273,7 @@ var CamStreamDescriptorSchema = object({
67352
68273
  url: string().optional(),
67353
68274
  codec: string().optional(),
67354
68275
  resolution: CamStreamResolutionSchema.optional(),
67355
- fps: number().positive().optional(),
68276
+ fps: number().positive().optional().meta(readingMeta("rate")),
67356
68277
  label: string().optional(),
67357
68278
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
67358
68279
  deviceFeatures: array(string()).optional(),
@@ -67439,18 +68360,18 @@ var StreamProfileSchema = _enum([
67439
68360
  ]);
67440
68361
  /** The current encoding config of one stream profile. */
67441
68362
  var StreamProfileConfigSchema = object({
67442
- width: number(),
67443
- height: number(),
68363
+ width: number().meta(readingMeta("none")),
68364
+ height: number().meta(readingMeta("none")),
67444
68365
  codec: _enum(["h264", "h265"]),
67445
- framerate: number(),
67446
- bitrate: number(),
68366
+ framerate: number().meta(readingMeta("rate")),
68367
+ bitrate: number().meta(readingMeta("bitrate")),
67447
68368
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
67448
68369
  encoderProfile: _enum([
67449
68370
  "high",
67450
68371
  "main",
67451
68372
  "baseline"
67452
68373
  ]).optional(),
67453
- gop: number().optional(),
68374
+ gop: number().optional().meta(readingMeta("none")),
67454
68375
  /**
67455
68376
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
67456
68377
  *
@@ -67822,7 +68743,8 @@ var switchCapability = {
67822
68743
  deviceTypes: [
67823
68744
  DeviceType.Switch,
67824
68745
  DeviceType.Siren,
67825
- DeviceType.Light
68746
+ DeviceType.Light,
68747
+ DeviceType.Fan
67826
68748
  ],
67827
68749
  methods: { setState: method(object({
67828
68750
  deviceId: number(),
@@ -67869,12 +68791,7 @@ var switchCapability = {
67869
68791
  label: "On",
67870
68792
  kind: "boolean"
67871
68793
  }
67872
- }] },
67873
- customize: {
67874
- version: 1,
67875
- advanced: "derive",
67876
- targets: []
67877
- }
68794
+ }] }
67878
68795
  };
67879
68796
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67880
68797
  function hasControlCharacter(value) {
@@ -68457,35 +69374,21 @@ var tamperCapability = {
68457
69374
  * entries with `device_class: temperature` and any future native
68458
69375
  * thermometer.
68459
69376
  *
68460
- * Unit normalisation: providers convert to Celsius before storing.
68461
- * The slice value is always Celsius so cross-cap aggregators
68462
- * (climate-control's `currentTemp`, energy analytics) can compose
68463
- * without per-source unit fixups. The canonical display unit (`°C`) is
68464
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68465
- * `sourceInfo`.
69377
+ * The number is Celsius (D944). Providers convert on ingest. A failed
69378
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
68466
69379
  *
68467
69380
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
68468
69381
  * frozen feed (provider hung) distinct from a "temperature hasn't
68469
69382
  * changed" steady state.
68470
69383
  */
68471
69384
  var TemperatureSensorStatusSchema = object({
68472
- /** Current temperature in Celsius. */
68473
- celsius: number().meta({
68474
- unit: "°C",
69385
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
69386
+ celsius: number().nullable().meta(readingMeta("temperature", {
69387
+ precision: 1,
68475
69388
  label: "Temperature"
68476
- }),
69389
+ })),
68477
69390
  /** Ms epoch when the slice was last updated (push or poll). */
68478
- lastFetchedAt: number(),
68479
- /** Live display unit from the upstream source (e.g. HA
68480
- * `attributes.unit_of_measurement`). The UI prefers this over the
68481
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
68482
- * Absent → the UI falls back to the role's canonical unit. */
68483
- unit: string().optional(),
68484
- /** Suggested decimal places for numeric display.
68485
- * Populated live from the upstream source when provided (e.g. HA
68486
- * `attributes.suggested_display_precision`). Falls back to
68487
- * auto-formatting when absent. */
68488
- precision: number().int().min(0).max(10).optional()
69391
+ lastFetchedAt: number()
68489
69392
  });
68490
69393
  var temperatureSensorCapability = {
68491
69394
  name: "temperature-sensor",
@@ -68516,12 +69419,7 @@ var temperatureSensorCapability = {
68516
69419
  } } },
68517
69420
  /** Clock fields: written, but excluded from the compare that decides
68518
69421
  * whether persisting is worth a SQLite commit. */
68519
- volatileStateFields: ["lastFetchedAt"],
68520
- customize: {
68521
- version: 1,
68522
- advanced: "derive",
68523
- targets: []
68524
- }
69422
+ volatileStateFields: ["lastFetchedAt"]
68525
69423
  };
68526
69424
  /**
68527
69425
  * toast — system-scoped singleton capability that streams toast
@@ -68804,16 +69702,16 @@ var VacuumControlStatusSchema = object({
68804
69702
  ]),
68805
69703
  /** 0..100 battery percentage. Null when the device has no battery
68806
69704
  * reading. */
68807
- batteryLevel: number().min(0).max(100).nullable(),
69705
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68808
69706
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68809
69707
  * the vacuum has no speed control. */
68810
69708
  fanSpeed: string().nullable(),
68811
69709
  /** Speed tokens the hardware accepts — drives the UI selector. */
68812
69710
  availableFanSpeeds: array(string()),
68813
69711
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68814
- progressPercent: number().min(0).max(100).nullable(),
69712
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68815
69713
  /** Current error code (0 / null = no error). */
68816
- errorCode: number().nullable(),
69714
+ errorCode: number().nullable().meta(readingMeta("none")),
68817
69715
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68818
69716
  errorLabel: string().nullable(),
68819
69717
  /** Ms epoch when the slice was last updated. */
@@ -68891,7 +69789,7 @@ var ValveStatusSchema = object({
68891
69789
  ]),
68892
69790
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68893
69791
  * intermediate position surface. */
68894
- position: number().min(0).max(100).nullable(),
69792
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68895
69793
  /** Ms epoch when the slice was last updated. */
68896
69794
  lastChangedAt: number()
68897
69795
  });
@@ -68999,9 +69897,9 @@ var vibrationCapability = {
68999
69897
  */
69000
69898
  var WaterHeaterStatusSchema = object({
69001
69899
  /** Current measured temperature. Null when not reported. */
69002
- currentTemp: number().nullable(),
69900
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
69003
69901
  /** Target temperature setpoint. Null when no setpoint surface. */
69004
- targetTemp: number().nullable(),
69902
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
69005
69903
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
69006
69904
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
69007
69905
  * device reports an unknown state. */
@@ -69012,9 +69910,9 @@ var WaterHeaterStatusSchema = object({
69012
69910
  * has no away surface. */
69013
69911
  away: boolean().nullable(),
69014
69912
  /** HA `min_temp` attribute. Null when not reported. */
69015
- minTemp: number().nullable(),
69913
+ minTemp: number().nullable().meta(readingMeta("temperature")),
69016
69914
  /** HA `max_temp` attribute. Null when not reported. */
69017
- maxTemp: number().nullable(),
69915
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
69018
69916
  /** Ms epoch when the slice was last updated. */
69019
69917
  lastChangedAt: number()
69020
69918
  });
@@ -69099,25 +69997,18 @@ var waterHeaterCapability = {
69099
69997
  * Forecast deferred — current conditions only for v1.
69100
69998
  */
69101
69999
  var WeatherStatusSchema = object({
69102
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69103
- * when no condition has been reported yet. */
70000
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69104
70001
  condition: string().nullable(),
69105
- /** Current temperature in the reported unit. Null when not provided. */
69106
- temperature: number().nullable(),
69107
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69108
- temperatureUnit: string().nullable(),
70002
+ /** Current temperature in Celsius. Null when not provided or refused. */
70003
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69109
70004
  /** Relative humidity (0..100). Null when not provided. */
69110
- humidity: number().min(0).max(100).nullable(),
69111
- /** Barometric pressure in the reported unit. Null when not provided. */
69112
- pressure: number().nullable(),
69113
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69114
- pressureUnit: string().nullable(),
69115
- /** Wind speed in the reported unit. Null when not provided. */
69116
- windSpeed: number().nullable(),
69117
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69118
- windSpeedUnit: string().nullable(),
70005
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
70006
+ /** Barometric pressure in hPa. Null when not provided or refused. */
70007
+ pressure: number().nullable().meta(readingMeta("pressure")),
70008
+ /** Wind speed in metres per second. Null when not provided or refused. */
70009
+ windSpeed: number().nullable().meta(readingMeta("speed")),
69119
70010
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69120
- windBearing: number().nullable(),
70011
+ windBearing: number().nullable().meta(readingMeta("angle")),
69121
70012
  /** Ms epoch when the slice was last updated. */
69122
70013
  lastFetchedAt: number()
69123
70014
  });
@@ -69145,12 +70036,12 @@ var weatherCapability = {
69145
70036
  */
69146
70037
  durability: "session",
69147
70038
  /**
69148
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
70039
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69149
70040
  */
69150
70041
  history: { fields: {
69151
70042
  temperature: {
69152
70043
  kind: "gauge",
69153
- unitFrom: "temperatureUnit",
70044
+ unit: "°C",
69154
70045
  deadband: .2
69155
70046
  },
69156
70047
  humidity: {
@@ -69160,12 +70051,12 @@ var weatherCapability = {
69160
70051
  },
69161
70052
  pressure: {
69162
70053
  kind: "gauge",
69163
- unitFrom: "pressureUnit",
70054
+ unit: "hPa",
69164
70055
  deadband: .5
69165
70056
  },
69166
70057
  windSpeed: {
69167
70058
  kind: "gauge",
69168
- unitFrom: "windSpeedUnit",
70059
+ unit: "m/s",
69169
70060
  deadband: .5
69170
70061
  }
69171
70062
  } }
@@ -70199,8 +71090,9 @@ function withClipEventLabels(provider, deps) {
70199
71090
  listClips
70200
71091
  };
70201
71092
  }
71093
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
70202
71094
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
70203
- 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;
71095
+ 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;
70204
71096
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
70205
71097
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
70206
71098
  new Set(CAP_NAMES);
@@ -70529,6 +71421,9 @@ var DEVICE_LOCAL_STATE_CAPS = {
70529
71421
  featureProbe: featureProbeCapability,
70530
71422
  flood: floodCapability,
70531
71423
  gas: gasCapability,
71424
+ groupActive: groupActiveCapability,
71425
+ groupCommands: groupCommandsCapability,
71426
+ groupStatus: groupStatusCapability,
70532
71427
  heatingValve: heatingValveCapability,
70533
71428
  humidifier: humidifierCapability,
70534
71429
  humiditySensor: humiditySensorCapability,
@@ -70538,6 +71433,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
70538
71433
  lawnMowerControl: lawnMowerControlCapability,
70539
71434
  lockControl: lockControlCapability,
70540
71435
  mediaPlayer: mediaPlayerCapability,
71436
+ meter: meterCapability,
70541
71437
  motion: motionCapability,
70542
71438
  motionTrigger: motionTriggerCapability,
70543
71439
  motionZones: motionZonesCapability,
@@ -71801,7 +72697,7 @@ var BaseDeviceProvider = class extends BaseAddon {
71801
72697
  function widenDeviceConstructor(ctor) {
71802
72698
  return ctor;
71803
72699
  }
71804
- 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;
72700
+ 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;
71805
72701
  new Set(Object.values(DeviceType));
71806
72702
  Object.freeze({
71807
72703
  "accessories.getStatus": {
@@ -71909,6 +72805,13 @@ Object.freeze({
71909
72805
  access: "view",
71910
72806
  role: "admin"
71911
72807
  },
72808
+ "addons.getPackageDocs": {
72809
+ capName: "addons",
72810
+ capScope: "system",
72811
+ addonId: null,
72812
+ access: "view",
72813
+ role: "admin"
72814
+ },
71912
72815
  "addons.getVersions": {
71913
72816
  capName: "addons",
71914
72817
  capScope: "system",
@@ -75017,6 +75920,27 @@ Object.freeze({
75017
75920
  access: "view",
75018
75921
  role: "viewer"
75019
75922
  },
75923
+ "groupActive.getStatus": {
75924
+ capName: "group-active",
75925
+ capScope: "device",
75926
+ addonId: null,
75927
+ access: "view",
75928
+ role: "viewer"
75929
+ },
75930
+ "groupCommands.getStatus": {
75931
+ capName: "group-commands",
75932
+ capScope: "device",
75933
+ addonId: null,
75934
+ access: "view",
75935
+ role: "viewer"
75936
+ },
75937
+ "groupStatus.getStatus": {
75938
+ capName: "group-status",
75939
+ capScope: "device",
75940
+ addonId: null,
75941
+ access: "view",
75942
+ role: "viewer"
75943
+ },
75020
75944
  "heatingValve.getStatus": {
75021
75945
  capName: "heating-valve",
75022
75946
  capScope: "device",
@@ -75969,6 +76893,13 @@ Object.freeze({
75969
76893
  access: "create",
75970
76894
  role: "admin"
75971
76895
  },
76896
+ "meter.getStatus": {
76897
+ capName: "meter",
76898
+ capScope: "device",
76899
+ addonId: null,
76900
+ access: "view",
76901
+ role: "viewer"
76902
+ },
75972
76903
  "metricsProvider.collectSnapshot": {
75973
76904
  capName: "metrics-provider",
75974
76905
  capScope: "system",
@@ -84067,7 +84998,39 @@ var APPLIANCE_CAP_VOCABULARY = {
84067
84998
  phrase: t("When {device} finishes", "Quando {device} finisce"),
84068
84999
  icon: "square"
84069
85000
  }
84070
- }
85001
+ },
85002
+ overrides: { byType: {
85003
+ [DeviceType.Washer]: {
85004
+ state: { running: {
85005
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
85006
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
85007
+ } },
85008
+ events: {
85009
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
85010
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
85011
+ }
85012
+ },
85013
+ [DeviceType.Dryer]: {
85014
+ state: { running: {
85015
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
85016
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
85017
+ } },
85018
+ events: {
85019
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
85020
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
85021
+ }
85022
+ },
85023
+ [DeviceType.Dishwasher]: {
85024
+ state: { running: {
85025
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
85026
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
85027
+ } },
85028
+ events: {
85029
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
85030
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
85031
+ }
85032
+ }
85033
+ } }
84071
85034
  },
84072
85035
  update: {
84073
85036
  scope: "device",
@@ -84839,6 +85802,41 @@ var SENSOR_CAP_VOCABULARY = {
84839
85802
  icon: "flame"
84840
85803
  }]
84841
85804
  },
85805
+ "group-active": {
85806
+ scope: "device",
85807
+ noun: t("Group state", "Stato del gruppo")
85808
+ },
85809
+ "group-commands": {
85810
+ scope: "device",
85811
+ noun: t("Group command", "Comando di gruppo")
85812
+ },
85813
+ "group-status": {
85814
+ scope: "device",
85815
+ noun: t("Group", "Gruppo"),
85816
+ state: [
85817
+ {
85818
+ kind: "numeric",
85819
+ field: "included",
85820
+ quantity: t("the members reporting", "i membri che rispondono"),
85821
+ label: t("Reporting", "Rispondono"),
85822
+ icon: "users"
85823
+ },
85824
+ {
85825
+ kind: "numeric",
85826
+ field: "excluded",
85827
+ quantity: t("the members left out", "i membri esclusi"),
85828
+ label: t("Excluded", "Esclusi"),
85829
+ icon: "users"
85830
+ },
85831
+ {
85832
+ kind: "numeric",
85833
+ field: "total",
85834
+ quantity: t("the member count", "il numero di membri"),
85835
+ label: t("Members", "Membri"),
85836
+ icon: "sigma"
85837
+ }
85838
+ ]
85839
+ },
84842
85840
  "humidity-sensor": {
84843
85841
  scope: "device",
84844
85842
  noun: t("Humidity", "Umidità"),
@@ -84864,6 +85862,17 @@ var SENSOR_CAP_VOCABULARY = {
84864
85862
  }],
84865
85863
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
84866
85864
  },
85865
+ meter: {
85866
+ scope: "device",
85867
+ noun: t("Meter", "Contatore"),
85868
+ state: [{
85869
+ kind: "numeric",
85870
+ field: "value",
85871
+ quantity: t("the total", "il totale"),
85872
+ label: t("Total", "Totale"),
85873
+ icon: "gauge"
85874
+ }]
85875
+ },
84867
85876
  "numeric-sensor": {
84868
85877
  scope: "device",
84869
85878
  noun: t("Sensor", "Sensore"),
@@ -84872,7 +85881,6 @@ var SENSOR_CAP_VOCABULARY = {
84872
85881
  field: "value",
84873
85882
  quantity: t("the reading", "il valore"),
84874
85883
  label: t("Reading", "Valore"),
84875
- unitField: "unit",
84876
85884
  icon: "gauge"
84877
85885
  }]
84878
85886
  },
@@ -85008,7 +86016,7 @@ var SENSOR_CAP_VOCABULARY = {
85008
86016
  field: "temperature",
85009
86017
  quantity: t("the outdoor temperature", "la temperatura esterna"),
85010
86018
  label: t("Temperature", "Temperatura"),
85011
- unitField: "temperatureUnit",
86019
+ unit: "°C",
85012
86020
  icon: "thermometer"
85013
86021
  },
85014
86022
  {
@@ -85024,7 +86032,7 @@ var SENSOR_CAP_VOCABULARY = {
85024
86032
  field: "windSpeed",
85025
86033
  quantity: t("the wind speed", "la velocità del vento"),
85026
86034
  label: t("Wind", "Vento"),
85027
- unitField: "windSpeedUnit",
86035
+ unit: "m/s",
85028
86036
  icon: "wind"
85029
86037
  },
85030
86038
  {
@@ -85032,7 +86040,7 @@ var SENSOR_CAP_VOCABULARY = {
85032
86040
  field: "pressure",
85033
86041
  quantity: t("the air pressure", "la pressione atmosferica"),
85034
86042
  label: t("Pressure", "Pressione"),
85035
- unitField: "pressureUnit",
86043
+ unit: "hPa",
85036
86044
  icon: "gauge"
85037
86045
  }
85038
86046
  ]
@@ -85337,8 +86345,47 @@ function projectAction(cap, action) {
85337
86345
  };
85338
86346
  }
85339
86347
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
85340
- 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;
86348
+ 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;
85341
86349
  DeviceFeature.BatteryOperated;
86350
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
86351
+ 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.";
86352
+ 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.";
86353
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
86354
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
86355
+ 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.";
86356
+ function withheld(reason) {
86357
+ return {
86358
+ [DeviceType.Washer]: {
86359
+ exported: false,
86360
+ reason
86361
+ },
86362
+ [DeviceType.Dryer]: {
86363
+ exported: false,
86364
+ reason
86365
+ },
86366
+ [DeviceType.Dishwasher]: {
86367
+ exported: false,
86368
+ reason
86369
+ }
86370
+ };
86371
+ }
86372
+ function offered(received) {
86373
+ return {
86374
+ [DeviceType.Washer]: {
86375
+ exported: true,
86376
+ as: received
86377
+ },
86378
+ [DeviceType.Dryer]: {
86379
+ exported: true,
86380
+ as: received
86381
+ },
86382
+ [DeviceType.Dishwasher]: {
86383
+ exported: true,
86384
+ as: received
86385
+ }
86386
+ };
86387
+ }
86388
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
85342
86389
  /** Reject after `ms`; always clears its own timer. */
85343
86390
  async function withTimeout$1(promise, ms, label) {
85344
86391
  let timer;
@@ -86511,6 +87558,21 @@ Object.freeze({
86511
87558
  form: "single",
86512
87559
  optional: false
86513
87560
  }],
87561
+ "groupActive.getStatus": [{
87562
+ name: "deviceId",
87563
+ form: "single",
87564
+ optional: false
87565
+ }],
87566
+ "groupCommands.getStatus": [{
87567
+ name: "deviceId",
87568
+ form: "single",
87569
+ optional: false
87570
+ }],
87571
+ "groupStatus.getStatus": [{
87572
+ name: "deviceId",
87573
+ form: "single",
87574
+ optional: false
87575
+ }],
86514
87576
  "heatingValve.getStatus": [{
86515
87577
  name: "deviceId",
86516
87578
  form: "single",
@@ -86711,6 +87773,11 @@ Object.freeze({
86711
87773
  form: "single",
86712
87774
  optional: false
86713
87775
  }],
87776
+ "meter.getStatus": [{
87777
+ name: "deviceId",
87778
+ form: "single",
87779
+ optional: false
87780
+ }],
86714
87781
  "motion.getStatus": [{
86715
87782
  name: "deviceId",
86716
87783
  form: "single",
@@ -88318,7 +89385,6 @@ Object.freeze({
88318
89385
  "network-access": "ingress",
88319
89386
  "smtp-provider": "email"
88320
89387
  });
88321
- DeviceType.Camera;
88322
89388
  /**
88323
89389
  * A saved Customize assignment (D936). The schema is the type: callers use
88324
89390
  * `z.infer`, they do not restate the shape.
@@ -88495,6 +89561,8 @@ new Map(Object.entries({
88495
89561
  "detection-blind": "device-events",
88496
89562
  "motion-noisy": "device-events",
88497
89563
  "motion-calm": "device-events",
89564
+ "package-llm-silent": "device-events",
89565
+ "package-llm-answered": "device-events",
88498
89566
  "recording-stalled": "device-events",
88499
89567
  "recording-healthy": "device-events",
88500
89568
  "recording-degraded": "device-events",
@@ -88578,6 +89646,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
88578
89646
  label: "Motion without a subject",
88579
89647
  kinds: ["motion-noisy", "motion-calm"]
88580
89648
  },
89649
+ {
89650
+ id: "package-llm",
89651
+ family: "device-events",
89652
+ label: "Package confirm",
89653
+ kinds: ["package-llm-silent", "package-llm-answered"]
89654
+ },
88581
89655
  {
88582
89656
  id: "recording",
88583
89657
  family: "device-events",
@@ -88759,6 +89833,8 @@ var DEVICE_EVENTS = [
88759
89833
  "detection-blind",
88760
89834
  "motion-noisy",
88761
89835
  "motion-calm",
89836
+ "package-llm-silent",
89837
+ "package-llm-answered",
88762
89838
  "recording-stalled",
88763
89839
  "recording-healthy",
88764
89840
  "recording-degraded",
@@ -89027,6 +90103,15 @@ new Map([
89027
90103
  snapshotLabel: "Cameras with constant motion now",
89028
90104
  snapshotSection: null
89029
90105
  },
90106
+ {
90107
+ id: "package-llm-confirm",
90108
+ family: "device-events",
90109
+ label: "Package confirm (not answering / answering again)",
90110
+ bad: ["package-llm-silent"],
90111
+ recovery: ["package-llm-answered"],
90112
+ snapshotLabel: "Cameras whose package confirm is not answering",
90113
+ snapshotSection: null
90114
+ },
89030
90115
  {
89031
90116
  id: "recording-health",
89032
90117
  family: "device-events",