@camstack/addon-provider-wyze 0.2.158 → 0.2.160

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
@@ -31,7 +31,7 @@ let net = require("net");
31
31
  net = __toESM(net, 1);
32
32
  let node_child_process = require("node:child_process");
33
33
  let node_tls = require("node:tls");
34
- //#region ../types/dist/event-category-DHXxB8t9.mjs
34
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
35
35
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
36
36
  EventCategory["SystemBoot"] = "system.boot";
37
37
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -357,6 +357,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
357
357
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
358
358
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
359
359
  /**
360
+ * A camera's package LLM confirm has failed closed three times in a row
361
+ * (D931, 2026-10-26). One edge per camera until
362
+ * {@link PipelinePackageLlmAnswered}.
363
+ */
364
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
365
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
366
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
367
+ /**
360
368
  * Per-camera pipeline config was mutated by the orchestrator
361
369
  * (a settings change via `setAgentInferenceDevices` /
362
370
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5466,7 +5474,7 @@ var ZodIssueCode = {
5466
5474
  var ZodFirstPartyTypeKind;
5467
5475
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5468
5476
  //#endregion
5469
- //#region ../types/dist/sleep-BErAxhul.mjs
5477
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5470
5478
  /**
5471
5479
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5472
5480
  * window to float samples (D455).
@@ -8020,6 +8028,99 @@ var RecordKindDeclarationSchema = object({
8020
8028
  }).strict();
8021
8029
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8022
8030
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8031
+ var READING_QUANTITIES = [
8032
+ "temperature",
8033
+ "pressure",
8034
+ "speed",
8035
+ "precipitation",
8036
+ "precipitation-rate",
8037
+ "length",
8038
+ "area",
8039
+ "illuminance",
8040
+ "irradiance",
8041
+ "power",
8042
+ "energy",
8043
+ "voltage",
8044
+ "current",
8045
+ "apparent-power",
8046
+ "humidity",
8047
+ "percentage",
8048
+ "concentration-volume",
8049
+ "concentration-mass",
8050
+ "concentration-ppb",
8051
+ "volume",
8052
+ "duration",
8053
+ "duration-ms",
8054
+ "information",
8055
+ "sound-level",
8056
+ "signal-level",
8057
+ "colour-temperature",
8058
+ "angle",
8059
+ "bitrate",
8060
+ "rate",
8061
+ "mass",
8062
+ "index",
8063
+ "ratio",
8064
+ "count",
8065
+ "none",
8066
+ "device"
8067
+ ];
8068
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8069
+ new Set(READING_QUANTITIES);
8070
+ /**
8071
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8072
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8073
+ */
8074
+ var QUANTITY_BASE = {
8075
+ temperature: "°C",
8076
+ pressure: "hPa",
8077
+ speed: "m/s",
8078
+ precipitation: "mm",
8079
+ "precipitation-rate": "mm/h",
8080
+ length: "m",
8081
+ area: "m²",
8082
+ illuminance: "lx",
8083
+ irradiance: "W/m²",
8084
+ power: "W",
8085
+ energy: "kWh",
8086
+ voltage: "V",
8087
+ current: "A",
8088
+ "apparent-power": "VA",
8089
+ humidity: "%",
8090
+ percentage: "%",
8091
+ "concentration-volume": "ppm",
8092
+ "concentration-mass": "µg/m³",
8093
+ "concentration-ppb": "ppb",
8094
+ volume: "L",
8095
+ duration: "s",
8096
+ "duration-ms": "ms",
8097
+ information: "B",
8098
+ "sound-level": "dBFS",
8099
+ "signal-level": "dBm",
8100
+ "colour-temperature": "mired",
8101
+ angle: "°",
8102
+ bitrate: "kbps",
8103
+ rate: "fps",
8104
+ mass: "kg",
8105
+ index: "none",
8106
+ ratio: "none",
8107
+ count: "none",
8108
+ none: "none",
8109
+ device: "device"
8110
+ };
8111
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8112
+ function baseUnitOf(quantity) {
8113
+ const symbol = QUANTITY_BASE[quantity];
8114
+ if (symbol === "none" || symbol === "device") return void 0;
8115
+ return symbol;
8116
+ }
8117
+ function readingMeta(quantity, extras) {
8118
+ return {
8119
+ quantity,
8120
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8121
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8122
+ };
8123
+ }
8023
8124
  /** Shared Zod schemas used across streaming capabilities. */
8024
8125
  var CamProfileSchema = _enum([
8025
8126
  "high",
@@ -8047,8 +8148,8 @@ var CamStreamKindSchema = _enum([
8047
8148
  "derived"
8048
8149
  ]);
8049
8150
  var CamStreamResolutionSchema = object({
8050
- width: number().int().positive(),
8051
- height: number().int().positive()
8151
+ width: number().int().positive().meta(readingMeta("none")),
8152
+ height: number().int().positive().meta(readingMeta("none"))
8052
8153
  });
8053
8154
  var CameraStreamSchema = object({
8054
8155
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8566,6 +8667,9 @@ var CAP_NAMES = Object.freeze([
8566
8667
  "filesystem-browse",
8567
8668
  "flood",
8568
8669
  "gas",
8670
+ "group-active",
8671
+ "group-commands",
8672
+ "group-status",
8569
8673
  "heating-valve",
8570
8674
  "humidifier",
8571
8675
  "humidity-sensor",
@@ -8586,6 +8690,7 @@ var CAP_NAMES = Object.freeze([
8586
8690
  "login-method",
8587
8691
  "media-player",
8588
8692
  "mesh-network",
8693
+ "meter",
8589
8694
  "metrics-provider",
8590
8695
  "model-convert",
8591
8696
  "model-distributor",
@@ -8695,14 +8800,6 @@ Object.freeze({
8695
8800
  {
8696
8801
  "path": "aqi",
8697
8802
  "valueType": "number"
8698
- },
8699
- {
8700
- "path": "unit",
8701
- "valueType": "string"
8702
- },
8703
- {
8704
- "path": "precision",
8705
- "valueType": "number"
8706
8803
  }
8707
8804
  ],
8708
8805
  "alarm-panel": [
@@ -8738,20 +8835,10 @@ Object.freeze({
8738
8835
  "valueType": "boolean"
8739
8836
  }
8740
8837
  ],
8741
- "ambient-light-sensor": [
8742
- {
8743
- "path": "lux",
8744
- "valueType": "number"
8745
- },
8746
- {
8747
- "path": "unit",
8748
- "valueType": "string"
8749
- },
8750
- {
8751
- "path": "precision",
8752
- "valueType": "number"
8753
- }
8754
- ],
8838
+ "ambient-light-sensor": [{
8839
+ "path": "lux",
8840
+ "valueType": "number"
8841
+ }],
8755
8842
  "audio-metrics": [
8756
8843
  {
8757
8844
  "path": "ts",
@@ -8831,6 +8918,10 @@ Object.freeze({
8831
8918
  "path": "lastUpdated",
8832
8919
  "valueType": "number"
8833
8920
  },
8921
+ {
8922
+ "path": "volts",
8923
+ "valueType": "number"
8924
+ },
8834
8925
  {
8835
8926
  "path": "lastContactAt",
8836
8927
  "valueType": "number"
@@ -9073,10 +9164,6 @@ Object.freeze({
9073
9164
  "datetime"
9074
9165
  ]
9075
9166
  },
9076
- {
9077
- "path": "unit",
9078
- "valueType": "string"
9079
- },
9080
9167
  {
9081
9168
  "path": "min",
9082
9169
  "valueType": "number"
@@ -9088,19 +9175,6 @@ Object.freeze({
9088
9175
  {
9089
9176
  "path": "step",
9090
9177
  "valueType": "number"
9091
- },
9092
- {
9093
- "path": "precision",
9094
- "valueType": "number"
9095
- },
9096
- {
9097
- "path": "format",
9098
- "valueType": "enum",
9099
- "enumValues": [
9100
- "date",
9101
- "time",
9102
- "datetime"
9103
- ]
9104
9178
  }
9105
9179
  ],
9106
9180
  "cover": [
@@ -9341,13 +9415,8 @@ Object.freeze({
9341
9415
  "path": "value",
9342
9416
  "valueType": "string"
9343
9417
  }, {
9344
- "path": "format",
9345
- "valueType": "enum",
9346
- "enumValues": [
9347
- "date",
9348
- "time",
9349
- "datetime"
9350
- ]
9418
+ "path": "timestamp",
9419
+ "valueType": "boolean"
9351
9420
  }],
9352
9421
  "event-emitter": [
9353
9422
  {
@@ -9420,6 +9489,24 @@ Object.freeze({
9420
9489
  "path": "detected",
9421
9490
  "valueType": "boolean"
9422
9491
  }],
9492
+ "group-active": [{
9493
+ "path": "active",
9494
+ "valueType": "boolean"
9495
+ }],
9496
+ "group-status": [
9497
+ {
9498
+ "path": "included",
9499
+ "valueType": "number"
9500
+ },
9501
+ {
9502
+ "path": "excluded",
9503
+ "valueType": "number"
9504
+ },
9505
+ {
9506
+ "path": "total",
9507
+ "valueType": "number"
9508
+ }
9509
+ ],
9423
9510
  "heating-valve": [{
9424
9511
  "path": "position",
9425
9512
  "valueType": "number"
@@ -9454,20 +9541,10 @@ Object.freeze({
9454
9541
  "valueType": "number"
9455
9542
  }
9456
9543
  ],
9457
- "humidity-sensor": [
9458
- {
9459
- "path": "percent",
9460
- "valueType": "number"
9461
- },
9462
- {
9463
- "path": "unit",
9464
- "valueType": "string"
9465
- },
9466
- {
9467
- "path": "precision",
9468
- "valueType": "number"
9469
- }
9470
- ],
9544
+ "humidity-sensor": [{
9545
+ "path": "percent",
9546
+ "valueType": "number"
9547
+ }],
9471
9548
  "image": [{
9472
9549
  "path": "url",
9473
9550
  "valueType": "string"
@@ -9675,6 +9752,10 @@ Object.freeze({
9675
9752
  ]
9676
9753
  }
9677
9754
  ],
9755
+ "meter": [{
9756
+ "path": "value",
9757
+ "valueType": "number"
9758
+ }],
9678
9759
  "motion": [
9679
9760
  {
9680
9761
  "path": "detected",
@@ -9806,20 +9887,10 @@ Object.freeze({
9806
9887
  "valueType": "boolean"
9807
9888
  }
9808
9889
  ],
9809
- "numeric-sensor": [
9810
- {
9811
- "path": "value",
9812
- "valueType": "number"
9813
- },
9814
- {
9815
- "path": "unit",
9816
- "valueType": "string"
9817
- },
9818
- {
9819
- "path": "precision",
9820
- "valueType": "number"
9821
- }
9822
- ],
9890
+ "numeric-sensor": [{
9891
+ "path": "value",
9892
+ "valueType": "number"
9893
+ }],
9823
9894
  "pet-feeder": [
9824
9895
  {
9825
9896
  "path": "foodLevel",
@@ -9911,14 +9982,6 @@ Object.freeze({
9911
9982
  {
9912
9983
  "path": "powerFactor",
9913
9984
  "valueType": "number"
9914
- },
9915
- {
9916
- "path": "unit",
9917
- "valueType": "string"
9918
- },
9919
- {
9920
- "path": "precision",
9921
- "valueType": "number"
9922
9985
  }
9923
9986
  ],
9924
9987
  "presence": [
@@ -9947,20 +10010,10 @@ Object.freeze({
9947
10010
  "valueType": "number"
9948
10011
  }
9949
10012
  ],
9950
- "pressure-sensor": [
9951
- {
9952
- "path": "hpa",
9953
- "valueType": "number"
9954
- },
9955
- {
9956
- "path": "unit",
9957
- "valueType": "string"
9958
- },
9959
- {
9960
- "path": "precision",
9961
- "valueType": "number"
9962
- }
9963
- ],
10013
+ "pressure-sensor": [{
10014
+ "path": "hpa",
10015
+ "valueType": "number"
10016
+ }],
9964
10017
  "privacy-mask": [{
9965
10018
  "path": "enabled",
9966
10019
  "valueType": "boolean"
@@ -10271,20 +10324,10 @@ Object.freeze({
10271
10324
  "path": "tampered",
10272
10325
  "valueType": "boolean"
10273
10326
  }],
10274
- "temperature-sensor": [
10275
- {
10276
- "path": "celsius",
10277
- "valueType": "number"
10278
- },
10279
- {
10280
- "path": "unit",
10281
- "valueType": "string"
10282
- },
10283
- {
10284
- "path": "precision",
10285
- "valueType": "number"
10286
- }
10287
- ],
10327
+ "temperature-sensor": [{
10328
+ "path": "celsius",
10329
+ "valueType": "number"
10330
+ }],
10288
10331
  "update": [
10289
10332
  {
10290
10333
  "path": "currentVersion",
@@ -10395,10 +10438,6 @@ Object.freeze({
10395
10438
  "path": "temperature",
10396
10439
  "valueType": "number"
10397
10440
  },
10398
- {
10399
- "path": "temperatureUnit",
10400
- "valueType": "string"
10401
- },
10402
10441
  {
10403
10442
  "path": "humidity",
10404
10443
  "valueType": "number"
@@ -10407,18 +10446,10 @@ Object.freeze({
10407
10446
  "path": "pressure",
10408
10447
  "valueType": "number"
10409
10448
  },
10410
- {
10411
- "path": "pressureUnit",
10412
- "valueType": "string"
10413
- },
10414
10449
  {
10415
10450
  "path": "windSpeed",
10416
10451
  "valueType": "number"
10417
10452
  },
10418
- {
10419
- "path": "windSpeedUnit",
10420
- "valueType": "string"
10421
- },
10422
10453
  {
10423
10454
  "path": "windBearing",
10424
10455
  "valueType": "number"
@@ -10482,7 +10513,7 @@ function userError(code, params, englishFallback) {
10482
10513
  return new CamstackError(code, params, englishFallback);
10483
10514
  }
10484
10515
  //#endregion
10485
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10516
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10486
10517
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10487
10518
  DeviceType["Camera"] = "camera";
10488
10519
  DeviceType["Hub"] = "hub";
@@ -10576,8 +10607,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10576
10607
  DeviceType["PetFeeder"] = "pet-feeder";
10577
10608
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10578
10609
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10579
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10610
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10611
+ * A plug-built dishwasher carries running-state and power instead. */
10580
10612
  DeviceType["Dishwasher"] = "dishwasher";
10613
+ /** Washing machine. A plug-built one is running while its power stays
10614
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10615
+ DeviceType["Washer"] = "washer";
10616
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10617
+ DeviceType["Dryer"] = "dryer";
10581
10618
  return DeviceType;
10582
10619
  }({});
10583
10620
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -10835,7 +10872,7 @@ function errMsg(err) {
10835
10872
  return String(err);
10836
10873
  }
10837
10874
  //#endregion
10838
- //#region ../types/dist/composition-kEl9mevu.mjs
10875
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
10839
10876
  /**
10840
10877
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10841
10878
  *
@@ -11150,6 +11187,13 @@ var table = {
11150
11187
  apply: () => {
11151
11188
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11152
11189
  }
11190
+ },
11191
+ schmitt: {
11192
+ minArgs: 3,
11193
+ maxArgs: 5,
11194
+ apply: () => {
11195
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11196
+ }
11153
11197
  }
11154
11198
  };
11155
11199
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11590,15 +11634,232 @@ function validateExpressionSource(src) {
11590
11634
  }
11591
11635
  return null;
11592
11636
  }
11593
- /** A composed device's stableId is this prefix plus the block id. */
11594
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11595
- /** Longest snippet a `code` source may carry (slice 4). */
11596
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11637
+ /**
11638
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11639
+ *
11640
+ * Standby of the control board is under 5 W; a motor or a heater is far
11641
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11642
+ * a cycle, so running starts only after 30 s above the line.
11643
+ *
11644
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11645
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11646
+ * report "finished" in the middle of the program, so the stop dwell is
11647
+ * 10 minutes on every type. The number is stored per type so one machine
11648
+ * can move later without moving the others.
11649
+ */
11650
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11651
+ var APPLIANCE_KINDS = [
11652
+ "washer",
11653
+ "dryer",
11654
+ "dishwasher"
11655
+ ];
11656
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11657
+ /**
11658
+ * Statistics and integrals over a history read (D944).
11659
+ *
11660
+ * Pure: the same points always give the same answer, so a restart recomputes
11661
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11662
+ * never the mean of the samples. A hole with no held value is not integrated
11663
+ * as zero. A truncated read is not published.
11664
+ */
11665
+ var STATISTIC_STATS = [
11666
+ "min",
11667
+ "max",
11668
+ "mean",
11669
+ "sum",
11670
+ "count",
11671
+ "change"
11672
+ ];
11673
+ /**
11674
+ * The range a statistics or integral helper asks history for (D944).
11675
+ *
11676
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11677
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11678
+ * is strictly longer than the longest that range can be. Forever qualifies.
11679
+ * Equal to a tier's retention is a refusal, not a short answer.
11680
+ */
11681
+ var MINUTE_MS = 6e4;
11682
+ var HOUR_MS = 60 * MINUTE_MS;
11683
+ var DAY_MS = 24 * HOUR_MS;
11684
+ var ROLLING_SPANS = [
11685
+ "1h",
11686
+ "24h",
11687
+ "2d",
11688
+ "7d",
11689
+ "30d",
11690
+ "custom"
11691
+ ];
11692
+ var CALENDAR_PERIODS = [
11693
+ "today",
11694
+ "yesterday",
11695
+ "this-week",
11696
+ "this-month"
11697
+ ];
11698
+ var RESET_PERIODS = [
11699
+ "never",
11700
+ "daily",
11701
+ "weekly",
11702
+ "monthly"
11703
+ ];
11704
+ 5 * MINUTE_MS;
11705
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11706
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11707
+ var rollingWindowObject = object({
11708
+ kind: literal("rolling"),
11709
+ span: _enum(ROLLING_SPANS),
11710
+ customMinutes: number().int().min(5).optional()
11711
+ }).check((payload) => {
11712
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11713
+ code: "custom",
11714
+ message: "a custom range needs a duration in minutes",
11715
+ path: ["customMinutes"],
11716
+ input: payload.value
11717
+ });
11718
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11719
+ code: "custom",
11720
+ message: "only a custom range carries a duration",
11721
+ path: ["customMinutes"],
11722
+ input: payload.value
11723
+ });
11724
+ });
11725
+ var RollingWindowSchema = rollingWindowObject;
11726
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
11727
+ kind: literal("calendar"),
11728
+ period: _enum(CALENDAR_PERIODS)
11729
+ })]);
11730
+ /**
11731
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11732
+ * id. Split out so helper schemas can name it without a cycle through
11733
+ * `composition.ts` (D659).
11734
+ */
11597
11735
  var CompositionSourceRefSchema = object({
11598
11736
  /** The owning addon; `stableId` is unique only within it. */
11599
11737
  addonId: string().min(1),
11600
11738
  stableId: string().min(1)
11601
11739
  });
11740
+ var GROUP_TEMPLATE_IDS = [
11741
+ "temperature",
11742
+ "humidity",
11743
+ "pressure",
11744
+ "illuminance",
11745
+ "power",
11746
+ "energy",
11747
+ "battery",
11748
+ "numeric",
11749
+ "motion",
11750
+ "contact",
11751
+ "leak",
11752
+ "smoke",
11753
+ "occupancy",
11754
+ "binary",
11755
+ "switch",
11756
+ "light",
11757
+ "fan",
11758
+ "cover",
11759
+ "valve",
11760
+ "lock",
11761
+ "media-player",
11762
+ "climate"
11763
+ ];
11764
+ var NUMERIC_AGGREGATES = [
11765
+ "mean",
11766
+ "median",
11767
+ "min",
11768
+ "max",
11769
+ "sum"
11770
+ ];
11771
+ var BINARY_AGGREGATES = ["any", "all"];
11772
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11773
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11774
+ var AGGREGATE_REDUCERS = [
11775
+ "mean",
11776
+ "median",
11777
+ "min",
11778
+ "max",
11779
+ "sum",
11780
+ "any",
11781
+ "all"
11782
+ ];
11783
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11784
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11785
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11786
+ /** One member. The field is stored so occupancy can name motion or binary. */
11787
+ var HelperMemberSchema = object({
11788
+ source: CompositionSourceRefSchema,
11789
+ cap: string().min(1),
11790
+ fieldPath: string().min(1)
11791
+ });
11792
+ var HelperSpecSchema = discriminatedUnion("kind", [
11793
+ object({
11794
+ kind: literal("group"),
11795
+ templateId: GroupTemplateIdSchema,
11796
+ aggregate: AggregateReducerSchema,
11797
+ members: array(HelperMemberSchema).min(1).max(64),
11798
+ /** Actuator groups only. Readings leave this unset. */
11799
+ active: ActiveCriterionSchema.optional()
11800
+ }),
11801
+ object({
11802
+ kind: literal("appliance"),
11803
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
11804
+ appliance: _enum(APPLIANCE_KINDS),
11805
+ thresholdWatts: number().positive(),
11806
+ startDwellMs: number().int().nonnegative(),
11807
+ stopDwellMs: number().int().nonnegative(),
11808
+ member: HelperMemberSchema
11809
+ }),
11810
+ object({
11811
+ kind: literal("statistic"),
11812
+ stat: _enum(STATISTIC_STATS),
11813
+ window: StatisticWindowSchema,
11814
+ source: HelperMemberSchema
11815
+ }),
11816
+ object({
11817
+ kind: literal("integral"),
11818
+ reset: _enum(RESET_PERIODS),
11819
+ output: _enum(["energy", "meter"]),
11820
+ source: HelperMemberSchema
11821
+ }),
11822
+ object({
11823
+ kind: literal("threshold"),
11824
+ source: HelperMemberSchema,
11825
+ high: number(),
11826
+ low: number(),
11827
+ onForMs: number().int().nonnegative(),
11828
+ offForMs: number().int().nonnegative()
11829
+ }),
11830
+ object({
11831
+ kind: literal("arithmetic"),
11832
+ op: _enum([
11833
+ "sum",
11834
+ "difference",
11835
+ "ratio"
11836
+ ]),
11837
+ inputs: array(HelperMemberSchema).min(2).max(64)
11838
+ }),
11839
+ object({
11840
+ kind: literal("expression"),
11841
+ expr: string().min(1),
11842
+ inputs: array(object({
11843
+ name: string().min(1),
11844
+ member: HelperMemberSchema
11845
+ })).min(1).max(64),
11846
+ output: _enum(["number", "boolean"])
11847
+ }),
11848
+ object({
11849
+ kind: literal("derivative"),
11850
+ window: RollingWindowSchema,
11851
+ per: _enum([
11852
+ "second",
11853
+ "minute",
11854
+ "hour"
11855
+ ]),
11856
+ source: HelperMemberSchema
11857
+ })
11858
+ ]);
11859
+ /** A composed device's stableId is this prefix plus the block id. */
11860
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11861
+ /** Longest snippet a `code` source may carry (slice 4). */
11862
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11602
11863
  /** One field of one capability of one source device. */
11603
11864
  var CompositionFieldReadSchema = object({
11604
11865
  source: CompositionSourceRefSchema,
@@ -11642,7 +11903,59 @@ var CompositionCodeSourceSchema = object({
11642
11903
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11643
11904
  CompositionFromSourceSchema,
11644
11905
  CompositionExpressionSourceSchema,
11645
- CompositionCodeSourceSchema
11906
+ CompositionCodeSourceSchema,
11907
+ object({
11908
+ kind: literal("aggregate"),
11909
+ reducer: _enum([
11910
+ "mean",
11911
+ "median",
11912
+ "min",
11913
+ "max",
11914
+ "sum",
11915
+ "any",
11916
+ "all"
11917
+ ]),
11918
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11919
+ /**
11920
+ * Set when the reads are an enum state, not a yes/no: the listed states
11921
+ * count as active (a cover is `open`). A state outside the list is inactive;
11922
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
11923
+ */
11924
+ activeWhen: array(string().min(1)).min(1).optional(),
11925
+ /** `value` is the reading; the others are the member counts. */
11926
+ publish: _enum([
11927
+ "value",
11928
+ "included",
11929
+ "excluded",
11930
+ "total"
11931
+ ]),
11932
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11933
+ empty: _enum([
11934
+ "null",
11935
+ "omit",
11936
+ "offline"
11937
+ ])
11938
+ }),
11939
+ object({
11940
+ kind: literal("series"),
11941
+ read: CompositionFieldReadSchema,
11942
+ /** `value` is the statistic or the integral. The others are the gap counts. */
11943
+ publish: _enum([
11944
+ "value",
11945
+ "included",
11946
+ "excluded",
11947
+ "total"
11948
+ ]),
11949
+ /**
11950
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
11951
+ * device unseeded until history answers, so a restart cannot publish 0.
11952
+ */
11953
+ empty: _enum([
11954
+ "null",
11955
+ "omit",
11956
+ "offline"
11957
+ ])
11958
+ })
11646
11959
  ]);
11647
11960
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11648
11961
  kind: literal("forward"),
@@ -11688,7 +12001,13 @@ var CompositionSchema = object({
11688
12001
  kind: literal("existing"),
11689
12002
  device: CompositionSourceRefSchema
11690
12003
  })]),
11691
- features: array(CompositionFeatureSchema).min(1).max(16)
12004
+ features: array(CompositionFeatureSchema).min(1).max(16),
12005
+ /**
12006
+ * Present when the block is a helper template. Absent on a free
12007
+ * composition (the room example, device 4603). The editor reads this,
12008
+ * never the compiled features.
12009
+ */
12010
+ helper: HelperSpecSchema.optional()
11692
12011
  }).superRefine((composition, ctx) => {
11693
12012
  const seen = /* @__PURE__ */ new Set();
11694
12013
  composition.features.forEach((feature, index) => {
@@ -16815,7 +17134,22 @@ var BrokerProviderInfoSchema = object({
16815
17134
  */
16816
17135
  interactiveConnectPath: string().min(1).optional(),
16817
17136
  /** Relative hub path to disconnect the linked account (same popup pattern). */
16818
- interactiveDisconnectPath: string().min(1).optional()
17137
+ interactiveDisconnectPath: string().min(1).optional(),
17138
+ /**
17139
+ * The kind signs in through a server-held interactive session instead of
17140
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17141
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17142
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17143
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17144
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17145
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17146
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17147
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17148
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17149
+ * offers it as the default path; the fields the session fills are
17150
+ * marked `filledBySignIn` in the kind's settings schema.
17151
+ */
17152
+ signIn: object({ basePath: string().min(1) }).optional()
16819
17153
  }))
16820
17154
  });
16821
17155
  var ListInputSchema = object({
@@ -17916,7 +18250,12 @@ var CompositionProblemSchema = object({
17916
18250
  "item-field-reserved",
17917
18251
  "target-missing",
17918
18252
  "target-unreadable",
17919
- "not-replaceable"
18253
+ "not-replaceable",
18254
+ "native-capability-replace",
18255
+ "mixed-quantity",
18256
+ "helper-mismatch",
18257
+ "member-constraint",
18258
+ "retention"
17920
18259
  ]),
17921
18260
  path: string(),
17922
18261
  message: string()
@@ -17937,7 +18276,8 @@ var CompositionUnavailableModeSchema = _enum([
17937
18276
  "null",
17938
18277
  "offline",
17939
18278
  "unknown",
17940
- "release"
18279
+ "release",
18280
+ "omit"
17941
18281
  ]);
17942
18282
  /**
17943
18283
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17974,9 +18314,11 @@ var CompositionFieldPlanSchema = object({
17974
18314
  reads: array(CompositionFieldReadSchema)
17975
18315
  });
17976
18316
  /**
17977
- * What a feature does to its target (D663): `new` builds a capability on a new
17978
- * device; on an existing device it `add`s a capability the device lacks, or
17979
- * `replace`s named fields of one it has natively.
18317
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18318
+ * new device; on an existing device it `add`s a capability the device lacks.
18319
+ * Replacing a capability the device already serves is refused
18320
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18321
+ * wire for a claim the hub already holds.
17980
18322
  */
17981
18323
  var CompositionFeatureModeSchema = _enum([
17982
18324
  "new",
@@ -18147,8 +18489,392 @@ Object.freeze({
18147
18489
  latchedAt: {
18148
18490
  kind: "fixed",
18149
18491
  result: ["number", "null"]
18492
+ },
18493
+ schmitt: {
18494
+ kind: "fixed",
18495
+ result: ["boolean", "null"]
18150
18496
  }
18151
18497
  });
18498
+ /**
18499
+ * One catalog row per base reading a group helper can build (D944 §3).
18500
+ * Data, not a form default written into the block.
18501
+ */
18502
+ var NOW$2 = {
18503
+ fieldPath: "lastFetchedAt",
18504
+ expr: "now"
18505
+ };
18506
+ var CHANGED = {
18507
+ fieldPath: "lastChangedAt",
18508
+ expr: "now"
18509
+ };
18510
+ function numeric(input) {
18511
+ return {
18512
+ ...input,
18513
+ family: "numeric",
18514
+ deviceType: DeviceType.Sensor,
18515
+ memberDeviceType: DeviceType.Sensor,
18516
+ memberDeviceTypes: [DeviceType.Sensor],
18517
+ activeWhen: void 0,
18518
+ commandCaps: [],
18519
+ baseUnit: baseUnitOf(input.quantity),
18520
+ aggregates: NUMERIC_AGGREGATES,
18521
+ accept: [{
18522
+ cap: input.outputCap,
18523
+ fieldPath: input.outputField
18524
+ }]
18525
+ };
18526
+ }
18527
+ function binary(input) {
18528
+ return {
18529
+ ...input,
18530
+ family: "binary",
18531
+ deviceType: DeviceType.Sensor,
18532
+ memberDeviceType: DeviceType.Sensor,
18533
+ memberDeviceTypes: [DeviceType.Sensor],
18534
+ activeWhen: void 0,
18535
+ commandCaps: [],
18536
+ quantity: "none",
18537
+ baseUnit: void 0,
18538
+ aggregates: BINARY_AGGREGATES,
18539
+ empty: "offline"
18540
+ };
18541
+ }
18542
+ var GROUP_TEMPLATES = [
18543
+ numeric({
18544
+ id: "temperature",
18545
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18546
+ role: DeviceRole.TemperatureSensor,
18547
+ outputCap: "temperature-sensor",
18548
+ outputField: "celsius",
18549
+ quantity: "temperature",
18550
+ defaultAggregate: "mean",
18551
+ empty: "null",
18552
+ companions: [NOW$2]
18553
+ }),
18554
+ numeric({
18555
+ id: "humidity",
18556
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18557
+ role: DeviceRole.HumiditySensor,
18558
+ outputCap: "humidity-sensor",
18559
+ outputField: "percent",
18560
+ quantity: "humidity",
18561
+ defaultAggregate: "mean",
18562
+ empty: "null",
18563
+ companions: [NOW$2]
18564
+ }),
18565
+ numeric({
18566
+ id: "pressure",
18567
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18568
+ role: DeviceRole.PressureSensor,
18569
+ outputCap: "pressure-sensor",
18570
+ outputField: "hpa",
18571
+ quantity: "pressure",
18572
+ defaultAggregate: "mean",
18573
+ empty: "null",
18574
+ companions: [NOW$2]
18575
+ }),
18576
+ numeric({
18577
+ id: "illuminance",
18578
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18579
+ role: DeviceRole.AmbientLightSensor,
18580
+ outputCap: "ambient-light-sensor",
18581
+ outputField: "lux",
18582
+ quantity: "illuminance",
18583
+ defaultAggregate: "mean",
18584
+ empty: "null",
18585
+ companions: [NOW$2]
18586
+ }),
18587
+ numeric({
18588
+ id: "power",
18589
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18590
+ role: DeviceRole.PowerSensor,
18591
+ outputCap: "power-meter",
18592
+ outputField: "watts",
18593
+ quantity: "power",
18594
+ defaultAggregate: "sum",
18595
+ empty: "omit",
18596
+ companions: [NOW$2]
18597
+ }),
18598
+ numeric({
18599
+ id: "energy",
18600
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18601
+ role: DeviceRole.EnergySensor,
18602
+ outputCap: "power-meter",
18603
+ outputField: "kwhTotal",
18604
+ quantity: "energy",
18605
+ defaultAggregate: "sum",
18606
+ empty: "omit",
18607
+ companions: [NOW$2]
18608
+ }),
18609
+ numeric({
18610
+ id: "battery",
18611
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18612
+ role: DeviceRole.BatterySensor,
18613
+ outputCap: "battery",
18614
+ outputField: "percentage",
18615
+ quantity: "percentage",
18616
+ defaultAggregate: "mean",
18617
+ empty: "null",
18618
+ companions: [
18619
+ {
18620
+ fieldPath: "charging",
18621
+ expr: "\"unknown\""
18622
+ },
18623
+ {
18624
+ fieldPath: "sleeping",
18625
+ expr: "false"
18626
+ },
18627
+ {
18628
+ fieldPath: "lastUpdated",
18629
+ expr: "now"
18630
+ }
18631
+ ]
18632
+ }),
18633
+ numeric({
18634
+ id: "numeric",
18635
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18636
+ role: DeviceRole.NumericSensor,
18637
+ outputCap: "numeric-sensor",
18638
+ outputField: "value",
18639
+ quantity: "device",
18640
+ defaultAggregate: "mean",
18641
+ empty: "null",
18642
+ companions: [NOW$2]
18643
+ }),
18644
+ binary({
18645
+ id: "motion",
18646
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18647
+ role: DeviceRole.MotionSensor,
18648
+ outputCap: "motion",
18649
+ outputField: "detected",
18650
+ defaultAggregate: "any",
18651
+ accept: [{
18652
+ cap: "motion",
18653
+ fieldPath: "detected"
18654
+ }],
18655
+ companions: [{
18656
+ fieldPath: "lastDetectedAt",
18657
+ expr: "null"
18658
+ }, {
18659
+ fieldPath: "autoClearAfterMs",
18660
+ expr: "null"
18661
+ }]
18662
+ }),
18663
+ binary({
18664
+ id: "contact",
18665
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18666
+ role: DeviceRole.ContactSensor,
18667
+ outputCap: "contact",
18668
+ outputField: "entryOpen",
18669
+ defaultAggregate: "any",
18670
+ accept: [{
18671
+ cap: "contact",
18672
+ fieldPath: "entryOpen"
18673
+ }],
18674
+ companions: [CHANGED]
18675
+ }),
18676
+ binary({
18677
+ id: "leak",
18678
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18679
+ role: DeviceRole.LeakSensor,
18680
+ outputCap: "flood",
18681
+ outputField: "flooded",
18682
+ defaultAggregate: "any",
18683
+ accept: [{
18684
+ cap: "flood",
18685
+ fieldPath: "flooded"
18686
+ }],
18687
+ companions: [CHANGED]
18688
+ }),
18689
+ binary({
18690
+ id: "smoke",
18691
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18692
+ role: DeviceRole.SmokeSensor,
18693
+ outputCap: "smoke",
18694
+ outputField: "detected",
18695
+ defaultAggregate: "any",
18696
+ accept: [{
18697
+ cap: "smoke",
18698
+ fieldPath: "detected"
18699
+ }],
18700
+ companions: [CHANGED]
18701
+ }),
18702
+ binary({
18703
+ id: "occupancy",
18704
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18705
+ role: DeviceRole.BinarySensor,
18706
+ outputCap: "binary",
18707
+ outputField: "on",
18708
+ defaultAggregate: "any",
18709
+ accept: [{
18710
+ cap: "motion",
18711
+ fieldPath: "detected"
18712
+ }, {
18713
+ cap: "binary",
18714
+ fieldPath: "on"
18715
+ }],
18716
+ companions: [CHANGED]
18717
+ }),
18718
+ binary({
18719
+ id: "binary",
18720
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18721
+ role: DeviceRole.BinarySensor,
18722
+ outputCap: "binary",
18723
+ outputField: "on",
18724
+ defaultAggregate: "any",
18725
+ accept: [{
18726
+ cap: "binary",
18727
+ fieldPath: "on"
18728
+ }],
18729
+ companions: [CHANGED]
18730
+ }),
18731
+ actuator({
18732
+ id: "switch",
18733
+ 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.",
18734
+ deviceType: DeviceType.Switch,
18735
+ outputCap: "switch",
18736
+ outputField: "on",
18737
+ member: {
18738
+ cap: "switch",
18739
+ fieldPath: "on"
18740
+ },
18741
+ commandCaps: ["switch"]
18742
+ }),
18743
+ actuator({
18744
+ id: "light",
18745
+ 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.",
18746
+ deviceType: DeviceType.Light,
18747
+ outputCap: "switch",
18748
+ outputField: "on",
18749
+ member: {
18750
+ cap: "switch",
18751
+ fieldPath: "on"
18752
+ },
18753
+ commandCaps: [
18754
+ "switch",
18755
+ "brightness",
18756
+ "color"
18757
+ ]
18758
+ }),
18759
+ actuator({
18760
+ id: "fan",
18761
+ 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.",
18762
+ deviceType: DeviceType.Fan,
18763
+ outputCap: "switch",
18764
+ outputField: "on",
18765
+ member: {
18766
+ cap: "switch",
18767
+ fieldPath: "on"
18768
+ },
18769
+ commandCaps: ["switch", "fan-control"]
18770
+ }),
18771
+ actuator({
18772
+ id: "cover",
18773
+ 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.",
18774
+ deviceType: DeviceType.Cover,
18775
+ outputCap: "group-active",
18776
+ outputField: "active",
18777
+ member: {
18778
+ cap: "cover",
18779
+ fieldPath: "state"
18780
+ },
18781
+ activeWhen: [
18782
+ "open",
18783
+ "opening",
18784
+ "stopped"
18785
+ ],
18786
+ commandCaps: ["cover"]
18787
+ }),
18788
+ actuator({
18789
+ id: "valve",
18790
+ 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.",
18791
+ deviceType: DeviceType.Valve,
18792
+ outputCap: "group-active",
18793
+ outputField: "active",
18794
+ member: {
18795
+ cap: "valve",
18796
+ fieldPath: "state"
18797
+ },
18798
+ activeWhen: [
18799
+ "open",
18800
+ "opening",
18801
+ "stopped"
18802
+ ],
18803
+ commandCaps: ["valve"]
18804
+ }),
18805
+ actuator({
18806
+ id: "lock",
18807
+ 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.",
18808
+ deviceType: DeviceType.Lock,
18809
+ outputCap: "group-active",
18810
+ outputField: "active",
18811
+ member: {
18812
+ cap: "lock-control",
18813
+ fieldPath: "state"
18814
+ },
18815
+ activeWhen: ["locked"],
18816
+ commandCaps: ["lock-control"]
18817
+ }),
18818
+ actuator({
18819
+ id: "media-player",
18820
+ 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.",
18821
+ deviceType: DeviceType.MediaPlayer,
18822
+ outputCap: "group-active",
18823
+ outputField: "active",
18824
+ member: {
18825
+ cap: "media-player",
18826
+ fieldPath: "state"
18827
+ },
18828
+ activeWhen: ["playing", "buffering"],
18829
+ commandCaps: ["media-player"]
18830
+ }),
18831
+ actuator({
18832
+ id: "climate",
18833
+ 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.",
18834
+ deviceType: DeviceType.Thermostat,
18835
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
18836
+ outputCap: "group-active",
18837
+ outputField: "active",
18838
+ member: {
18839
+ cap: "climate-control",
18840
+ fieldPath: "mode"
18841
+ },
18842
+ activeWhen: [
18843
+ "heat",
18844
+ "cool",
18845
+ "auto",
18846
+ "heat_cool",
18847
+ "fan_only",
18848
+ "dry"
18849
+ ],
18850
+ commandCaps: ["climate-control"]
18851
+ })
18852
+ ];
18853
+ function actuator(input) {
18854
+ return {
18855
+ id: input.id,
18856
+ summary: input.summary,
18857
+ deviceType: input.deviceType,
18858
+ outputCap: input.outputCap,
18859
+ outputField: input.outputField,
18860
+ commandCaps: input.commandCaps,
18861
+ family: "actuator",
18862
+ role: void 0,
18863
+ memberDeviceType: input.deviceType,
18864
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
18865
+ activeWhen: input.activeWhen,
18866
+ quantity: "none",
18867
+ baseUnit: void 0,
18868
+ aggregates: BINARY_AGGREGATES,
18869
+ defaultAggregate: "any",
18870
+ empty: "offline",
18871
+ accept: [input.member],
18872
+ companions: [CHANGED]
18873
+ };
18874
+ }
18875
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18876
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18877
+ DeviceType.Camera;
18152
18878
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18153
18879
  /**
18154
18880
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19862,12 +20588,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19862
20588
  * providers detect renames by matching `uniqueId` and patch `id`
19863
20589
  * in-place so dispatch keeps working.
19864
20590
  *
19865
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19866
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19867
- * state push and are the single source of truth. The `device_class` tag is
19868
- * an HA-specific input that is consumed at the discovery boundary
19869
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19870
- * stored after that point.
20591
+ * **Display unit and precision are NOT stored here.** They are a read-time
20592
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20593
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20594
+ * neither is repeated on each sample. The `device_class` tag is an
20595
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20596
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19871
20597
  *
19872
20598
  * The `raw` escape hatch carries provider-specific attributes that don't
19873
20599
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19881,6 +20607,14 @@ var SourceInfoSchema = object({
19881
20607
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19882
20608
  * Used to detect rename when `id` changes. */
19883
20609
  uniqueId: string().optional(),
20610
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20611
+ quantity: ReadingQuantitySchema.optional(),
20612
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20613
+ dateTimeShape: _enum([
20614
+ "date",
20615
+ "time",
20616
+ "datetime"
20617
+ ]).optional(),
19884
20618
  /** Provider-specific extras that don't fit the structured slots.
19885
20619
  * Whitelist what goes here per provider — do NOT dump entire HA
19886
20620
  * attribute blobs (would explode the meta JSON column). */
@@ -41002,7 +41736,8 @@ method(object({
41002
41736
  "enum"
41003
41737
  ]),
41004
41738
  enumValues: array(string()).optional(),
41005
- item: boolean().optional()
41739
+ item: boolean().optional(),
41740
+ quantity: ReadingQuantitySchema.optional()
41006
41741
  })).readonly(),
41007
41742
  itemArray: object({
41008
41743
  path: string(),
@@ -43363,7 +44098,7 @@ var NetworkAccessStatusSchema = object({
43363
44098
  });
43364
44099
  /**
43365
44100
  * Optional, richer endpoint shape returned by providers that expose
43366
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44101
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43367
44102
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43368
44103
  * the originating provider config (mode + sourcePort) so the
43369
44104
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44846,8 +45581,8 @@ var MaskPolygonShapeSchema = object({
44846
45581
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44847
45582
  var MaskGridShapeSchema = object({
44848
45583
  kind: literal("grid"),
44849
- gridWidth: number(),
44850
- gridHeight: number(),
45584
+ gridWidth: number().meta(readingMeta("none")),
45585
+ gridHeight: number().meta(readingMeta("none")),
44851
45586
  cells: array(boolean())
44852
45587
  });
44853
45588
  discriminatedUnion("kind", [
@@ -44959,6 +45694,8 @@ var NcSystemEventKindSchema = _enum([
44959
45694
  "detection-blind",
44960
45695
  "motion-noisy",
44961
45696
  "motion-calm",
45697
+ "package-llm-silent",
45698
+ "package-llm-answered",
44962
45699
  "recording-stalled",
44963
45700
  "recording-healthy",
44964
45701
  "recording-degraded",
@@ -46048,7 +46785,20 @@ var NcRuleInputSchema = object({
46048
46785
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46049
46786
  * path does not run Zod defaults.
46050
46787
  */
46051
- groupingSec: number().int().min(5).max(60).optional()
46788
+ groupingSec: number().int().min(5).max(60).optional(),
46789
+ /**
46790
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46791
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46792
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46793
+ * same mark the Library writes, and records whose notification it was and
46794
+ * when. A channel with no buttons gets the same two choices as links.
46795
+ *
46796
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46797
+ * cap path): absent and `false` both mean off, which is what every rule
46798
+ * authored before this field meant. An editor turns it off by writing
46799
+ * `false`, so it needs no `clear` key.
46800
+ */
46801
+ askVerdict: boolean().optional()
46052
46802
  });
46053
46803
  /**
46054
46804
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -56154,6 +56904,28 @@ var PackageVersionInfoSchema = object({
56154
56904
  deprecated: string().optional(),
56155
56905
  distTags: array(string()).readonly()
56156
56906
  });
56907
+ /**
56908
+ * One published document. `ok` is markdown from the registry. `missing` means
56909
+ * the registry answered and the file was not there. `error` is a registry or
56910
+ * tarball failure — never an empty string standing in for either.
56911
+ */
56912
+ var PackageDocBodySchema = discriminatedUnion("status", [
56913
+ object({
56914
+ status: literal("ok"),
56915
+ markdown: string()
56916
+ }),
56917
+ object({ status: literal("missing") }),
56918
+ object({
56919
+ status: literal("error"),
56920
+ message: string()
56921
+ })
56922
+ ]);
56923
+ var PackageDocsSchema = object({
56924
+ name: string(),
56925
+ version: string(),
56926
+ readme: PackageDocBodySchema,
56927
+ changelog: PackageDocBodySchema
56928
+ });
56157
56929
  var SearchResultSchema = object({
56158
56930
  name: string(),
56159
56931
  version: string(),
@@ -56372,7 +57144,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56372
57144
  }), object({ success: literal(true) }), {
56373
57145
  kind: "mutation",
56374
57146
  auth: "admin"
56375
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57147
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57148
+ name: string().min(1),
57149
+ version: string().min(1).optional()
57150
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56376
57151
  kind: "mutation",
56377
57152
  auth: "admin"
56378
57153
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56431,27 +57206,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56431
57206
  */
56432
57207
  var AirQualitySensorStatusSchema = object({
56433
57208
  /** Carbon dioxide concentration in ppm. */
56434
- co2Ppm: number().min(0).optional(),
57209
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56435
57210
  /** Total volatile organic compounds in ppb. */
56436
- vocPpb: number().min(0).optional(),
57211
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56437
57212
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56438
- pm25: number().min(0).optional(),
57213
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56439
57214
  /** Particulate matter ≤ 10 μm in µg/m³. */
56440
- pm10: number().min(0).optional(),
57215
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56441
57216
  /** Composite AQI value (typically 0..500). */
56442
- aqi: number().optional(),
57217
+ aqi: number().optional().meta(readingMeta("index")),
56443
57218
  /** Ms epoch when the slice was last updated. */
56444
- lastFetchedAt: number(),
56445
- /** Live display unit of the single metric this slice carries (e.g. HA
56446
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56447
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56448
- * per slice is unambiguous. */
56449
- unit: string().optional(),
56450
- /** Suggested decimal places for numeric display.
56451
- * Populated live from the upstream source when provided (e.g. HA
56452
- * `attributes.suggested_display_precision`). Falls back to
56453
- * auto-formatting when absent. */
56454
- precision: number().int().min(0).max(10).optional()
57219
+ lastFetchedAt: number()
56455
57220
  });
56456
57221
  var airQualitySensorCapability = {
56457
57222
  name: "air-quality-sensor",
@@ -56488,12 +57253,12 @@ var airQualitySensorCapability = {
56488
57253
  },
56489
57254
  pm25: {
56490
57255
  kind: "gauge",
56491
- unit: "ug/m3",
57256
+ unit: "µg/m³",
56492
57257
  deadband: 1
56493
57258
  },
56494
57259
  pm10: {
56495
57260
  kind: "gauge",
56496
- unit: "ug/m3",
57261
+ unit: "µg/m³",
56497
57262
  deadband: 1
56498
57263
  },
56499
57264
  aqi: {
@@ -56511,22 +57276,13 @@ var airQualitySensorCapability = {
56511
57276
  * entries with `device_class: illuminance`.
56512
57277
  */
56513
57278
  var AmbientLightSensorStatusSchema = object({
56514
- /** Current illuminance in lux (lx). */
56515
- lux: number().min(0).meta({
56516
- unit: "lx",
57279
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57280
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57281
+ precision: 0,
56517
57282
  label: "Illuminance"
56518
- }),
57283
+ })),
56519
57284
  /** Ms epoch when the slice was last updated. */
56520
- lastFetchedAt: number(),
56521
- /** Live display unit from the upstream source (e.g. HA
56522
- * `attributes.unit_of_measurement`). The UI prefers this over the
56523
- * role's canonical unit. Absent → fall back to the canonical unit. */
56524
- unit: string().optional(),
56525
- /** Suggested decimal places for numeric display.
56526
- * Populated live from the upstream source when provided (e.g. HA
56527
- * `attributes.suggested_display_precision`). Falls back to
56528
- * auto-formatting when absent. */
56529
- precision: number().int().min(0).max(10).optional()
57285
+ lastFetchedAt: number()
56530
57286
  });
56531
57287
  var ambientLightSensorCapability = {
56532
57288
  name: "ambient-light-sensor",
@@ -56558,12 +57314,7 @@ var ambientLightSensorCapability = {
56558
57314
  } } },
56559
57315
  /** Clock fields: written, but excluded from the compare that decides
56560
57316
  * whether persisting is worth a SQLite commit. */
56561
- volatileStateFields: ["lastFetchedAt"],
56562
- customize: {
56563
- version: 1,
56564
- advanced: "derive",
56565
- targets: []
56566
- }
57317
+ volatileStateFields: ["lastFetchedAt"]
56567
57318
  };
56568
57319
  /**
56569
57320
  * Per-class audio metrics aggregated over a sliding window.
@@ -56790,10 +57541,7 @@ var BatteryStatusSchema = object({
56790
57541
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56791
57542
  * declines to band a non-finite reading, which is the correct shape.
56792
57543
  */
56793
- percentage: number().min(0).max(100).nullable().meta({
56794
- unit: "%",
56795
- label: "Charge"
56796
- }),
57544
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56797
57545
  /**
56798
57546
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56799
57547
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56820,7 +57568,14 @@ var BatteryStatusSchema = object({
56820
57568
  */
56821
57569
  sleeping: boolean().meta({ label: "Sleeping" }),
56822
57570
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56823
- lastUpdated: number(),
57571
+ lastUpdated: number().meta(readingMeta("none")),
57572
+ /**
57573
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57574
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57575
+ * that only report charge. A second reading on this cap, not a second
57576
+ * `numeric-sensor` on the same device.
57577
+ */
57578
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56824
57579
  /**
56825
57580
  * Ms epoch of the last time the device PROVED it was reachable — a
56826
57581
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56867,12 +57622,12 @@ var BATTERY_DEVICE_TYPES = [
56867
57622
  DeviceType.Switch,
56868
57623
  DeviceType.PetFeeder,
56869
57624
  DeviceType.Container,
56870
- DeviceType.Thermostat
57625
+ DeviceType.Thermostat,
57626
+ DeviceType.EventEmitter
56871
57627
  ];
56872
57628
  /** Generic Customize target for the percentage reading (D936). */
56873
57629
  var batteryCustomize = {
56874
57630
  version: 1,
56875
- advanced: "derive",
56876
57631
  targets: [{
56877
57632
  id: "percentage",
56878
57633
  cardinality: "scalar",
@@ -56881,7 +57636,7 @@ var batteryCustomize = {
56881
57636
  stateField: "percentage"
56882
57637
  },
56883
57638
  deviceTypes: BATTERY_DEVICE_TYPES,
56884
- apply: "add-or-replace",
57639
+ apply: "add",
56885
57640
  path: "percentage",
56886
57641
  sources: [{
56887
57642
  kind: "field",
@@ -57002,10 +57757,16 @@ onStatusChanged: { data: object({
57002
57757
  /**
57003
57758
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57004
57759
  */
57005
- history: { fields: { percentage: {
57006
- kind: "gauge",
57007
- unit: "%"
57008
- } } },
57760
+ history: { fields: {
57761
+ percentage: {
57762
+ kind: "gauge",
57763
+ unit: "%"
57764
+ },
57765
+ volts: {
57766
+ kind: "gauge",
57767
+ unit: "V"
57768
+ }
57769
+ } },
57009
57770
  customize: batteryCustomize,
57010
57771
  /** Clock fields: written, but excluded from the compare that decides
57011
57772
  * whether persisting is worth a SQLite commit. */
@@ -57052,12 +57813,7 @@ var binaryCapability = {
57052
57813
  history: { fields: { on: { kind: "boolean" } } },
57053
57814
  /** Clock fields: written, but excluded from the compare that decides
57054
57815
  * whether persisting is worth a SQLite commit. */
57055
- volatileStateFields: ["lastChangedAt"],
57056
- customize: {
57057
- version: 1,
57058
- advanced: "derive",
57059
- targets: []
57060
- }
57816
+ volatileStateFields: ["lastChangedAt"]
57061
57817
  };
57062
57818
  /**
57063
57819
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57073,7 +57829,7 @@ var binaryCapability = {
57073
57829
  */
57074
57830
  var BrightnessStatusSchema = object({
57075
57831
  /** Current level as 0..100 inclusive. Firmware-reported. */
57076
- percentage: number().min(0).max(100),
57832
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57077
57833
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57078
57834
  lastChangedAt: number()
57079
57835
  });
@@ -57527,21 +58283,21 @@ var ClimateControlStatusSchema = object({
57527
58283
  /** Single setpoint in Celsius. Used by single-target modes
57528
58284
  * (heat / cool / dry). Null when the device is in a range mode
57529
58285
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57530
- target: number().nullable(),
58286
+ target: number().nullable().meta(readingMeta("temperature")),
57531
58287
  /** Upper bound of the dual setpoint in Celsius. Populated only
57532
58288
  * in `heat_cool` mode. */
57533
- targetHigh: number().nullable(),
58289
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57534
58290
  /** Lower bound of the dual setpoint in Celsius. Populated only
57535
58291
  * in `heat_cool` mode. */
57536
- targetLow: number().nullable(),
58292
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57537
58293
  /** Target relative humidity (0..100). Null when the device has no
57538
58294
  * humidity control. */
57539
- targetHumidity: number().min(0).max(100).nullable(),
58295
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57540
58296
  /** Current measured temperature in Celsius. Read-only — pushed by
57541
58297
  * the device's internal sensor. */
57542
- currentTemp: number().nullable(),
58298
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57543
58299
  /** Current measured relative humidity (0..100). Read-only. */
57544
- currentHumidity: number().min(0).max(100).nullable(),
58300
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57545
58301
  /** Vertical louver swing on/off. Null when the device has no vertical
57546
58302
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57547
58303
  swingVertical: boolean().nullable(),
@@ -57549,11 +58305,11 @@ var ClimateControlStatusSchema = object({
57549
58305
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57550
58306
  swingHorizontal: boolean().nullable(),
57551
58307
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57552
- minTemp: number().nullable().optional(),
58308
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57553
58309
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57554
- maxTemp: number().nullable().optional(),
58310
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57555
58311
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57556
- tempStep: number().positive().nullable().optional(),
58312
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57557
58313
  /** Ms epoch when the slice was last updated (push or command). */
57558
58314
  lastFetchedAt: number()
57559
58315
  });
@@ -57695,17 +58451,17 @@ var climateControlCapability = {
57695
58451
  * modes to expose in the picker without a slice fetch.
57696
58452
  */
57697
58453
  var RgbTripletSchema = object({
57698
- r: number().int().min(0).max(255),
57699
- g: number().int().min(0).max(255),
57700
- b: number().int().min(0).max(255)
58454
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58455
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58456
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57701
58457
  });
57702
58458
  var HsvTripletSchema = object({
57703
58459
  /** Hue in degrees, 0..360. */
57704
- h: number().min(0).max(360),
58460
+ h: number().min(0).max(360).meta(readingMeta("none")),
57705
58461
  /** Saturation as 0..100 inclusive. */
57706
- s: number().min(0).max(100),
58462
+ s: number().min(0).max(100).meta(readingMeta("none")),
57707
58463
  /** Value/brightness as 0..100 inclusive. */
57708
- v: number().min(0).max(100)
58464
+ v: number().min(0).max(100).meta(readingMeta("none"))
57709
58465
  });
57710
58466
  var ColorInputSchema = discriminatedUnion("mode", [
57711
58467
  object({
@@ -57734,7 +58490,7 @@ var ColorStatusSchema = object({
57734
58490
  /** Populated when `mode === 'hsv'`. */
57735
58491
  hsv: HsvTripletSchema.optional(),
57736
58492
  /** Populated when `mode === 'mired'`. */
57737
- mireds: number().int().optional(),
58493
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57738
58494
  /** Ms epoch of the last operator-driven change. */
57739
58495
  lastChangedAt: number()
57740
58496
  });
@@ -57937,18 +58693,12 @@ _enum([
57937
58693
  "empty",
57938
58694
  "full"
57939
58695
  ]);
57940
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57941
- * each. */
57942
- var ConsumableRemainingUnitSchema = _enum([
57943
- "days",
57944
- "hours",
57945
- "cycles"
57946
- ]);
57947
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57948
- * days). `value` null = unknown (D393). */
58696
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58697
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58698
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57949
58699
  var ConsumableRemainingSchema = object({
57950
- value: number().min(0).nullable(),
57951
- unit: ConsumableRemainingUnitSchema
58700
+ value: number().min(0).nullable().meta(readingMeta("device")),
58701
+ kind: ConsumableRemainingKindSchema
57952
58702
  });
57953
58703
  /** A single consumable item. Either a continuous `level` (remaining
57954
58704
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57961,7 +58711,7 @@ var ConsumableItemSchema = object({
57961
58711
  /** Display name. */
57962
58712
  label: string().min(1),
57963
58713
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57964
- level: number().min(0).max(100).nullable(),
58714
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57965
58715
  /** The provider's discrete verdict when known (binary mode). */
57966
58716
  status: ConsumableStatusSchema.nullable(),
57967
58717
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58043,8 +58793,8 @@ var consumablesCustomize = {
58043
58793
  transforms: [{ kind: "identity" }]
58044
58794
  }],
58045
58795
  literals: [{
58046
- path: "remaining.unit",
58047
- values: [...ConsumableRemainingUnitSchema.options]
58796
+ path: "remaining.kind",
58797
+ values: [...ConsumableRemainingKindSchema.options]
58048
58798
  }]
58049
58799
  },
58050
58800
  {
@@ -58173,7 +58923,6 @@ reset: method(object({
58173
58923
  },
58174
58924
  "remaining.value": {
58175
58925
  kind: "gauge",
58176
- unitFrom: "remaining.unit",
58177
58926
  deadband: 1,
58178
58927
  displayName: "Remaining"
58179
58928
  }
@@ -58243,10 +58992,9 @@ var contactCapability = {
58243
58992
  * (empty array for the other kinds). It lives in the slice because
58244
58993
  * HA `select.*` entities may update their options at runtime.
58245
58994
  *
58246
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58247
- * `kind === 'numeric'` and are absent (undefined) for all other
58248
- * kinds. They are populated live from HA attributes on each state
58249
- * push — consistent with `options` and the sensor-unit approach.
58995
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58996
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58997
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58250
58998
  */
58251
58999
  var ControlStatusSchema = object({
58252
59000
  kind: _enum([
@@ -58258,39 +59006,17 @@ var ControlStatusSchema = object({
58258
59006
  /** Current value. Numeric kind → number; select/text/datetime → string.
58259
59007
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58260
59008
  * or time-only — HA accepts all three forms). */
58261
- value: union([number(), string()]),
59009
+ value: union([number().meta(readingMeta("device")), string()]),
58262
59010
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58263
59011
  options: array(string()),
58264
59012
  /** Ms epoch when the slice was last updated. */
58265
59013
  lastChangedAt: number(),
58266
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58267
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58268
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58269
- unit: string().optional(),
58270
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58271
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58272
- min: number().optional(),
58273
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58274
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58275
- max: number().optional(),
58276
- /** Step increment. Populated live from HA `attributes.step`.
58277
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58278
- step: number().optional(),
58279
- /** Suggested decimal places for numeric display. Populated live from HA
58280
- * `attributes.suggested_display_precision`, or derived from `step` when the
58281
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58282
- * other kinds. Falls back to auto-formatting when absent. */
58283
- precision: number().int().min(0).max(10).optional(),
58284
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58285
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58286
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58287
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58288
- * `has_date`/`has_time` attributes on each push. */
58289
- format: _enum([
58290
- "date",
58291
- "time",
58292
- "datetime"
58293
- ]).optional()
59014
+ /** Minimum allowed value, in the device quantity's base unit. */
59015
+ min: number().optional().meta(readingMeta("device")),
59016
+ /** Maximum allowed value, in the device quantity's base unit. */
59017
+ max: number().optional().meta(readingMeta("device")),
59018
+ /** Step increment, in the device quantity's base unit. */
59019
+ step: number().optional().meta(readingMeta("device"))
58294
59020
  });
58295
59021
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58296
59022
  object({
@@ -58356,9 +59082,9 @@ var CoverStatusSchema = object({
58356
59082
  ]),
58357
59083
  /** 0 = fully closed, 100 = fully open. Null when the device has
58358
59084
  * no intermediate position surface. */
58359
- position: number().min(0).max(100).nullable(),
59085
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58360
59086
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58361
- tiltPosition: number().min(0).max(100).nullable(),
59087
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58362
59088
  /** Ms epoch when the slice was last updated. */
58363
59089
  lastChangedAt: number()
58364
59090
  });
@@ -58461,9 +59187,9 @@ var NormalizedRangeSchema$1 = object({
58461
59187
  var DayNightStatusSchema = object({
58462
59188
  mode: DayNightModeSchema,
58463
59189
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58464
- sensitivity: number().optional(),
59190
+ sensitivity: number().optional().meta(readingMeta("none")),
58465
59191
  /** Delay before the IR-cut filter flips, in seconds. */
58466
- switchDelaySec: number().optional(),
59192
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58467
59193
  lastFetchedAt: number()
58468
59194
  });
58469
59195
  /**
@@ -58681,11 +59407,11 @@ var DishwasherStatusSchema = object({
58681
59407
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58682
59408
  availablePrograms: array(string()),
58683
59409
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58684
- remainingSeconds: number().int().nonnegative().nullable(),
59410
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58685
59411
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58686
- delayedStartSeconds: number().int().nonnegative().nullable(),
59412
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58687
59413
  /** Cycle completion 0..100, or null when not applicable. */
58688
- progressPercent: number().min(0).max(100).nullable(),
59414
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58689
59415
  /** True when the door is reported open; null when the door state is not known. */
58690
59416
  doorOpen: boolean().nullable(),
58691
59417
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58709,7 +59435,7 @@ var DishwasherStatusSchema = object({
58709
59435
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58710
59436
  machineCareDue: boolean().nullable(),
58711
59437
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58712
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59438
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58713
59439
  /** Option states: null when the appliance does not expose the option or has not said. */
58714
59440
  halfLoad: boolean().nullable(),
58715
59441
  hygienePlus: boolean().nullable(),
@@ -58724,7 +59450,7 @@ var DishwasherStatusSchema = object({
58724
59450
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58725
59451
  activeEvents: array(string()),
58726
59452
  /** Numeric error code when the appliance reports one; null otherwise. */
58727
- errorCode: number().nullable(),
59453
+ errorCode: number().nullable().meta(readingMeta("none")),
58728
59454
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58729
59455
  errorLabel: string().nullable(),
58730
59456
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58955,7 +59681,7 @@ var DoorbellStatusSchema = object({
58955
59681
  /** Ms epoch of the last press. null = never observed since this provider started. */
58956
59682
  lastPressedAt: number().nullable(),
58957
59683
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58958
- pressCountSinceStart: number()
59684
+ pressCountSinceStart: number().meta(readingMeta("count"))
58959
59685
  });
58960
59686
  var DoorbellPressEventSchema = object({
58961
59687
  deviceId: number(),
@@ -59020,17 +59746,7 @@ onPressed: {
59020
59746
  history: "none",
59021
59747
  customize: doorbellCustomize
59022
59748
  };
59023
- /**
59024
- * Enum-state sensor — a string value picked from a finite option set.
59025
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59026
- * states, weather conditions, contact-source channels, mode strings).
59027
- * The option set is stable across observations; it is persisted in the
59028
- * device config blob (via the control slice at adoption) and is NOT
59029
- * stored in `sourceInfo`.
59030
- */
59031
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59032
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59033
- var EnumSensorDateTimeFormatSchema = _enum([
59749
+ _enum([
59034
59750
  "date",
59035
59751
  "time",
59036
59752
  "datetime"
@@ -59038,12 +59754,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59038
59754
  var EnumSensorStatusSchema = object({
59039
59755
  value: string(),
59040
59756
  /**
59041
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59042
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59043
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59044
- * a time-only sensor → `'time'`.
59757
+ * Set when `value` is a timestamp rather than an enum state, so history
59758
+ * does not learn ISO strings as states. The display shape (date / time /
59759
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59045
59760
  */
59046
- format: EnumSensorDateTimeFormatSchema.optional(),
59761
+ timestamp: boolean().optional(),
59047
59762
  /** Ms epoch when the slice was last updated. */
59048
59763
  lastFetchedAt: number()
59049
59764
  });
@@ -59070,11 +59785,11 @@ var enumSensorCapability = {
59070
59785
  * History (D850): the value as an `enum` series (an appliance's program or
59071
59786
  * operation state, an HVAC action). The option set is per device, so the
59072
59787
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59073
- * `format` carries a timestamp, not a state, and is not recorded.
59788
+ * `timestamp` carries a clock, not a state, and is not recorded.
59074
59789
  */
59075
59790
  history: { fields: { value: {
59076
59791
  kind: "enum",
59077
- skipWhenSet: "format"
59792
+ skipWhenSet: "timestamp"
59078
59793
  } } },
59079
59794
  /** Clock fields: written, but excluded from the compare that decides
59080
59795
  * whether persisting is worth a SQLite commit. */
@@ -59094,13 +59809,13 @@ var EventFireSchema = object({
59094
59809
  deviceId: number(),
59095
59810
  eventType: string(),
59096
59811
  data: record(string(), unknown()).nullable(),
59097
- timestamp: number(),
59098
- seq: number()
59812
+ timestamp: number().meta(readingMeta("none")),
59813
+ seq: number().meta(readingMeta("count"))
59099
59814
  });
59100
59815
  var EventEmitterStatusSchema = object({
59101
59816
  eventTypes: array(string()),
59102
59817
  lastEvent: EventFireSchema.nullable(),
59103
- eventCountSinceStart: number()
59818
+ eventCountSinceStart: number().meta(readingMeta("count"))
59104
59819
  });
59105
59820
  var eventEmitterCapability = {
59106
59821
  name: "event-emitter",
@@ -59513,10 +60228,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59513
60228
  var FanControlStatusSchema = object({
59514
60229
  /** Active speed as 0..100 inclusive. Null when the device has no
59515
60230
  * speed surface (single-speed fan). */
59516
- percentage: number().min(0).max(100).nullable(),
60231
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59517
60232
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59518
60233
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59519
- percentageStep: number().positive().optional(),
60234
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59520
60235
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59521
60236
  * — empty string when no preset is active or supported. */
59522
60237
  preset: string(),
@@ -59637,7 +60352,7 @@ var FeatureProbeStatusSchema = object({
59637
60352
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59638
60353
  model: string().nullable(),
59639
60354
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59640
- channelCount: number().nullable(),
60355
+ channelCount: number().nullable().meta(readingMeta("count")),
59641
60356
  /**
59642
60357
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59643
60358
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59760,9 +60475,151 @@ var gasCapability = {
59760
60475
  * whether persisting is worth a SQLite commit. */
59761
60476
  volatileStateFields: ["lastChangedAt"]
59762
60477
  };
60478
+ var GroupCommandsSchema = object({
60479
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60480
+ results: array(object({
60481
+ deviceId: number().int().nonnegative(),
60482
+ cap: string().min(1),
60483
+ method: string().min(1),
60484
+ ok: boolean(),
60485
+ error: string().optional(),
60486
+ /** Ms epoch of the attempt. */
60487
+ at: number()
60488
+ })) });
60489
+ var groupCommandsCapability = {
60490
+ name: "group-commands",
60491
+ scope: "device",
60492
+ deviceNative: true,
60493
+ mode: "singleton",
60494
+ deviceTypes: [
60495
+ DeviceType.Switch,
60496
+ DeviceType.Light,
60497
+ DeviceType.Fan,
60498
+ DeviceType.Cover,
60499
+ DeviceType.Valve,
60500
+ DeviceType.Lock,
60501
+ DeviceType.MediaPlayer,
60502
+ DeviceType.Thermostat,
60503
+ DeviceType.Climate
60504
+ ],
60505
+ methods: {},
60506
+ status: {
60507
+ schema: GroupCommandsSchema,
60508
+ kind: "push"
60509
+ },
60510
+ runtimeState: GroupCommandsSchema,
60511
+ /**
60512
+ * Runtime-state durability: **session** — the last attempt. A restored
60513
+ * failure would outlive the command that caused it.
60514
+ *
60515
+ * See `RuntimeStateDurability`. Enforced by
60516
+ * `scripts/check-runtime-state-durability.ts`.
60517
+ */
60518
+ durability: "session",
60519
+ history: "none"
60520
+ };
60521
+ /**
60522
+ * Whether a helper actuator group counts as active (D944): at least one
60523
+ * included member is, or every included member is. What "active" means is the
60524
+ * type's: a cover is open, a lock is locked, a media player is playing, a
60525
+ * climate device is not off. A group of lights, switches and fans reports
60526
+ * `switch.on` instead; this cap is for the types whose own state is not a
60527
+ * yes/no. Unknown members are left out (`group-status` counts them) and an
60528
+ * empty included set takes the device offline, never `active: false`.
60529
+ */
60530
+ var GroupActiveSchema = object({
60531
+ active: boolean().meta({ label: "Active" }),
60532
+ /** Ms epoch when the slice was last updated. */
60533
+ lastChangedAt: number()
60534
+ });
60535
+ var groupActiveCapability = {
60536
+ name: "group-active",
60537
+ scope: "device",
60538
+ deviceNative: true,
60539
+ mode: "singleton",
60540
+ deviceTypes: [
60541
+ DeviceType.Cover,
60542
+ DeviceType.Valve,
60543
+ DeviceType.Lock,
60544
+ DeviceType.MediaPlayer,
60545
+ DeviceType.Climate,
60546
+ DeviceType.Thermostat
60547
+ ],
60548
+ methods: {},
60549
+ status: {
60550
+ schema: GroupActiveSchema,
60551
+ kind: "push"
60552
+ },
60553
+ runtimeState: GroupActiveSchema,
60554
+ /**
60555
+ * Runtime-state durability: **session** — recomputed from the members on the
60556
+ * next source event; a restored value would outlive the members it named.
60557
+ *
60558
+ * See `RuntimeStateDurability`. Enforced by
60559
+ * `scripts/check-runtime-state-durability.ts`.
60560
+ */
60561
+ durability: "session",
60562
+ history: "none"
60563
+ };
60564
+ /**
60565
+ * How many members of a helper group answered (D944). Counts are integers,
60566
+ * not measurements: zero included is a real count, never a stand-in reading.
60567
+ * The reading itself stays on the output capability. History is none — the
60568
+ * series that later statistics read is the output cap's own history block.
60569
+ */
60570
+ var GroupStatusSchema = object({
60571
+ /** Members whose current value entered the aggregate. */
60572
+ included: number().int().nonnegative().meta(readingMeta("count", {
60573
+ precision: 0,
60574
+ label: "Reporting"
60575
+ })),
60576
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60577
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60578
+ precision: 0,
60579
+ label: "Excluded"
60580
+ })),
60581
+ /** `included` + `excluded`. */
60582
+ total: number().int().nonnegative().meta(readingMeta("count", {
60583
+ precision: 0,
60584
+ label: "Members"
60585
+ }))
60586
+ });
60587
+ var groupStatusCapability = {
60588
+ name: "group-status",
60589
+ scope: "device",
60590
+ deviceNative: true,
60591
+ mode: "singleton",
60592
+ deviceTypes: [
60593
+ DeviceType.Sensor,
60594
+ DeviceType.Switch,
60595
+ DeviceType.Light,
60596
+ DeviceType.Fan,
60597
+ DeviceType.Cover,
60598
+ DeviceType.Valve,
60599
+ DeviceType.Lock,
60600
+ DeviceType.MediaPlayer,
60601
+ DeviceType.Thermostat,
60602
+ DeviceType.Climate
60603
+ ],
60604
+ methods: {},
60605
+ status: {
60606
+ schema: GroupStatusSchema,
60607
+ kind: "push"
60608
+ },
60609
+ runtimeState: GroupStatusSchema,
60610
+ /**
60611
+ * Runtime-state durability: **session** — recomputed from members on the
60612
+ * next source event; a restored count would outlive the readings it named.
60613
+ *
60614
+ * See `RuntimeStateDurability`. Enforced by
60615
+ * `scripts/check-runtime-state-durability.ts`.
60616
+ */
60617
+ durability: "session",
60618
+ history: "none"
60619
+ };
59763
60620
  var HeatingValveStatusSchema = object({
59764
60621
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59765
- position: number().min(0).max(100).nullable(),
60622
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59766
60623
  /** Ms epoch when the slice was last updated (push or poll). */
59767
60624
  lastFetchedAt: number()
59768
60625
  });
@@ -59805,9 +60662,9 @@ var HumidifierStatusSchema = object({
59805
60662
  /** Whether the humidifier is currently on. */
59806
60663
  on: boolean(),
59807
60664
  /** Current measured relative humidity (0..100). Null when not reported. */
59808
- currentHumidity: number().min(0).max(100).nullable(),
60665
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59809
60666
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59810
- targetHumidity: number().min(0).max(100).nullable(),
60667
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59811
60668
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59812
60669
  * device has no mode surface. */
59813
60670
  mode: string().nullable(),
@@ -59817,9 +60674,9 @@ var HumidifierStatusSchema = object({
59817
60674
  * `idle` / `off`). Null when the device doesn't report it. */
59818
60675
  action: string().nullable(),
59819
60676
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59820
- minHumidity: number().nullable(),
60677
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59821
60678
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59822
- maxHumidity: number().nullable(),
60679
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59823
60680
  /** Ms epoch when the slice was last updated. */
59824
60681
  lastChangedAt: number()
59825
60682
  });
@@ -59889,22 +60746,13 @@ var humidifierCapability = {
59889
60746
  * `sourceInfo`.
59890
60747
  */
59891
60748
  var HumiditySensorStatusSchema = object({
59892
- /** Current relative humidity, 0..100. */
59893
- percent: number().min(0).max(100).meta({
59894
- unit: "%",
60749
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60750
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60751
+ precision: 0,
59895
60752
  label: "Humidity"
59896
- }),
60753
+ })),
59897
60754
  /** Ms epoch when the slice was last updated. */
59898
- lastFetchedAt: number(),
59899
- /** Live display unit from the upstream source (e.g. HA
59900
- * `attributes.unit_of_measurement`). The UI prefers this over the
59901
- * role's canonical unit. Absent → fall back to the canonical unit. */
59902
- unit: string().optional(),
59903
- /** Suggested decimal places for numeric display.
59904
- * Populated live from the upstream source when provided (e.g. HA
59905
- * `attributes.suggested_display_precision`). Falls back to
59906
- * auto-formatting when absent. */
59907
- precision: number().int().min(0).max(10).optional()
60755
+ lastFetchedAt: number()
59908
60756
  });
59909
60757
  var humiditySensorCapability = {
59910
60758
  name: "humidity-sensor",
@@ -59935,12 +60783,7 @@ var humiditySensorCapability = {
59935
60783
  } } },
59936
60784
  /** Clock fields: written, but excluded from the compare that decides
59937
60785
  * whether persisting is worth a SQLite commit. */
59938
- volatileStateFields: ["lastFetchedAt"],
59939
- customize: {
59940
- version: 1,
59941
- advanced: "derive",
59942
- targets: []
59943
- }
60786
+ volatileStateFields: ["lastFetchedAt"]
59944
60787
  };
59945
60788
  /**
59946
60789
  * Image display cap. Models a single still image exposed by an integration —
@@ -59961,7 +60804,7 @@ var ImageStatusSchema = object({
59961
60804
  * entity exposes no `entity_picture` (yet). */
59962
60805
  url: string().nullable(),
59963
60806
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59964
- lastUpdated: number().nullable()
60807
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59965
60808
  });
59966
60809
  var imageCapability = {
59967
60810
  name: "image",
@@ -60049,19 +60892,19 @@ var NormalizedRangeSchema = object({
60049
60892
  */
60050
60893
  var ImageSettingsStatusSchema = object({
60051
60894
  /** Normalized 0–100. */
60052
- brightness: number().optional(),
60895
+ brightness: number().optional().meta(readingMeta("none")),
60053
60896
  /** Normalized 0–100. */
60054
- contrast: number().optional(),
60897
+ contrast: number().optional().meta(readingMeta("none")),
60055
60898
  /** Normalized 0–100. */
60056
- saturation: number().optional(),
60899
+ saturation: number().optional().meta(readingMeta("none")),
60057
60900
  /** Normalized 0–100. */
60058
- sharpness: number().optional(),
60901
+ sharpness: number().optional().meta(readingMeta("none")),
60059
60902
  mirror: boolean().optional(),
60060
60903
  flip: boolean().optional(),
60061
60904
  rotate: ImageRotateSchema.optional(),
60062
60905
  whiteBalance: WhiteBalanceModeSchema.optional(),
60063
60906
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60064
- warmth: number().optional(),
60907
+ warmth: number().optional().meta(readingMeta("none")),
60065
60908
  exposureMode: ExposureModeSchema.optional(),
60066
60909
  backlightMode: BacklightModeSchema.optional(),
60067
60910
  lastFetchedAt: number()
@@ -60340,10 +61183,10 @@ var IntercomAbilitySchema = object({
60340
61183
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60341
61184
  codecs: array(string()),
60342
61185
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60343
- sampleRate: number(),
61186
+ sampleRate: number().meta(readingMeta("none")),
60344
61187
  duplex: _enum(["half", "full"]),
60345
61188
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60346
- maxBacklogMs: number()
61189
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60347
61190
  });
60348
61191
  var IntercomStatusSchema = object({
60349
61192
  /** True while a talk session is open. */
@@ -60570,13 +61413,13 @@ var LawnMowerControlStatusSchema = object({
60570
61413
  activity: LawnMowerActivitySchema,
60571
61414
  /** 0..100 battery percentage. Null when the device has no battery
60572
61415
  * reading. */
60573
- batteryLevel: number().min(0).max(100).nullable(),
61416
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60574
61417
  /** 0..100 mowing-completion percentage of the current task, or null when no
60575
61418
  * task is active / progress is unavailable. */
60576
- progressPercent: number().min(0).max(100).nullable(),
61419
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60577
61420
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60578
61421
  * or null when unknown. */
60579
- currentCode: number().nullable(),
61422
+ currentCode: number().nullable().meta(readingMeta("none")),
60580
61423
  /** Human label for {@link currentCode}, or null when undecodable. */
60581
61424
  currentCodeLabel: string().nullable(),
60582
61425
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61029,7 +61872,14 @@ var lockControlCapability = {
61029
61872
  }),
61030
61873
  unlock: method(object({
61031
61874
  deviceId: number().int().nonnegative(),
61032
- code: string().min(1).optional()
61875
+ code: string().min(1).optional(),
61876
+ /**
61877
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
61878
+ * with one call is refused unless the caller says it was confirmed. A
61879
+ * single lock ignores it. The refusal lives in the callee, so no
61880
+ * automation or API caller reaches the doors by skipping a dialog.
61881
+ */
61882
+ confirm: boolean().optional()
61033
61883
  }), _void(), {
61034
61884
  kind: "mutation",
61035
61885
  auth: "protected"
@@ -61066,6 +61916,44 @@ var lockControlCapability = {
61066
61916
  volatileStateFields: ["lastChangedAt"]
61067
61917
  };
61068
61918
  /**
61919
+ * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
61920
+ * so dashboards keep one series; every other instantaneous reading lands here.
61921
+ * `value` is the time integral in the source quantity's accumulated base
61922
+ * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
61923
+ */
61924
+ var MeterStatusSchema = object({
61925
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
61926
+ lastFetchedAt: number()
61927
+ });
61928
+ var meterCapability = {
61929
+ name: "meter",
61930
+ scope: "device",
61931
+ deviceNative: true,
61932
+ mode: "singleton",
61933
+ deviceTypes: [DeviceType.Sensor],
61934
+ methods: {},
61935
+ status: {
61936
+ schema: MeterStatusSchema,
61937
+ kind: "push"
61938
+ },
61939
+ runtimeState: MeterStatusSchema,
61940
+ /**
61941
+ * Runtime-state durability: **restored** — the total is a cache of the
61942
+ * history integral. A restart shows it only until the history pass
61943
+ * recomputes the open period; it is not an in-memory buffer.
61944
+ *
61945
+ * See `RuntimeStateDurability`. Enforced by
61946
+ * `scripts/check-runtime-state-durability.ts`.
61947
+ */
61948
+ durability: "restored",
61949
+ history: { fields: { value: {
61950
+ kind: "counter",
61951
+ deadband: .001,
61952
+ minIntervalMs: 6e4
61953
+ } } },
61954
+ volatileStateFields: ["lastFetchedAt"]
61955
+ };
61956
+ /**
61069
61957
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
61070
61958
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
61071
61959
  * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
@@ -61109,7 +61997,7 @@ var MediaPlayerStatusSchema = object({
61109
61997
  state: MediaPlayerStateSchema,
61110
61998
  /** Volume as 0..100 inclusive. Null when the device has no volume
61111
61999
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61112
- volumeLevel: number().min(0).max(100).nullable(),
62000
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61113
62001
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61114
62002
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61115
62003
  isMuted: boolean().nullable(),
@@ -61122,10 +62010,10 @@ var MediaPlayerStatusSchema = object({
61122
62010
  currentMedia: MediaInfoSchema.nullable(),
61123
62011
  /** Current playback position in ms. Null when not seekable or
61124
62012
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61125
- positionMs: number().int().nonnegative().nullable(),
62013
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61126
62014
  /** Total duration of the current media in ms. Null when unknown
61127
62015
  * (live stream) or nothing is playing. */
61128
- durationMs: number().int().nonnegative().nullable(),
62016
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61129
62017
  /** Shuffle toggle. Null when no shuffle surface.
61130
62018
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61131
62019
  shuffle: boolean().nullable(),
@@ -61434,7 +62322,7 @@ var motionCustomize = {
61434
62322
  eventName: "onMotionChanged"
61435
62323
  },
61436
62324
  deviceTypes: [DeviceType.Camera],
61437
- apply: "replace",
62325
+ apply: "add",
61438
62326
  applyAuthority: {
61439
62327
  kind: "camera-sensors",
61440
62328
  timeoutField: "motionCooldownMs"
@@ -61506,7 +62394,7 @@ var MotionStatusSchema = object({
61506
62394
  * it, every provider seeded it at boot, and every session left it
61507
62395
  * `detected: true` after its detach.
61508
62396
  */
61509
- autoClearAfterMs: number().nullable()
62397
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61510
62398
  });
61511
62399
  /**
61512
62400
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61543,12 +62431,18 @@ var motionCapability = {
61543
62431
  methods: {
61544
62432
  /**
61545
62433
  * Pull the current motion state synchronously. Convenience shortcut
61546
- * for consumers that don't need the full status object; equivalent
61547
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61548
- * `getStatus` once the auto-injected status surface lands in every
61549
- * consumer.
62434
+ * for consumers that don't need the full status object. A composed
62435
+ * group answers from its slice (D944): unknown throws, it is not off.
61550
62436
  */
61551
62437
  isDetected: method(object({ deviceId: number() }), boolean()) },
62438
+ /**
62439
+ * A composed motion group serves `isDetected` from `detected` (D944).
62440
+ * Without this the cap stays uncomposable (`has-commands`).
62441
+ */
62442
+ composedMethods: { isDetected: {
62443
+ kind: "slice",
62444
+ field: "detected"
62445
+ } },
61552
62446
  events: {
61553
62447
  /**
61554
62448
  * Fires every time the runner transitions a camera between
@@ -61688,7 +62582,7 @@ var MotionZoneRegionSchema = object({
61688
62582
  * the grid region(s). */
61689
62583
  var MotionZoneStatusSchema = object({
61690
62584
  enabled: boolean(),
61691
- sensitivity: number(),
62585
+ sensitivity: number().meta(readingMeta("none")),
61692
62586
  /** Grid region(s). Today exactly one `grid` shape. */
61693
62587
  regions: array(MotionZoneRegionSchema),
61694
62588
  lastFetchedAt: number()
@@ -62058,7 +62952,7 @@ var NavigationStatusSchema = object({
62058
62952
  following: boolean(),
62059
62953
  flash: boolean(),
62060
62954
  lightMode: NavigationLightModeSchema,
62061
- lightLevel: number().min(40).max(100),
62955
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62062
62956
  /** Ms epoch when the slice was last updated. */
62063
62957
  lastChangedAt: number()
62064
62958
  });
@@ -62192,13 +63086,13 @@ var NetworkLinkStatusSchema = object({
62192
63086
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62193
63087
  * SKIP a null rather than coerce it.
62194
63088
  */
62195
- signalPercent: number().min(0).max(100).nullable(),
63089
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62196
63090
  /** Raw received signal strength in dBm, when the firmware reports one. */
62197
- rssiDbm: number().optional(),
63091
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62198
63092
  /** Network name of a wireless link, when the firmware reports it. */
62199
63093
  ssid: string().optional(),
62200
63094
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62201
- lastUpdated: number()
63095
+ lastUpdated: number().meta(readingMeta("none"))
62202
63096
  });
62203
63097
  var networkLinkCapability = {
62204
63098
  name: "network-link",
@@ -62583,7 +63477,7 @@ var NotifierStatusSchema = object({
62583
63477
  lastError: string().nullable(),
62584
63478
  /** Number of deliveries currently buffered server-side (rate-limited
62585
63479
  * notifiers may queue). 0 when the provider sends synchronously. */
62586
- queueDepth: number().int().nonnegative(),
63480
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62587
63481
  /** Per-feature capability matrix — authoritative source for the
62588
63482
  * compose-form field gating. */
62589
63483
  supports: NotifierSupportsSchema
@@ -62675,11 +63569,9 @@ onSent: { data: object({
62675
63569
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62676
63570
  * for: water flow, distance, weight, frequency, signal strength, …).
62677
63571
  *
62678
- * The `unit` and `precision` fields carry the live display metadata that
62679
- * the upstream source provides with each state push (HA
62680
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62681
- * The UI reads them directly from the slice — they are the single source
62682
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63572
+ * The quantity is not on this slice. It is chosen when the device is
63573
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63574
+ * that quantity's base unit. `null` is a refused sample (D393).
62683
63575
  *
62684
63576
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62685
63577
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62688,15 +63580,8 @@ onSent: { data: object({
62688
63580
  * numeric values land here.
62689
63581
  */
62690
63582
  var NumericSensorStatusSchema = object({
62691
- value: number(),
62692
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62693
- * Populated live from the upstream source on each state push.
62694
- * Absent for unitless sensors. */
62695
- unit: string().optional(),
62696
- /** Suggested decimal places for numeric display.
62697
- * Populated live from the upstream source when provided.
62698
- * Falls back to auto-formatting when absent. */
62699
- precision: number().int().min(0).max(10).optional(),
63583
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63584
+ value: number().nullable().meta(readingMeta("device")),
62700
63585
  /** Ms epoch when the slice was last updated. */
62701
63586
  lastFetchedAt: number()
62702
63587
  });
@@ -62720,11 +63605,11 @@ var numericSensorCapability = {
62720
63605
  */
62721
63606
  durability: "restored",
62722
63607
  /**
62723
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63608
+ * History (D850): recorded series. The unit is the device quantity's base,
63609
+ * resolved when the series is read — the cap has no single unit.
62724
63610
  */
62725
63611
  history: { fields: { value: {
62726
63612
  kind: "gauge",
62727
- unitFrom: "unit",
62728
63613
  minIntervalMs: 3e4
62729
63614
  } } },
62730
63615
  /** Clock fields: written, but excluded from the compare that decides
@@ -63016,23 +63901,26 @@ var PetFeederStatusSchema = object({
63016
63901
  /** Food currently in the bowl (grams). Null when the device has not
63017
63902
  * reported a reading yet. On dual-hopper models this is the combined
63018
63903
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63019
- foodLevel: number().nullable(),
63904
+ foodLevel: number().nullable().meta(readingMeta("none")),
63020
63905
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63021
63906
  * single-hopper models. */
63022
- food1: number().nullable(),
63907
+ food1: number().nullable().meta(readingMeta("none")),
63023
63908
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63024
63909
  * single-hopper models. */
63025
- food2: number().nullable(),
63910
+ food2: number().nullable().meta(readingMeta("none")),
63026
63911
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63027
63912
  * (`device_class: problem`, on = low). True when the bowl is empty /
63028
63913
  * below the feeder's low threshold. */
63029
63914
  lowFood: boolean(),
63030
63915
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63031
63916
  * device has no battery reading. */
63032
- batteryPower: number().min(0).max(100).nullable(),
63033
- /** Days of desiccant life remaining. Null when the model has no
63034
- * desiccant sensor. */
63035
- desiccantLeftDays: number().nullable(),
63917
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63918
+ /**
63919
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63920
+ * Providers convert from the days the feeder reports. Null when the
63921
+ * model has no desiccant sensor.
63922
+ */
63923
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63036
63924
  /** True while a feed is in progress. */
63037
63925
  feeding: boolean(),
63038
63926
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63043,7 +63931,7 @@ var PetFeederStatusSchema = object({
63043
63931
  * with `errorCode` for consumers that want the raw integer. */
63044
63932
  error: string().nullable(),
63045
63933
  /** Raw device error code (0 / null = no error). */
63046
- errorCode: number().nullable(),
63934
+ errorCode: number().nullable().meta(readingMeta("none")),
63047
63935
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63048
63936
  isDualHopper: boolean(),
63049
63937
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63053,7 +63941,7 @@ var PetFeederStatusSchema = object({
63053
63941
  /** Play a chime when dispensing. */
63054
63942
  feedSound: boolean(),
63055
63943
  /** Speaker / prompt volume level (device-scaled integer). */
63056
- volume: number(),
63944
+ volume: number().meta(readingMeta("none")),
63057
63945
  /** Ms epoch when the slice was last refreshed from the cloud. */
63058
63946
  lastFetchedAt: number()
63059
63947
  });
@@ -63172,7 +64060,7 @@ var petFeederCapability = {
63172
64060
  },
63173
64061
  desiccantLeftDays: {
63174
64062
  kind: "gauge",
63175
- unit: "days"
64063
+ unit: "s"
63176
64064
  }
63177
64065
  } }
63178
64066
  };
@@ -63573,43 +64461,88 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63573
64461
  */
63574
64462
  var PowerMeterStatusSchema = object({
63575
64463
  /** Instantaneous power draw in watts. */
63576
- watts: number().optional().meta({
63577
- unit: "W",
63578
- label: "Power"
63579
- }),
64464
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63580
64465
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63581
- kwhTotal: number().optional().meta({
63582
- unit: "kWh",
63583
- label: "Energy"
63584
- }),
64466
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63585
64467
  /** Voltage in volts. */
63586
- volts: number().optional(),
64468
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
63587
64469
  /** Current in amperes. */
63588
- amps: number().optional(),
63589
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63590
- va: number().optional(),
64470
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
64471
+ /** Apparent power in volt-amperes. */
64472
+ va: number().optional().meta(readingMeta("apparent-power")),
63591
64473
  /** Power factor (0..1) if reported. */
63592
- powerFactor: number().min(0).max(1).optional(),
64474
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63593
64475
  /** Ms epoch when the slice was last updated. */
63594
- lastFetchedAt: number(),
63595
- /** Live display unit of the single metric this slice carries (e.g. HA
63596
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63597
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63598
- * unit per slice is unambiguous. The UI prefers this over the role's
63599
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63600
- unit: string().optional(),
63601
- /** Suggested decimal places for numeric display.
63602
- * Populated live from the upstream source when provided (e.g. HA
63603
- * `attributes.suggested_display_precision`). Falls back to
63604
- * auto-formatting when absent. */
63605
- precision: number().int().min(0).max(10).optional()
64476
+ lastFetchedAt: number()
63606
64477
  });
64478
+ /**
64479
+ * Loads that draw metered mains power (D951). The reading is a feature of the
64480
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
64481
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
64482
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
64483
+ * camera and the like are left out: their draw is not what anyone meters.
64484
+ */
64485
+ var POWER_METER_DEVICE_TYPES = [
64486
+ DeviceType.Sensor,
64487
+ DeviceType.Switch,
64488
+ DeviceType.Light,
64489
+ DeviceType.Fan,
64490
+ DeviceType.Climate,
64491
+ DeviceType.Thermostat,
64492
+ DeviceType.Humidifier,
64493
+ DeviceType.WaterHeater,
64494
+ DeviceType.MediaPlayer,
64495
+ DeviceType.Washer,
64496
+ DeviceType.Dryer,
64497
+ DeviceType.Dishwasher,
64498
+ DeviceType.Vacuum,
64499
+ DeviceType.LawnMower,
64500
+ DeviceType.PetFeeder
64501
+ ];
64502
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
64503
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
64504
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
64505
+ function powerTarget(id, unit) {
64506
+ return {
64507
+ id,
64508
+ cardinality: "scalar",
64509
+ vocabulary: {
64510
+ cap: "power-meter",
64511
+ stateField: id
64512
+ },
64513
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
64514
+ apply: "add",
64515
+ path: id,
64516
+ sources: [{
64517
+ kind: "numeric",
64518
+ requiredUnit: unit,
64519
+ transforms: [{ kind: "identity" }]
64520
+ }],
64521
+ companions: [{
64522
+ kind: "clock",
64523
+ path: "lastFetchedAt",
64524
+ when: "always"
64525
+ }],
64526
+ sourceReport: { status: "assignment-status" },
64527
+ advancedExpression: true
64528
+ };
64529
+ }
64530
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
64531
+ var powerMeterCustomize = {
64532
+ version: 1,
64533
+ targets: [
64534
+ powerTarget("watts", "W"),
64535
+ powerTarget("kwhTotal", "kWh"),
64536
+ powerTarget("volts", "V"),
64537
+ powerTarget("amps", "A")
64538
+ ]
64539
+ };
63607
64540
  var powerMeterCapability = {
63608
64541
  name: "power-meter",
63609
64542
  scope: "device",
63610
64543
  deviceNative: true,
63611
64544
  mode: "singleton",
63612
- deviceTypes: [DeviceType.Sensor],
64545
+ deviceTypes: POWER_METER_DEVICE_TYPES,
63613
64546
  methods: {},
63614
64547
  status: {
63615
64548
  schema: PowerMeterStatusSchema,
@@ -63667,11 +64600,7 @@ var powerMeterCapability = {
63667
64600
  /** Clock fields: written, but excluded from the compare that decides
63668
64601
  * whether persisting is worth a SQLite commit. */
63669
64602
  volatileStateFields: ["lastFetchedAt"],
63670
- customize: {
63671
- version: 1,
63672
- advanced: "derive",
63673
- targets: []
63674
- }
64603
+ customize: powerMeterCustomize
63675
64604
  };
63676
64605
  /**
63677
64606
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63691,11 +64620,11 @@ var powerMeterCapability = {
63691
64620
  */
63692
64621
  var GpsLocationSchema = object({
63693
64622
  /** Latitude in decimal degrees, -90..90. */
63694
- latitude: number().min(-90).max(90),
64623
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63695
64624
  /** Longitude in decimal degrees, -180..180. */
63696
- longitude: number().min(-180).max(180),
64625
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63697
64626
  /** Reported accuracy in meters (lower = better). */
63698
- accuracyMeters: number().nonnegative()
64627
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63699
64628
  });
63700
64629
  var PresenceStatusSchema = object({
63701
64630
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63707,7 +64636,7 @@ var PresenceStatusSchema = object({
63707
64636
  gps: GpsLocationSchema.nullable(),
63708
64637
  /** Optional battery level of the tracking device (0..100). Null
63709
64638
  * when not reported. */
63710
- batteryPercent: number().min(0).max(100).nullable(),
64639
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63711
64640
  /** Ms epoch when the slice was last updated. */
63712
64641
  lastChangedAt: number()
63713
64642
  });
@@ -63757,19 +64686,10 @@ var presenceCapability = {
63757
64686
  * `sourceInfo`.
63758
64687
  */
63759
64688
  var PressureSensorStatusSchema = object({
63760
- /** Current pressure in hPa. */
63761
- hpa: number(),
64689
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64690
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63762
64691
  /** Ms epoch when the slice was last updated. */
63763
- lastFetchedAt: number(),
63764
- /** Live display unit from the upstream source (e.g. HA
63765
- * `attributes.unit_of_measurement`). The UI prefers this over the
63766
- * role's canonical unit. Absent → fall back to the canonical unit. */
63767
- unit: string().optional(),
63768
- /** Suggested decimal places for numeric display.
63769
- * Populated live from the upstream source when provided (e.g. HA
63770
- * `attributes.suggested_display_precision`). Falls back to
63771
- * auto-formatting when absent. */
63772
- precision: number().int().min(0).max(10).optional()
64692
+ lastFetchedAt: number()
63773
64693
  });
63774
64694
  var pressureSensorCapability = {
63775
64695
  name: "pressure-sensor",
@@ -64183,8 +65103,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64183
65103
  }), _void(), { kind: "mutation" });
64184
65104
  var PtzAutotrackSettingsSchema = object({
64185
65105
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64186
- stopDelaySeconds: number().int().min(0).max(300),
64187
- disappearDelaySeconds: number().int().min(0).max(300)
65106
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65107
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64188
65108
  });
64189
65109
  /**
64190
65110
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64338,7 +65258,7 @@ var RunningStateStatusSchema = object({
64338
65258
  /** null = unknown — never coerced to false (D393). */
64339
65259
  running: boolean().nullable(),
64340
65260
  /** Epoch ms the current boolean began; null while `running` is null. */
64341
- since: number().nullable(),
65261
+ since: number().nullable().meta(readingMeta("none")),
64342
65262
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64343
65263
  phase: string().nullable(),
64344
65264
  /** null until a reading has been published (D393). */
@@ -64353,7 +65273,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64353
65273
  /** Generic Customize target for the running boolean (D936). */
64354
65274
  var runningCustomize = {
64355
65275
  version: 1,
64356
- advanced: "derive",
64357
65276
  targets: [{
64358
65277
  id: "running",
64359
65278
  cardinality: "scalar",
@@ -64362,6 +65281,8 @@ var runningCustomize = {
64362
65281
  stateField: "running"
64363
65282
  },
64364
65283
  deviceTypes: [
65284
+ DeviceType.Washer,
65285
+ DeviceType.Dryer,
64365
65286
  DeviceType.Dishwasher,
64366
65287
  DeviceType.Vacuum,
64367
65288
  DeviceType.LawnMower,
@@ -64373,7 +65294,7 @@ var runningCustomize = {
64373
65294
  DeviceType.Thermostat,
64374
65295
  DeviceType.PetFeeder
64375
65296
  ],
64376
- apply: "add-or-replace",
65297
+ apply: "add",
64377
65298
  path: "running",
64378
65299
  sources: [
64379
65300
  {
@@ -65637,9 +66558,9 @@ var RecordTriggerSchema = _enum([
65637
66558
  */
65638
66559
  var RecordWindowSchema = object({
65639
66560
  trigger: RecordTriggerSchema,
65640
- day: number().int().min(0).max(6),
65641
- startMinute: number().int().min(0).max(1439),
65642
- endMinute: number().int().min(1).max(1440)
66561
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66562
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66563
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65643
66564
  });
65644
66565
  /** Status of one physical volume, as the camera itself describes it. */
65645
66566
  var OnboardStorageVolumeSchema = object({
@@ -65715,10 +66636,10 @@ var RecordingOnboardStatusSchema = object({
65715
66636
  /** Master "record to the card at all" switch. */
65716
66637
  enabled: boolean().nullable(),
65717
66638
  overwriteWhenFull: boolean().nullable(),
65718
- preRecordSec: number().nullable(),
65719
- postRecordSec: number().nullable(),
66639
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66640
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65720
66641
  /** Length of one recorded file, in minutes. */
65721
- segmentMinutes: number().nullable(),
66642
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65722
66643
  /** The primary track's weekly windows, flattened. */
65723
66644
  windows: array(RecordWindowSchema),
65724
66645
  /**
@@ -65734,7 +66655,7 @@ var RecordingOnboardStatusSchema = object({
65734
66655
  * provider reporting a non-zero count also reports the schedule as not
65735
66656
  * writable.
65736
66657
  */
65737
- unreadableWindows: number(),
66658
+ unreadableWindows: number().meta(readingMeta("count")),
65738
66659
  /**
65739
66660
  * The camera is scheduled to record and has NO usable storage.
65740
66661
  *
@@ -66579,7 +67500,7 @@ var CamStreamDescriptorSchema = object({
66579
67500
  url: string().optional(),
66580
67501
  codec: string().optional(),
66581
67502
  resolution: CamStreamResolutionSchema.optional(),
66582
- fps: number().positive().optional(),
67503
+ fps: number().positive().optional().meta(readingMeta("rate")),
66583
67504
  label: string().optional(),
66584
67505
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66585
67506
  deviceFeatures: array(string()).optional(),
@@ -66666,18 +67587,18 @@ var StreamProfileSchema = _enum([
66666
67587
  ]);
66667
67588
  /** The current encoding config of one stream profile. */
66668
67589
  var StreamProfileConfigSchema = object({
66669
- width: number(),
66670
- height: number(),
67590
+ width: number().meta(readingMeta("none")),
67591
+ height: number().meta(readingMeta("none")),
66671
67592
  codec: _enum(["h264", "h265"]),
66672
- framerate: number(),
66673
- bitrate: number(),
67593
+ framerate: number().meta(readingMeta("rate")),
67594
+ bitrate: number().meta(readingMeta("bitrate")),
66674
67595
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66675
67596
  encoderProfile: _enum([
66676
67597
  "high",
66677
67598
  "main",
66678
67599
  "baseline"
66679
67600
  ]).optional(),
66680
- gop: number().optional(),
67601
+ gop: number().optional().meta(readingMeta("none")),
66681
67602
  /**
66682
67603
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66683
67604
  *
@@ -66851,7 +67772,8 @@ var switchCapability = {
66851
67772
  deviceTypes: [
66852
67773
  DeviceType.Switch,
66853
67774
  DeviceType.Siren,
66854
- DeviceType.Light
67775
+ DeviceType.Light,
67776
+ DeviceType.Fan
66855
67777
  ],
66856
67778
  methods: { setState: method(object({
66857
67779
  deviceId: number(),
@@ -66898,12 +67820,7 @@ var switchCapability = {
66898
67820
  label: "On",
66899
67821
  kind: "boolean"
66900
67822
  }
66901
- }] },
66902
- customize: {
66903
- version: 1,
66904
- advanced: "derive",
66905
- targets: []
66906
- }
67823
+ }] }
66907
67824
  };
66908
67825
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66909
67826
  function hasControlCharacter(value) {
@@ -67486,35 +68403,21 @@ var tamperCapability = {
67486
68403
  * entries with `device_class: temperature` and any future native
67487
68404
  * thermometer.
67488
68405
  *
67489
- * Unit normalisation: providers convert to Celsius before storing.
67490
- * The slice value is always Celsius so cross-cap aggregators
67491
- * (climate-control's `currentTemp`, energy analytics) can compose
67492
- * without per-source unit fixups. The canonical display unit (`°C`) is
67493
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67494
- * `sourceInfo`.
68406
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68407
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67495
68408
  *
67496
68409
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67497
68410
  * frozen feed (provider hung) distinct from a "temperature hasn't
67498
68411
  * changed" steady state.
67499
68412
  */
67500
68413
  var TemperatureSensorStatusSchema = object({
67501
- /** Current temperature in Celsius. */
67502
- celsius: number().meta({
67503
- unit: "°C",
68414
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68415
+ celsius: number().nullable().meta(readingMeta("temperature", {
68416
+ precision: 1,
67504
68417
  label: "Temperature"
67505
- }),
68418
+ })),
67506
68419
  /** Ms epoch when the slice was last updated (push or poll). */
67507
- lastFetchedAt: number(),
67508
- /** Live display unit from the upstream source (e.g. HA
67509
- * `attributes.unit_of_measurement`). The UI prefers this over the
67510
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67511
- * Absent → the UI falls back to the role's canonical unit. */
67512
- unit: string().optional(),
67513
- /** Suggested decimal places for numeric display.
67514
- * Populated live from the upstream source when provided (e.g. HA
67515
- * `attributes.suggested_display_precision`). Falls back to
67516
- * auto-formatting when absent. */
67517
- precision: number().int().min(0).max(10).optional()
68420
+ lastFetchedAt: number()
67518
68421
  });
67519
68422
  var temperatureSensorCapability = {
67520
68423
  name: "temperature-sensor",
@@ -67545,12 +68448,7 @@ var temperatureSensorCapability = {
67545
68448
  } } },
67546
68449
  /** Clock fields: written, but excluded from the compare that decides
67547
68450
  * whether persisting is worth a SQLite commit. */
67548
- volatileStateFields: ["lastFetchedAt"],
67549
- customize: {
67550
- version: 1,
67551
- advanced: "derive",
67552
- targets: []
67553
- }
68451
+ volatileStateFields: ["lastFetchedAt"]
67554
68452
  };
67555
68453
  /**
67556
68454
  * toast — system-scoped singleton capability that streams toast
@@ -67833,16 +68731,16 @@ var VacuumControlStatusSchema = object({
67833
68731
  ]),
67834
68732
  /** 0..100 battery percentage. Null when the device has no battery
67835
68733
  * reading. */
67836
- batteryLevel: number().min(0).max(100).nullable(),
68734
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67837
68735
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67838
68736
  * the vacuum has no speed control. */
67839
68737
  fanSpeed: string().nullable(),
67840
68738
  /** Speed tokens the hardware accepts — drives the UI selector. */
67841
68739
  availableFanSpeeds: array(string()),
67842
68740
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67843
- progressPercent: number().min(0).max(100).nullable(),
68741
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67844
68742
  /** Current error code (0 / null = no error). */
67845
- errorCode: number().nullable(),
68743
+ errorCode: number().nullable().meta(readingMeta("none")),
67846
68744
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67847
68745
  errorLabel: string().nullable(),
67848
68746
  /** Ms epoch when the slice was last updated. */
@@ -67920,7 +68818,7 @@ var ValveStatusSchema = object({
67920
68818
  ]),
67921
68819
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67922
68820
  * intermediate position surface. */
67923
- position: number().min(0).max(100).nullable(),
68821
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67924
68822
  /** Ms epoch when the slice was last updated. */
67925
68823
  lastChangedAt: number()
67926
68824
  });
@@ -68028,9 +68926,9 @@ var vibrationCapability = {
68028
68926
  */
68029
68927
  var WaterHeaterStatusSchema = object({
68030
68928
  /** Current measured temperature. Null when not reported. */
68031
- currentTemp: number().nullable(),
68929
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68032
68930
  /** Target temperature setpoint. Null when no setpoint surface. */
68033
- targetTemp: number().nullable(),
68931
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68034
68932
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68035
68933
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68036
68934
  * device reports an unknown state. */
@@ -68041,9 +68939,9 @@ var WaterHeaterStatusSchema = object({
68041
68939
  * has no away surface. */
68042
68940
  away: boolean().nullable(),
68043
68941
  /** HA `min_temp` attribute. Null when not reported. */
68044
- minTemp: number().nullable(),
68942
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68045
68943
  /** HA `max_temp` attribute. Null when not reported. */
68046
- maxTemp: number().nullable(),
68944
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68047
68945
  /** Ms epoch when the slice was last updated. */
68048
68946
  lastChangedAt: number()
68049
68947
  });
@@ -68128,25 +69026,18 @@ var waterHeaterCapability = {
68128
69026
  * Forecast deferred — current conditions only for v1.
68129
69027
  */
68130
69028
  var WeatherStatusSchema = object({
68131
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68132
- * when no condition has been reported yet. */
69029
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68133
69030
  condition: string().nullable(),
68134
- /** Current temperature in the reported unit. Null when not provided. */
68135
- temperature: number().nullable(),
68136
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68137
- temperatureUnit: string().nullable(),
69031
+ /** Current temperature in Celsius. Null when not provided or refused. */
69032
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68138
69033
  /** Relative humidity (0..100). Null when not provided. */
68139
- humidity: number().min(0).max(100).nullable(),
68140
- /** Barometric pressure in the reported unit. Null when not provided. */
68141
- pressure: number().nullable(),
68142
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68143
- pressureUnit: string().nullable(),
68144
- /** Wind speed in the reported unit. Null when not provided. */
68145
- windSpeed: number().nullable(),
68146
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68147
- windSpeedUnit: string().nullable(),
69034
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
69035
+ /** Barometric pressure in hPa. Null when not provided or refused. */
69036
+ pressure: number().nullable().meta(readingMeta("pressure")),
69037
+ /** Wind speed in metres per second. Null when not provided or refused. */
69038
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68148
69039
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68149
- windBearing: number().nullable(),
69040
+ windBearing: number().nullable().meta(readingMeta("angle")),
68150
69041
  /** Ms epoch when the slice was last updated. */
68151
69042
  lastFetchedAt: number()
68152
69043
  });
@@ -68174,12 +69065,12 @@ var weatherCapability = {
68174
69065
  */
68175
69066
  durability: "session",
68176
69067
  /**
68177
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69068
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68178
69069
  */
68179
69070
  history: { fields: {
68180
69071
  temperature: {
68181
69072
  kind: "gauge",
68182
- unitFrom: "temperatureUnit",
69073
+ unit: "°C",
68183
69074
  deadband: .2
68184
69075
  },
68185
69076
  humidity: {
@@ -68189,12 +69080,12 @@ var weatherCapability = {
68189
69080
  },
68190
69081
  pressure: {
68191
69082
  kind: "gauge",
68192
- unitFrom: "pressureUnit",
69083
+ unit: "hPa",
68193
69084
  deadband: .5
68194
69085
  },
68195
69086
  windSpeed: {
68196
69087
  kind: "gauge",
68197
- unitFrom: "windSpeedUnit",
69088
+ unit: "m/s",
68198
69089
  deadband: .5
68199
69090
  }
68200
69091
  } }
@@ -69095,8 +69986,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
69095
69986
  kind: "mutation",
69096
69987
  auth: "admin"
69097
69988
  });
69989
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
69098
69990
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69099
- 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;
69991
+ 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;
69100
69992
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
69101
69993
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
69102
69994
  new Set(CAP_NAMES);
@@ -69425,6 +70317,9 @@ var DEVICE_LOCAL_STATE_CAPS = {
69425
70317
  featureProbe: featureProbeCapability,
69426
70318
  flood: floodCapability,
69427
70319
  gas: gasCapability,
70320
+ groupActive: groupActiveCapability,
70321
+ groupCommands: groupCommandsCapability,
70322
+ groupStatus: groupStatusCapability,
69428
70323
  heatingValve: heatingValveCapability,
69429
70324
  humidifier: humidifierCapability,
69430
70325
  humiditySensor: humiditySensorCapability,
@@ -69434,6 +70329,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
69434
70329
  lawnMowerControl: lawnMowerControlCapability,
69435
70330
  lockControl: lockControlCapability,
69436
70331
  mediaPlayer: mediaPlayerCapability,
70332
+ meter: meterCapability,
69437
70333
  motion: motionCapability,
69438
70334
  motionTrigger: motionTriggerCapability,
69439
70335
  motionZones: motionZonesCapability,
@@ -70693,7 +71589,7 @@ var BaseDeviceProvider = class extends BaseAddon {
70693
71589
  return toDeviceSummary(device, this.addonId);
70694
71590
  }
70695
71591
  };
70696
- 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;
71592
+ 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;
70697
71593
  new Set(Object.values(DeviceType));
70698
71594
  Object.freeze({
70699
71595
  "accessories.getStatus": {
@@ -70801,6 +71697,13 @@ Object.freeze({
70801
71697
  access: "view",
70802
71698
  role: "admin"
70803
71699
  },
71700
+ "addons.getPackageDocs": {
71701
+ capName: "addons",
71702
+ capScope: "system",
71703
+ addonId: null,
71704
+ access: "view",
71705
+ role: "admin"
71706
+ },
70804
71707
  "addons.getVersions": {
70805
71708
  capName: "addons",
70806
71709
  capScope: "system",
@@ -73909,6 +74812,27 @@ Object.freeze({
73909
74812
  access: "view",
73910
74813
  role: "viewer"
73911
74814
  },
74815
+ "groupActive.getStatus": {
74816
+ capName: "group-active",
74817
+ capScope: "device",
74818
+ addonId: null,
74819
+ access: "view",
74820
+ role: "viewer"
74821
+ },
74822
+ "groupCommands.getStatus": {
74823
+ capName: "group-commands",
74824
+ capScope: "device",
74825
+ addonId: null,
74826
+ access: "view",
74827
+ role: "viewer"
74828
+ },
74829
+ "groupStatus.getStatus": {
74830
+ capName: "group-status",
74831
+ capScope: "device",
74832
+ addonId: null,
74833
+ access: "view",
74834
+ role: "viewer"
74835
+ },
73912
74836
  "heatingValve.getStatus": {
73913
74837
  capName: "heating-valve",
73914
74838
  capScope: "device",
@@ -74861,6 +75785,13 @@ Object.freeze({
74861
75785
  access: "create",
74862
75786
  role: "admin"
74863
75787
  },
75788
+ "meter.getStatus": {
75789
+ capName: "meter",
75790
+ capScope: "device",
75791
+ addonId: null,
75792
+ access: "view",
75793
+ role: "viewer"
75794
+ },
74864
75795
  "metricsProvider.collectSnapshot": {
74865
75796
  capName: "metrics-provider",
74866
75797
  capScope: "system",
@@ -82959,7 +83890,39 @@ var APPLIANCE_CAP_VOCABULARY = {
82959
83890
  phrase: t("When {device} finishes", "Quando {device} finisce"),
82960
83891
  icon: "square"
82961
83892
  }
82962
- }
83893
+ },
83894
+ overrides: { byType: {
83895
+ [DeviceType.Washer]: {
83896
+ state: { running: {
83897
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
83898
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
83899
+ } },
83900
+ events: {
83901
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
83902
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
83903
+ }
83904
+ },
83905
+ [DeviceType.Dryer]: {
83906
+ state: { running: {
83907
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
83908
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
83909
+ } },
83910
+ events: {
83911
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
83912
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
83913
+ }
83914
+ },
83915
+ [DeviceType.Dishwasher]: {
83916
+ state: { running: {
83917
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
83918
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
83919
+ } },
83920
+ events: {
83921
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
83922
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
83923
+ }
83924
+ }
83925
+ } }
82963
83926
  },
82964
83927
  update: {
82965
83928
  scope: "device",
@@ -83731,6 +84694,41 @@ var SENSOR_CAP_VOCABULARY = {
83731
84694
  icon: "flame"
83732
84695
  }]
83733
84696
  },
84697
+ "group-active": {
84698
+ scope: "device",
84699
+ noun: t("Group state", "Stato del gruppo")
84700
+ },
84701
+ "group-commands": {
84702
+ scope: "device",
84703
+ noun: t("Group command", "Comando di gruppo")
84704
+ },
84705
+ "group-status": {
84706
+ scope: "device",
84707
+ noun: t("Group", "Gruppo"),
84708
+ state: [
84709
+ {
84710
+ kind: "numeric",
84711
+ field: "included",
84712
+ quantity: t("the members reporting", "i membri che rispondono"),
84713
+ label: t("Reporting", "Rispondono"),
84714
+ icon: "users"
84715
+ },
84716
+ {
84717
+ kind: "numeric",
84718
+ field: "excluded",
84719
+ quantity: t("the members left out", "i membri esclusi"),
84720
+ label: t("Excluded", "Esclusi"),
84721
+ icon: "users"
84722
+ },
84723
+ {
84724
+ kind: "numeric",
84725
+ field: "total",
84726
+ quantity: t("the member count", "il numero di membri"),
84727
+ label: t("Members", "Membri"),
84728
+ icon: "sigma"
84729
+ }
84730
+ ]
84731
+ },
83734
84732
  "humidity-sensor": {
83735
84733
  scope: "device",
83736
84734
  noun: t("Humidity", "Umidità"),
@@ -83756,6 +84754,17 @@ var SENSOR_CAP_VOCABULARY = {
83756
84754
  }],
83757
84755
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
83758
84756
  },
84757
+ meter: {
84758
+ scope: "device",
84759
+ noun: t("Meter", "Contatore"),
84760
+ state: [{
84761
+ kind: "numeric",
84762
+ field: "value",
84763
+ quantity: t("the total", "il totale"),
84764
+ label: t("Total", "Totale"),
84765
+ icon: "gauge"
84766
+ }]
84767
+ },
83759
84768
  "numeric-sensor": {
83760
84769
  scope: "device",
83761
84770
  noun: t("Sensor", "Sensore"),
@@ -83764,7 +84773,6 @@ var SENSOR_CAP_VOCABULARY = {
83764
84773
  field: "value",
83765
84774
  quantity: t("the reading", "il valore"),
83766
84775
  label: t("Reading", "Valore"),
83767
- unitField: "unit",
83768
84776
  icon: "gauge"
83769
84777
  }]
83770
84778
  },
@@ -83900,7 +84908,7 @@ var SENSOR_CAP_VOCABULARY = {
83900
84908
  field: "temperature",
83901
84909
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83902
84910
  label: t("Temperature", "Temperatura"),
83903
- unitField: "temperatureUnit",
84911
+ unit: "°C",
83904
84912
  icon: "thermometer"
83905
84913
  },
83906
84914
  {
@@ -83916,7 +84924,7 @@ var SENSOR_CAP_VOCABULARY = {
83916
84924
  field: "windSpeed",
83917
84925
  quantity: t("the wind speed", "la velocità del vento"),
83918
84926
  label: t("Wind", "Vento"),
83919
- unitField: "windSpeedUnit",
84927
+ unit: "m/s",
83920
84928
  icon: "wind"
83921
84929
  },
83922
84930
  {
@@ -83924,7 +84932,7 @@ var SENSOR_CAP_VOCABULARY = {
83924
84932
  field: "pressure",
83925
84933
  quantity: t("the air pressure", "la pressione atmosferica"),
83926
84934
  label: t("Pressure", "Pressione"),
83927
- unitField: "pressureUnit",
84935
+ unit: "hPa",
83928
84936
  icon: "gauge"
83929
84937
  }
83930
84938
  ]
@@ -84229,8 +85237,47 @@ function projectAction(cap, action) {
84229
85237
  };
84230
85238
  }
84231
85239
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
84232
- 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;
85240
+ 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;
84233
85241
  DeviceFeature.BatteryOperated;
85242
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
85243
+ 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.";
85244
+ 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.";
85245
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
85246
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
85247
+ 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.";
85248
+ function withheld(reason) {
85249
+ return {
85250
+ [DeviceType.Washer]: {
85251
+ exported: false,
85252
+ reason
85253
+ },
85254
+ [DeviceType.Dryer]: {
85255
+ exported: false,
85256
+ reason
85257
+ },
85258
+ [DeviceType.Dishwasher]: {
85259
+ exported: false,
85260
+ reason
85261
+ }
85262
+ };
85263
+ }
85264
+ function offered(received) {
85265
+ return {
85266
+ [DeviceType.Washer]: {
85267
+ exported: true,
85268
+ as: received
85269
+ },
85270
+ [DeviceType.Dryer]: {
85271
+ exported: true,
85272
+ as: received
85273
+ },
85274
+ [DeviceType.Dishwasher]: {
85275
+ exported: true,
85276
+ as: received
85277
+ }
85278
+ };
85279
+ }
85280
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
84234
85281
  Object.freeze({
84235
85282
  "accessories.getStatus": [{
84236
85283
  name: "deviceId",
@@ -85197,6 +86244,21 @@ Object.freeze({
85197
86244
  form: "single",
85198
86245
  optional: false
85199
86246
  }],
86247
+ "groupActive.getStatus": [{
86248
+ name: "deviceId",
86249
+ form: "single",
86250
+ optional: false
86251
+ }],
86252
+ "groupCommands.getStatus": [{
86253
+ name: "deviceId",
86254
+ form: "single",
86255
+ optional: false
86256
+ }],
86257
+ "groupStatus.getStatus": [{
86258
+ name: "deviceId",
86259
+ form: "single",
86260
+ optional: false
86261
+ }],
85200
86262
  "heatingValve.getStatus": [{
85201
86263
  name: "deviceId",
85202
86264
  form: "single",
@@ -85397,6 +86459,11 @@ Object.freeze({
85397
86459
  form: "single",
85398
86460
  optional: false
85399
86461
  }],
86462
+ "meter.getStatus": [{
86463
+ name: "deviceId",
86464
+ form: "single",
86465
+ optional: false
86466
+ }],
85400
86467
  "motion.getStatus": [{
85401
86468
  name: "deviceId",
85402
86469
  form: "single",
@@ -87004,7 +88071,6 @@ Object.freeze({
87004
88071
  "network-access": "ingress",
87005
88072
  "smtp-provider": "email"
87006
88073
  });
87007
- DeviceType.Camera;
87008
88074
  /**
87009
88075
  * A saved Customize assignment (D936). The schema is the type: callers use
87010
88076
  * `z.infer`, they do not restate the shape.
@@ -87181,6 +88247,8 @@ new Map(Object.entries({
87181
88247
  "detection-blind": "device-events",
87182
88248
  "motion-noisy": "device-events",
87183
88249
  "motion-calm": "device-events",
88250
+ "package-llm-silent": "device-events",
88251
+ "package-llm-answered": "device-events",
87184
88252
  "recording-stalled": "device-events",
87185
88253
  "recording-healthy": "device-events",
87186
88254
  "recording-degraded": "device-events",
@@ -87264,6 +88332,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87264
88332
  label: "Motion without a subject",
87265
88333
  kinds: ["motion-noisy", "motion-calm"]
87266
88334
  },
88335
+ {
88336
+ id: "package-llm",
88337
+ family: "device-events",
88338
+ label: "Package confirm",
88339
+ kinds: ["package-llm-silent", "package-llm-answered"]
88340
+ },
87267
88341
  {
87268
88342
  id: "recording",
87269
88343
  family: "device-events",
@@ -87445,6 +88519,8 @@ var DEVICE_EVENTS = [
87445
88519
  "detection-blind",
87446
88520
  "motion-noisy",
87447
88521
  "motion-calm",
88522
+ "package-llm-silent",
88523
+ "package-llm-answered",
87448
88524
  "recording-stalled",
87449
88525
  "recording-healthy",
87450
88526
  "recording-degraded",
@@ -87713,6 +88789,15 @@ new Map([
87713
88789
  snapshotLabel: "Cameras with constant motion now",
87714
88790
  snapshotSection: null
87715
88791
  },
88792
+ {
88793
+ id: "package-llm-confirm",
88794
+ family: "device-events",
88795
+ label: "Package confirm (not answering / answering again)",
88796
+ bad: ["package-llm-silent"],
88797
+ recovery: ["package-llm-answered"],
88798
+ snapshotLabel: "Cameras whose package confirm is not answering",
88799
+ snapshotSection: null
88800
+ },
87716
88801
  {
87717
88802
  id: "recording-health",
87718
88803
  family: "device-events",