@camstack/addon-terminal 0.1.153 → 0.1.155

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 node_module = require("node:module");
31
31
  let sharp = require("sharp");
32
32
  sharp = __toESM(sharp);
33
33
  let node_http = require("node:http");
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. */
@@ -8625,6 +8726,9 @@ var CAP_NAMES = Object.freeze([
8625
8726
  "filesystem-browse",
8626
8727
  "flood",
8627
8728
  "gas",
8729
+ "group-active",
8730
+ "group-commands",
8731
+ "group-status",
8628
8732
  "heating-valve",
8629
8733
  "humidifier",
8630
8734
  "humidity-sensor",
@@ -8645,6 +8749,7 @@ var CAP_NAMES = Object.freeze([
8645
8749
  "login-method",
8646
8750
  "media-player",
8647
8751
  "mesh-network",
8752
+ "meter",
8648
8753
  "metrics-provider",
8649
8754
  "model-convert",
8650
8755
  "model-distributor",
@@ -8754,14 +8859,6 @@ Object.freeze({
8754
8859
  {
8755
8860
  "path": "aqi",
8756
8861
  "valueType": "number"
8757
- },
8758
- {
8759
- "path": "unit",
8760
- "valueType": "string"
8761
- },
8762
- {
8763
- "path": "precision",
8764
- "valueType": "number"
8765
8862
  }
8766
8863
  ],
8767
8864
  "alarm-panel": [
@@ -8797,20 +8894,10 @@ Object.freeze({
8797
8894
  "valueType": "boolean"
8798
8895
  }
8799
8896
  ],
8800
- "ambient-light-sensor": [
8801
- {
8802
- "path": "lux",
8803
- "valueType": "number"
8804
- },
8805
- {
8806
- "path": "unit",
8807
- "valueType": "string"
8808
- },
8809
- {
8810
- "path": "precision",
8811
- "valueType": "number"
8812
- }
8813
- ],
8897
+ "ambient-light-sensor": [{
8898
+ "path": "lux",
8899
+ "valueType": "number"
8900
+ }],
8814
8901
  "audio-metrics": [
8815
8902
  {
8816
8903
  "path": "ts",
@@ -8890,6 +8977,10 @@ Object.freeze({
8890
8977
  "path": "lastUpdated",
8891
8978
  "valueType": "number"
8892
8979
  },
8980
+ {
8981
+ "path": "volts",
8982
+ "valueType": "number"
8983
+ },
8893
8984
  {
8894
8985
  "path": "lastContactAt",
8895
8986
  "valueType": "number"
@@ -9132,10 +9223,6 @@ Object.freeze({
9132
9223
  "datetime"
9133
9224
  ]
9134
9225
  },
9135
- {
9136
- "path": "unit",
9137
- "valueType": "string"
9138
- },
9139
9226
  {
9140
9227
  "path": "min",
9141
9228
  "valueType": "number"
@@ -9147,19 +9234,6 @@ Object.freeze({
9147
9234
  {
9148
9235
  "path": "step",
9149
9236
  "valueType": "number"
9150
- },
9151
- {
9152
- "path": "precision",
9153
- "valueType": "number"
9154
- },
9155
- {
9156
- "path": "format",
9157
- "valueType": "enum",
9158
- "enumValues": [
9159
- "date",
9160
- "time",
9161
- "datetime"
9162
- ]
9163
9237
  }
9164
9238
  ],
9165
9239
  "cover": [
@@ -9400,13 +9474,8 @@ Object.freeze({
9400
9474
  "path": "value",
9401
9475
  "valueType": "string"
9402
9476
  }, {
9403
- "path": "format",
9404
- "valueType": "enum",
9405
- "enumValues": [
9406
- "date",
9407
- "time",
9408
- "datetime"
9409
- ]
9477
+ "path": "timestamp",
9478
+ "valueType": "boolean"
9410
9479
  }],
9411
9480
  "event-emitter": [
9412
9481
  {
@@ -9479,6 +9548,24 @@ Object.freeze({
9479
9548
  "path": "detected",
9480
9549
  "valueType": "boolean"
9481
9550
  }],
9551
+ "group-active": [{
9552
+ "path": "active",
9553
+ "valueType": "boolean"
9554
+ }],
9555
+ "group-status": [
9556
+ {
9557
+ "path": "included",
9558
+ "valueType": "number"
9559
+ },
9560
+ {
9561
+ "path": "excluded",
9562
+ "valueType": "number"
9563
+ },
9564
+ {
9565
+ "path": "total",
9566
+ "valueType": "number"
9567
+ }
9568
+ ],
9482
9569
  "heating-valve": [{
9483
9570
  "path": "position",
9484
9571
  "valueType": "number"
@@ -9513,20 +9600,10 @@ Object.freeze({
9513
9600
  "valueType": "number"
9514
9601
  }
9515
9602
  ],
9516
- "humidity-sensor": [
9517
- {
9518
- "path": "percent",
9519
- "valueType": "number"
9520
- },
9521
- {
9522
- "path": "unit",
9523
- "valueType": "string"
9524
- },
9525
- {
9526
- "path": "precision",
9527
- "valueType": "number"
9528
- }
9529
- ],
9603
+ "humidity-sensor": [{
9604
+ "path": "percent",
9605
+ "valueType": "number"
9606
+ }],
9530
9607
  "image": [{
9531
9608
  "path": "url",
9532
9609
  "valueType": "string"
@@ -9734,6 +9811,10 @@ Object.freeze({
9734
9811
  ]
9735
9812
  }
9736
9813
  ],
9814
+ "meter": [{
9815
+ "path": "value",
9816
+ "valueType": "number"
9817
+ }],
9737
9818
  "motion": [
9738
9819
  {
9739
9820
  "path": "detected",
@@ -9865,20 +9946,10 @@ Object.freeze({
9865
9946
  "valueType": "boolean"
9866
9947
  }
9867
9948
  ],
9868
- "numeric-sensor": [
9869
- {
9870
- "path": "value",
9871
- "valueType": "number"
9872
- },
9873
- {
9874
- "path": "unit",
9875
- "valueType": "string"
9876
- },
9877
- {
9878
- "path": "precision",
9879
- "valueType": "number"
9880
- }
9881
- ],
9949
+ "numeric-sensor": [{
9950
+ "path": "value",
9951
+ "valueType": "number"
9952
+ }],
9882
9953
  "pet-feeder": [
9883
9954
  {
9884
9955
  "path": "foodLevel",
@@ -9970,14 +10041,6 @@ Object.freeze({
9970
10041
  {
9971
10042
  "path": "powerFactor",
9972
10043
  "valueType": "number"
9973
- },
9974
- {
9975
- "path": "unit",
9976
- "valueType": "string"
9977
- },
9978
- {
9979
- "path": "precision",
9980
- "valueType": "number"
9981
10044
  }
9982
10045
  ],
9983
10046
  "presence": [
@@ -10006,20 +10069,10 @@ Object.freeze({
10006
10069
  "valueType": "number"
10007
10070
  }
10008
10071
  ],
10009
- "pressure-sensor": [
10010
- {
10011
- "path": "hpa",
10012
- "valueType": "number"
10013
- },
10014
- {
10015
- "path": "unit",
10016
- "valueType": "string"
10017
- },
10018
- {
10019
- "path": "precision",
10020
- "valueType": "number"
10021
- }
10022
- ],
10072
+ "pressure-sensor": [{
10073
+ "path": "hpa",
10074
+ "valueType": "number"
10075
+ }],
10023
10076
  "privacy-mask": [{
10024
10077
  "path": "enabled",
10025
10078
  "valueType": "boolean"
@@ -10330,20 +10383,10 @@ Object.freeze({
10330
10383
  "path": "tampered",
10331
10384
  "valueType": "boolean"
10332
10385
  }],
10333
- "temperature-sensor": [
10334
- {
10335
- "path": "celsius",
10336
- "valueType": "number"
10337
- },
10338
- {
10339
- "path": "unit",
10340
- "valueType": "string"
10341
- },
10342
- {
10343
- "path": "precision",
10344
- "valueType": "number"
10345
- }
10346
- ],
10386
+ "temperature-sensor": [{
10387
+ "path": "celsius",
10388
+ "valueType": "number"
10389
+ }],
10347
10390
  "update": [
10348
10391
  {
10349
10392
  "path": "currentVersion",
@@ -10454,10 +10497,6 @@ Object.freeze({
10454
10497
  "path": "temperature",
10455
10498
  "valueType": "number"
10456
10499
  },
10457
- {
10458
- "path": "temperatureUnit",
10459
- "valueType": "string"
10460
- },
10461
10500
  {
10462
10501
  "path": "humidity",
10463
10502
  "valueType": "number"
@@ -10466,18 +10505,10 @@ Object.freeze({
10466
10505
  "path": "pressure",
10467
10506
  "valueType": "number"
10468
10507
  },
10469
- {
10470
- "path": "pressureUnit",
10471
- "valueType": "string"
10472
- },
10473
10508
  {
10474
10509
  "path": "windSpeed",
10475
10510
  "valueType": "number"
10476
10511
  },
10477
- {
10478
- "path": "windSpeedUnit",
10479
- "valueType": "string"
10480
- },
10481
10512
  {
10482
10513
  "path": "windBearing",
10483
10514
  "valueType": "number"
@@ -10527,7 +10558,7 @@ object({
10527
10558
  params: record(string(), union([string(), number()])).optional()
10528
10559
  });
10529
10560
  //#endregion
10530
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10561
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10531
10562
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10532
10563
  DeviceType["Camera"] = "camera";
10533
10564
  DeviceType["Hub"] = "hub";
@@ -10621,8 +10652,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10621
10652
  DeviceType["PetFeeder"] = "pet-feeder";
10622
10653
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10623
10654
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10624
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10655
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10656
+ * A plug-built dishwasher carries running-state and power instead. */
10625
10657
  DeviceType["Dishwasher"] = "dishwasher";
10658
+ /** Washing machine. A plug-built one is running while its power stays
10659
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10660
+ DeviceType["Washer"] = "washer";
10661
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10662
+ DeviceType["Dryer"] = "dryer";
10626
10663
  return DeviceType;
10627
10664
  }({});
10628
10665
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -10880,7 +10917,7 @@ function errMsg(err) {
10880
10917
  return String(err);
10881
10918
  }
10882
10919
  //#endregion
10883
- //#region ../types/dist/composition-kEl9mevu.mjs
10920
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
10884
10921
  /**
10885
10922
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10886
10923
  *
@@ -11195,6 +11232,13 @@ var table = {
11195
11232
  apply: () => {
11196
11233
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11197
11234
  }
11235
+ },
11236
+ schmitt: {
11237
+ minArgs: 3,
11238
+ maxArgs: 5,
11239
+ apply: () => {
11240
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11241
+ }
11198
11242
  }
11199
11243
  };
11200
11244
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11635,15 +11679,232 @@ function validateExpressionSource(src) {
11635
11679
  }
11636
11680
  return null;
11637
11681
  }
11638
- /** A composed device's stableId is this prefix plus the block id. */
11639
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11640
- /** Longest snippet a `code` source may carry (slice 4). */
11641
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11682
+ /**
11683
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11684
+ *
11685
+ * Standby of the control board is under 5 W; a motor or a heater is far
11686
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11687
+ * a cycle, so running starts only after 30 s above the line.
11688
+ *
11689
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11690
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11691
+ * report "finished" in the middle of the program, so the stop dwell is
11692
+ * 10 minutes on every type. The number is stored per type so one machine
11693
+ * can move later without moving the others.
11694
+ */
11695
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11696
+ var APPLIANCE_KINDS = [
11697
+ "washer",
11698
+ "dryer",
11699
+ "dishwasher"
11700
+ ];
11701
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11702
+ /**
11703
+ * Statistics and integrals over a history read (D944).
11704
+ *
11705
+ * Pure: the same points always give the same answer, so a restart recomputes
11706
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11707
+ * never the mean of the samples. A hole with no held value is not integrated
11708
+ * as zero. A truncated read is not published.
11709
+ */
11710
+ var STATISTIC_STATS = [
11711
+ "min",
11712
+ "max",
11713
+ "mean",
11714
+ "sum",
11715
+ "count",
11716
+ "change"
11717
+ ];
11718
+ /**
11719
+ * The range a statistics or integral helper asks history for (D944).
11720
+ *
11721
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11722
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11723
+ * is strictly longer than the longest that range can be. Forever qualifies.
11724
+ * Equal to a tier's retention is a refusal, not a short answer.
11725
+ */
11726
+ var MINUTE_MS = 6e4;
11727
+ var HOUR_MS = 60 * MINUTE_MS;
11728
+ var DAY_MS = 24 * HOUR_MS;
11729
+ var ROLLING_SPANS = [
11730
+ "1h",
11731
+ "24h",
11732
+ "2d",
11733
+ "7d",
11734
+ "30d",
11735
+ "custom"
11736
+ ];
11737
+ var CALENDAR_PERIODS = [
11738
+ "today",
11739
+ "yesterday",
11740
+ "this-week",
11741
+ "this-month"
11742
+ ];
11743
+ var RESET_PERIODS = [
11744
+ "never",
11745
+ "daily",
11746
+ "weekly",
11747
+ "monthly"
11748
+ ];
11749
+ 5 * MINUTE_MS;
11750
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11751
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11752
+ var rollingWindowObject = object({
11753
+ kind: literal("rolling"),
11754
+ span: _enum(ROLLING_SPANS),
11755
+ customMinutes: number().int().min(5).optional()
11756
+ }).check((payload) => {
11757
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11758
+ code: "custom",
11759
+ message: "a custom range needs a duration in minutes",
11760
+ path: ["customMinutes"],
11761
+ input: payload.value
11762
+ });
11763
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11764
+ code: "custom",
11765
+ message: "only a custom range carries a duration",
11766
+ path: ["customMinutes"],
11767
+ input: payload.value
11768
+ });
11769
+ });
11770
+ var RollingWindowSchema = rollingWindowObject;
11771
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
11772
+ kind: literal("calendar"),
11773
+ period: _enum(CALENDAR_PERIODS)
11774
+ })]);
11775
+ /**
11776
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11777
+ * id. Split out so helper schemas can name it without a cycle through
11778
+ * `composition.ts` (D659).
11779
+ */
11642
11780
  var CompositionSourceRefSchema = object({
11643
11781
  /** The owning addon; `stableId` is unique only within it. */
11644
11782
  addonId: string().min(1),
11645
11783
  stableId: string().min(1)
11646
11784
  });
11785
+ var GROUP_TEMPLATE_IDS = [
11786
+ "temperature",
11787
+ "humidity",
11788
+ "pressure",
11789
+ "illuminance",
11790
+ "power",
11791
+ "energy",
11792
+ "battery",
11793
+ "numeric",
11794
+ "motion",
11795
+ "contact",
11796
+ "leak",
11797
+ "smoke",
11798
+ "occupancy",
11799
+ "binary",
11800
+ "switch",
11801
+ "light",
11802
+ "fan",
11803
+ "cover",
11804
+ "valve",
11805
+ "lock",
11806
+ "media-player",
11807
+ "climate"
11808
+ ];
11809
+ var NUMERIC_AGGREGATES = [
11810
+ "mean",
11811
+ "median",
11812
+ "min",
11813
+ "max",
11814
+ "sum"
11815
+ ];
11816
+ var BINARY_AGGREGATES = ["any", "all"];
11817
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11818
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11819
+ var AGGREGATE_REDUCERS = [
11820
+ "mean",
11821
+ "median",
11822
+ "min",
11823
+ "max",
11824
+ "sum",
11825
+ "any",
11826
+ "all"
11827
+ ];
11828
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11829
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11830
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11831
+ /** One member. The field is stored so occupancy can name motion or binary. */
11832
+ var HelperMemberSchema = object({
11833
+ source: CompositionSourceRefSchema,
11834
+ cap: string().min(1),
11835
+ fieldPath: string().min(1)
11836
+ });
11837
+ var HelperSpecSchema = discriminatedUnion("kind", [
11838
+ object({
11839
+ kind: literal("group"),
11840
+ templateId: GroupTemplateIdSchema,
11841
+ aggregate: AggregateReducerSchema,
11842
+ members: array(HelperMemberSchema).min(1).max(64),
11843
+ /** Actuator groups only. Readings leave this unset. */
11844
+ active: ActiveCriterionSchema.optional()
11845
+ }),
11846
+ object({
11847
+ kind: literal("appliance"),
11848
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
11849
+ appliance: _enum(APPLIANCE_KINDS),
11850
+ thresholdWatts: number().positive(),
11851
+ startDwellMs: number().int().nonnegative(),
11852
+ stopDwellMs: number().int().nonnegative(),
11853
+ member: HelperMemberSchema
11854
+ }),
11855
+ object({
11856
+ kind: literal("statistic"),
11857
+ stat: _enum(STATISTIC_STATS),
11858
+ window: StatisticWindowSchema,
11859
+ source: HelperMemberSchema
11860
+ }),
11861
+ object({
11862
+ kind: literal("integral"),
11863
+ reset: _enum(RESET_PERIODS),
11864
+ output: _enum(["energy", "meter"]),
11865
+ source: HelperMemberSchema
11866
+ }),
11867
+ object({
11868
+ kind: literal("threshold"),
11869
+ source: HelperMemberSchema,
11870
+ high: number(),
11871
+ low: number(),
11872
+ onForMs: number().int().nonnegative(),
11873
+ offForMs: number().int().nonnegative()
11874
+ }),
11875
+ object({
11876
+ kind: literal("arithmetic"),
11877
+ op: _enum([
11878
+ "sum",
11879
+ "difference",
11880
+ "ratio"
11881
+ ]),
11882
+ inputs: array(HelperMemberSchema).min(2).max(64)
11883
+ }),
11884
+ object({
11885
+ kind: literal("expression"),
11886
+ expr: string().min(1),
11887
+ inputs: array(object({
11888
+ name: string().min(1),
11889
+ member: HelperMemberSchema
11890
+ })).min(1).max(64),
11891
+ output: _enum(["number", "boolean"])
11892
+ }),
11893
+ object({
11894
+ kind: literal("derivative"),
11895
+ window: RollingWindowSchema,
11896
+ per: _enum([
11897
+ "second",
11898
+ "minute",
11899
+ "hour"
11900
+ ]),
11901
+ source: HelperMemberSchema
11902
+ })
11903
+ ]);
11904
+ /** A composed device's stableId is this prefix plus the block id. */
11905
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11906
+ /** Longest snippet a `code` source may carry (slice 4). */
11907
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11647
11908
  /** One field of one capability of one source device. */
11648
11909
  var CompositionFieldReadSchema = object({
11649
11910
  source: CompositionSourceRefSchema,
@@ -11687,7 +11948,59 @@ var CompositionCodeSourceSchema = object({
11687
11948
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11688
11949
  CompositionFromSourceSchema,
11689
11950
  CompositionExpressionSourceSchema,
11690
- CompositionCodeSourceSchema
11951
+ CompositionCodeSourceSchema,
11952
+ object({
11953
+ kind: literal("aggregate"),
11954
+ reducer: _enum([
11955
+ "mean",
11956
+ "median",
11957
+ "min",
11958
+ "max",
11959
+ "sum",
11960
+ "any",
11961
+ "all"
11962
+ ]),
11963
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11964
+ /**
11965
+ * Set when the reads are an enum state, not a yes/no: the listed states
11966
+ * count as active (a cover is `open`). A state outside the list is inactive;
11967
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
11968
+ */
11969
+ activeWhen: array(string().min(1)).min(1).optional(),
11970
+ /** `value` is the reading; the others are the member counts. */
11971
+ publish: _enum([
11972
+ "value",
11973
+ "included",
11974
+ "excluded",
11975
+ "total"
11976
+ ]),
11977
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11978
+ empty: _enum([
11979
+ "null",
11980
+ "omit",
11981
+ "offline"
11982
+ ])
11983
+ }),
11984
+ object({
11985
+ kind: literal("series"),
11986
+ read: CompositionFieldReadSchema,
11987
+ /** `value` is the statistic or the integral. The others are the gap counts. */
11988
+ publish: _enum([
11989
+ "value",
11990
+ "included",
11991
+ "excluded",
11992
+ "total"
11993
+ ]),
11994
+ /**
11995
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
11996
+ * device unseeded until history answers, so a restart cannot publish 0.
11997
+ */
11998
+ empty: _enum([
11999
+ "null",
12000
+ "omit",
12001
+ "offline"
12002
+ ])
12003
+ })
11691
12004
  ]);
11692
12005
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11693
12006
  kind: literal("forward"),
@@ -11733,7 +12046,13 @@ var CompositionSchema = object({
11733
12046
  kind: literal("existing"),
11734
12047
  device: CompositionSourceRefSchema
11735
12048
  })]),
11736
- features: array(CompositionFeatureSchema).min(1).max(16)
12049
+ features: array(CompositionFeatureSchema).min(1).max(16),
12050
+ /**
12051
+ * Present when the block is a helper template. Absent on a free
12052
+ * composition (the room example, device 4603). The editor reads this,
12053
+ * never the compiled features.
12054
+ */
12055
+ helper: HelperSpecSchema.optional()
11737
12056
  }).superRefine((composition, ctx) => {
11738
12057
  const seen = /* @__PURE__ */ new Set();
11739
12058
  composition.features.forEach((feature, index) => {
@@ -16872,7 +17191,22 @@ var BrokerProviderInfoSchema = object({
16872
17191
  */
16873
17192
  interactiveConnectPath: string().min(1).optional(),
16874
17193
  /** Relative hub path to disconnect the linked account (same popup pattern). */
16875
- interactiveDisconnectPath: string().min(1).optional()
17194
+ interactiveDisconnectPath: string().min(1).optional(),
17195
+ /**
17196
+ * The kind signs in through a server-held interactive session instead of
17197
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17198
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17199
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17200
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17201
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17202
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17203
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17204
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17205
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17206
+ * offers it as the default path; the fields the session fills are
17207
+ * marked `filledBySignIn` in the kind's settings schema.
17208
+ */
17209
+ signIn: object({ basePath: string().min(1) }).optional()
16876
17210
  }))
16877
17211
  });
16878
17212
  var ListInputSchema = object({
@@ -17973,7 +18307,12 @@ var CompositionProblemSchema = object({
17973
18307
  "item-field-reserved",
17974
18308
  "target-missing",
17975
18309
  "target-unreadable",
17976
- "not-replaceable"
18310
+ "not-replaceable",
18311
+ "native-capability-replace",
18312
+ "mixed-quantity",
18313
+ "helper-mismatch",
18314
+ "member-constraint",
18315
+ "retention"
17977
18316
  ]),
17978
18317
  path: string(),
17979
18318
  message: string()
@@ -17994,7 +18333,8 @@ var CompositionUnavailableModeSchema = _enum([
17994
18333
  "null",
17995
18334
  "offline",
17996
18335
  "unknown",
17997
- "release"
18336
+ "release",
18337
+ "omit"
17998
18338
  ]);
17999
18339
  /**
18000
18340
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18031,9 +18371,11 @@ var CompositionFieldPlanSchema = object({
18031
18371
  reads: array(CompositionFieldReadSchema)
18032
18372
  });
18033
18373
  /**
18034
- * What a feature does to its target (D663): `new` builds a capability on a new
18035
- * device; on an existing device it `add`s a capability the device lacks, or
18036
- * `replace`s named fields of one it has natively.
18374
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18375
+ * new device; on an existing device it `add`s a capability the device lacks.
18376
+ * Replacing a capability the device already serves is refused
18377
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18378
+ * wire for a claim the hub already holds.
18037
18379
  */
18038
18380
  var CompositionFeatureModeSchema = _enum([
18039
18381
  "new",
@@ -18204,8 +18546,392 @@ Object.freeze({
18204
18546
  latchedAt: {
18205
18547
  kind: "fixed",
18206
18548
  result: ["number", "null"]
18549
+ },
18550
+ schmitt: {
18551
+ kind: "fixed",
18552
+ result: ["boolean", "null"]
18207
18553
  }
18208
18554
  });
18555
+ /**
18556
+ * One catalog row per base reading a group helper can build (D944 §3).
18557
+ * Data, not a form default written into the block.
18558
+ */
18559
+ var NOW$2 = {
18560
+ fieldPath: "lastFetchedAt",
18561
+ expr: "now"
18562
+ };
18563
+ var CHANGED = {
18564
+ fieldPath: "lastChangedAt",
18565
+ expr: "now"
18566
+ };
18567
+ function numeric(input) {
18568
+ return {
18569
+ ...input,
18570
+ family: "numeric",
18571
+ deviceType: DeviceType.Sensor,
18572
+ memberDeviceType: DeviceType.Sensor,
18573
+ memberDeviceTypes: [DeviceType.Sensor],
18574
+ activeWhen: void 0,
18575
+ commandCaps: [],
18576
+ baseUnit: baseUnitOf(input.quantity),
18577
+ aggregates: NUMERIC_AGGREGATES,
18578
+ accept: [{
18579
+ cap: input.outputCap,
18580
+ fieldPath: input.outputField
18581
+ }]
18582
+ };
18583
+ }
18584
+ function binary(input) {
18585
+ return {
18586
+ ...input,
18587
+ family: "binary",
18588
+ deviceType: DeviceType.Sensor,
18589
+ memberDeviceType: DeviceType.Sensor,
18590
+ memberDeviceTypes: [DeviceType.Sensor],
18591
+ activeWhen: void 0,
18592
+ commandCaps: [],
18593
+ quantity: "none",
18594
+ baseUnit: void 0,
18595
+ aggregates: BINARY_AGGREGATES,
18596
+ empty: "offline"
18597
+ };
18598
+ }
18599
+ var GROUP_TEMPLATES = [
18600
+ numeric({
18601
+ id: "temperature",
18602
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18603
+ role: DeviceRole.TemperatureSensor,
18604
+ outputCap: "temperature-sensor",
18605
+ outputField: "celsius",
18606
+ quantity: "temperature",
18607
+ defaultAggregate: "mean",
18608
+ empty: "null",
18609
+ companions: [NOW$2]
18610
+ }),
18611
+ numeric({
18612
+ id: "humidity",
18613
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18614
+ role: DeviceRole.HumiditySensor,
18615
+ outputCap: "humidity-sensor",
18616
+ outputField: "percent",
18617
+ quantity: "humidity",
18618
+ defaultAggregate: "mean",
18619
+ empty: "null",
18620
+ companions: [NOW$2]
18621
+ }),
18622
+ numeric({
18623
+ id: "pressure",
18624
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18625
+ role: DeviceRole.PressureSensor,
18626
+ outputCap: "pressure-sensor",
18627
+ outputField: "hpa",
18628
+ quantity: "pressure",
18629
+ defaultAggregate: "mean",
18630
+ empty: "null",
18631
+ companions: [NOW$2]
18632
+ }),
18633
+ numeric({
18634
+ id: "illuminance",
18635
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18636
+ role: DeviceRole.AmbientLightSensor,
18637
+ outputCap: "ambient-light-sensor",
18638
+ outputField: "lux",
18639
+ quantity: "illuminance",
18640
+ defaultAggregate: "mean",
18641
+ empty: "null",
18642
+ companions: [NOW$2]
18643
+ }),
18644
+ numeric({
18645
+ id: "power",
18646
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18647
+ role: DeviceRole.PowerSensor,
18648
+ outputCap: "power-meter",
18649
+ outputField: "watts",
18650
+ quantity: "power",
18651
+ defaultAggregate: "sum",
18652
+ empty: "omit",
18653
+ companions: [NOW$2]
18654
+ }),
18655
+ numeric({
18656
+ id: "energy",
18657
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18658
+ role: DeviceRole.EnergySensor,
18659
+ outputCap: "power-meter",
18660
+ outputField: "kwhTotal",
18661
+ quantity: "energy",
18662
+ defaultAggregate: "sum",
18663
+ empty: "omit",
18664
+ companions: [NOW$2]
18665
+ }),
18666
+ numeric({
18667
+ id: "battery",
18668
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18669
+ role: DeviceRole.BatterySensor,
18670
+ outputCap: "battery",
18671
+ outputField: "percentage",
18672
+ quantity: "percentage",
18673
+ defaultAggregate: "mean",
18674
+ empty: "null",
18675
+ companions: [
18676
+ {
18677
+ fieldPath: "charging",
18678
+ expr: "\"unknown\""
18679
+ },
18680
+ {
18681
+ fieldPath: "sleeping",
18682
+ expr: "false"
18683
+ },
18684
+ {
18685
+ fieldPath: "lastUpdated",
18686
+ expr: "now"
18687
+ }
18688
+ ]
18689
+ }),
18690
+ numeric({
18691
+ id: "numeric",
18692
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18693
+ role: DeviceRole.NumericSensor,
18694
+ outputCap: "numeric-sensor",
18695
+ outputField: "value",
18696
+ quantity: "device",
18697
+ defaultAggregate: "mean",
18698
+ empty: "null",
18699
+ companions: [NOW$2]
18700
+ }),
18701
+ binary({
18702
+ id: "motion",
18703
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18704
+ role: DeviceRole.MotionSensor,
18705
+ outputCap: "motion",
18706
+ outputField: "detected",
18707
+ defaultAggregate: "any",
18708
+ accept: [{
18709
+ cap: "motion",
18710
+ fieldPath: "detected"
18711
+ }],
18712
+ companions: [{
18713
+ fieldPath: "lastDetectedAt",
18714
+ expr: "null"
18715
+ }, {
18716
+ fieldPath: "autoClearAfterMs",
18717
+ expr: "null"
18718
+ }]
18719
+ }),
18720
+ binary({
18721
+ id: "contact",
18722
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18723
+ role: DeviceRole.ContactSensor,
18724
+ outputCap: "contact",
18725
+ outputField: "entryOpen",
18726
+ defaultAggregate: "any",
18727
+ accept: [{
18728
+ cap: "contact",
18729
+ fieldPath: "entryOpen"
18730
+ }],
18731
+ companions: [CHANGED]
18732
+ }),
18733
+ binary({
18734
+ id: "leak",
18735
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18736
+ role: DeviceRole.LeakSensor,
18737
+ outputCap: "flood",
18738
+ outputField: "flooded",
18739
+ defaultAggregate: "any",
18740
+ accept: [{
18741
+ cap: "flood",
18742
+ fieldPath: "flooded"
18743
+ }],
18744
+ companions: [CHANGED]
18745
+ }),
18746
+ binary({
18747
+ id: "smoke",
18748
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18749
+ role: DeviceRole.SmokeSensor,
18750
+ outputCap: "smoke",
18751
+ outputField: "detected",
18752
+ defaultAggregate: "any",
18753
+ accept: [{
18754
+ cap: "smoke",
18755
+ fieldPath: "detected"
18756
+ }],
18757
+ companions: [CHANGED]
18758
+ }),
18759
+ binary({
18760
+ id: "occupancy",
18761
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18762
+ role: DeviceRole.BinarySensor,
18763
+ outputCap: "binary",
18764
+ outputField: "on",
18765
+ defaultAggregate: "any",
18766
+ accept: [{
18767
+ cap: "motion",
18768
+ fieldPath: "detected"
18769
+ }, {
18770
+ cap: "binary",
18771
+ fieldPath: "on"
18772
+ }],
18773
+ companions: [CHANGED]
18774
+ }),
18775
+ binary({
18776
+ id: "binary",
18777
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18778
+ role: DeviceRole.BinarySensor,
18779
+ outputCap: "binary",
18780
+ outputField: "on",
18781
+ defaultAggregate: "any",
18782
+ accept: [{
18783
+ cap: "binary",
18784
+ fieldPath: "on"
18785
+ }],
18786
+ companions: [CHANGED]
18787
+ }),
18788
+ actuator({
18789
+ id: "switch",
18790
+ 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.",
18791
+ deviceType: DeviceType.Switch,
18792
+ outputCap: "switch",
18793
+ outputField: "on",
18794
+ member: {
18795
+ cap: "switch",
18796
+ fieldPath: "on"
18797
+ },
18798
+ commandCaps: ["switch"]
18799
+ }),
18800
+ actuator({
18801
+ id: "light",
18802
+ 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.",
18803
+ deviceType: DeviceType.Light,
18804
+ outputCap: "switch",
18805
+ outputField: "on",
18806
+ member: {
18807
+ cap: "switch",
18808
+ fieldPath: "on"
18809
+ },
18810
+ commandCaps: [
18811
+ "switch",
18812
+ "brightness",
18813
+ "color"
18814
+ ]
18815
+ }),
18816
+ actuator({
18817
+ id: "fan",
18818
+ 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.",
18819
+ deviceType: DeviceType.Fan,
18820
+ outputCap: "switch",
18821
+ outputField: "on",
18822
+ member: {
18823
+ cap: "switch",
18824
+ fieldPath: "on"
18825
+ },
18826
+ commandCaps: ["switch", "fan-control"]
18827
+ }),
18828
+ actuator({
18829
+ id: "cover",
18830
+ 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.",
18831
+ deviceType: DeviceType.Cover,
18832
+ outputCap: "group-active",
18833
+ outputField: "active",
18834
+ member: {
18835
+ cap: "cover",
18836
+ fieldPath: "state"
18837
+ },
18838
+ activeWhen: [
18839
+ "open",
18840
+ "opening",
18841
+ "stopped"
18842
+ ],
18843
+ commandCaps: ["cover"]
18844
+ }),
18845
+ actuator({
18846
+ id: "valve",
18847
+ 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.",
18848
+ deviceType: DeviceType.Valve,
18849
+ outputCap: "group-active",
18850
+ outputField: "active",
18851
+ member: {
18852
+ cap: "valve",
18853
+ fieldPath: "state"
18854
+ },
18855
+ activeWhen: [
18856
+ "open",
18857
+ "opening",
18858
+ "stopped"
18859
+ ],
18860
+ commandCaps: ["valve"]
18861
+ }),
18862
+ actuator({
18863
+ id: "lock",
18864
+ 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.",
18865
+ deviceType: DeviceType.Lock,
18866
+ outputCap: "group-active",
18867
+ outputField: "active",
18868
+ member: {
18869
+ cap: "lock-control",
18870
+ fieldPath: "state"
18871
+ },
18872
+ activeWhen: ["locked"],
18873
+ commandCaps: ["lock-control"]
18874
+ }),
18875
+ actuator({
18876
+ id: "media-player",
18877
+ 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.",
18878
+ deviceType: DeviceType.MediaPlayer,
18879
+ outputCap: "group-active",
18880
+ outputField: "active",
18881
+ member: {
18882
+ cap: "media-player",
18883
+ fieldPath: "state"
18884
+ },
18885
+ activeWhen: ["playing", "buffering"],
18886
+ commandCaps: ["media-player"]
18887
+ }),
18888
+ actuator({
18889
+ id: "climate",
18890
+ 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.",
18891
+ deviceType: DeviceType.Thermostat,
18892
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
18893
+ outputCap: "group-active",
18894
+ outputField: "active",
18895
+ member: {
18896
+ cap: "climate-control",
18897
+ fieldPath: "mode"
18898
+ },
18899
+ activeWhen: [
18900
+ "heat",
18901
+ "cool",
18902
+ "auto",
18903
+ "heat_cool",
18904
+ "fan_only",
18905
+ "dry"
18906
+ ],
18907
+ commandCaps: ["climate-control"]
18908
+ })
18909
+ ];
18910
+ function actuator(input) {
18911
+ return {
18912
+ id: input.id,
18913
+ summary: input.summary,
18914
+ deviceType: input.deviceType,
18915
+ outputCap: input.outputCap,
18916
+ outputField: input.outputField,
18917
+ commandCaps: input.commandCaps,
18918
+ family: "actuator",
18919
+ role: void 0,
18920
+ memberDeviceType: input.deviceType,
18921
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
18922
+ activeWhen: input.activeWhen,
18923
+ quantity: "none",
18924
+ baseUnit: void 0,
18925
+ aggregates: BINARY_AGGREGATES,
18926
+ defaultAggregate: "any",
18927
+ empty: "offline",
18928
+ accept: [input.member],
18929
+ companions: [CHANGED]
18930
+ };
18931
+ }
18932
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18933
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18934
+ DeviceType.Camera;
18209
18935
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18210
18936
  /**
18211
18937
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19919,12 +20645,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19919
20645
  * providers detect renames by matching `uniqueId` and patch `id`
19920
20646
  * in-place so dispatch keeps working.
19921
20647
  *
19922
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19923
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19924
- * state push and are the single source of truth. The `device_class` tag is
19925
- * an HA-specific input that is consumed at the discovery boundary
19926
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19927
- * stored after that point.
20648
+ * **Display unit and precision are NOT stored here.** They are a read-time
20649
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20650
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20651
+ * neither is repeated on each sample. The `device_class` tag is an
20652
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20653
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19928
20654
  *
19929
20655
  * The `raw` escape hatch carries provider-specific attributes that don't
19930
20656
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19938,6 +20664,14 @@ var SourceInfoSchema = object({
19938
20664
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19939
20665
  * Used to detect rename when `id` changes. */
19940
20666
  uniqueId: string().optional(),
20667
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20668
+ quantity: ReadingQuantitySchema.optional(),
20669
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20670
+ dateTimeShape: _enum([
20671
+ "date",
20672
+ "time",
20673
+ "datetime"
20674
+ ]).optional(),
19941
20675
  /** Provider-specific extras that don't fit the structured slots.
19942
20676
  * Whitelist what goes here per provider — do NOT dump entire HA
19943
20677
  * attribute blobs (would explode the meta JSON column). */
@@ -40961,7 +41695,8 @@ method(object({
40961
41695
  "enum"
40962
41696
  ]),
40963
41697
  enumValues: array(string()).optional(),
40964
- item: boolean().optional()
41698
+ item: boolean().optional(),
41699
+ quantity: ReadingQuantitySchema.optional()
40965
41700
  })).readonly(),
40966
41701
  itemArray: object({
40967
41702
  path: string(),
@@ -43322,7 +44057,7 @@ var NetworkAccessStatusSchema = object({
43322
44057
  });
43323
44058
  /**
43324
44059
  * Optional, richer endpoint shape returned by providers that expose
43325
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44060
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43326
44061
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43327
44062
  * the originating provider config (mode + sourcePort) so the
43328
44063
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44805,8 +45540,8 @@ var MaskPolygonShapeSchema = object({
44805
45540
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44806
45541
  var MaskGridShapeSchema = object({
44807
45542
  kind: literal("grid"),
44808
- gridWidth: number(),
44809
- gridHeight: number(),
45543
+ gridWidth: number().meta(readingMeta("none")),
45544
+ gridHeight: number().meta(readingMeta("none")),
44810
45545
  cells: array(boolean())
44811
45546
  });
44812
45547
  discriminatedUnion("kind", [
@@ -44918,6 +45653,8 @@ var NcSystemEventKindSchema = _enum([
44918
45653
  "detection-blind",
44919
45654
  "motion-noisy",
44920
45655
  "motion-calm",
45656
+ "package-llm-silent",
45657
+ "package-llm-answered",
44921
45658
  "recording-stalled",
44922
45659
  "recording-healthy",
44923
45660
  "recording-degraded",
@@ -46007,7 +46744,20 @@ var NcRuleInputSchema = object({
46007
46744
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46008
46745
  * path does not run Zod defaults.
46009
46746
  */
46010
- groupingSec: number().int().min(5).max(60).optional()
46747
+ groupingSec: number().int().min(5).max(60).optional(),
46748
+ /**
46749
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46750
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46751
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46752
+ * same mark the Library writes, and records whose notification it was and
46753
+ * when. A channel with no buttons gets the same two choices as links.
46754
+ *
46755
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46756
+ * cap path): absent and `false` both mean off, which is what every rule
46757
+ * authored before this field meant. An editor turns it off by writing
46758
+ * `false`, so it needs no `clear` key.
46759
+ */
46760
+ askVerdict: boolean().optional()
46011
46761
  });
46012
46762
  /**
46013
46763
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -56157,6 +56907,28 @@ var PackageVersionInfoSchema = object({
56157
56907
  deprecated: string().optional(),
56158
56908
  distTags: array(string()).readonly()
56159
56909
  });
56910
+ /**
56911
+ * One published document. `ok` is markdown from the registry. `missing` means
56912
+ * the registry answered and the file was not there. `error` is a registry or
56913
+ * tarball failure — never an empty string standing in for either.
56914
+ */
56915
+ var PackageDocBodySchema = discriminatedUnion("status", [
56916
+ object({
56917
+ status: literal("ok"),
56918
+ markdown: string()
56919
+ }),
56920
+ object({ status: literal("missing") }),
56921
+ object({
56922
+ status: literal("error"),
56923
+ message: string()
56924
+ })
56925
+ ]);
56926
+ var PackageDocsSchema = object({
56927
+ name: string(),
56928
+ version: string(),
56929
+ readme: PackageDocBodySchema,
56930
+ changelog: PackageDocBodySchema
56931
+ });
56160
56932
  var SearchResultSchema = object({
56161
56933
  name: string(),
56162
56934
  version: string(),
@@ -56375,7 +57147,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56375
57147
  }), object({ success: literal(true) }), {
56376
57148
  kind: "mutation",
56377
57149
  auth: "admin"
56378
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57150
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57151
+ name: string().min(1),
57152
+ version: string().min(1).optional()
57153
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56379
57154
  kind: "mutation",
56380
57155
  auth: "admin"
56381
57156
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56434,27 +57209,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56434
57209
  */
56435
57210
  var AirQualitySensorStatusSchema = object({
56436
57211
  /** Carbon dioxide concentration in ppm. */
56437
- co2Ppm: number().min(0).optional(),
57212
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56438
57213
  /** Total volatile organic compounds in ppb. */
56439
- vocPpb: number().min(0).optional(),
57214
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56440
57215
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56441
- pm25: number().min(0).optional(),
57216
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56442
57217
  /** Particulate matter ≤ 10 μm in µg/m³. */
56443
- pm10: number().min(0).optional(),
57218
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56444
57219
  /** Composite AQI value (typically 0..500). */
56445
- aqi: number().optional(),
57220
+ aqi: number().optional().meta(readingMeta("index")),
56446
57221
  /** Ms epoch when the slice was last updated. */
56447
- lastFetchedAt: number(),
56448
- /** Live display unit of the single metric this slice carries (e.g. HA
56449
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56450
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56451
- * per slice is unambiguous. */
56452
- unit: string().optional(),
56453
- /** Suggested decimal places for numeric display.
56454
- * Populated live from the upstream source when provided (e.g. HA
56455
- * `attributes.suggested_display_precision`). Falls back to
56456
- * auto-formatting when absent. */
56457
- precision: number().int().min(0).max(10).optional()
57222
+ lastFetchedAt: number()
56458
57223
  });
56459
57224
  var airQualitySensorCapability = {
56460
57225
  name: "air-quality-sensor",
@@ -56491,12 +57256,12 @@ var airQualitySensorCapability = {
56491
57256
  },
56492
57257
  pm25: {
56493
57258
  kind: "gauge",
56494
- unit: "ug/m3",
57259
+ unit: "µg/m³",
56495
57260
  deadband: 1
56496
57261
  },
56497
57262
  pm10: {
56498
57263
  kind: "gauge",
56499
- unit: "ug/m3",
57264
+ unit: "µg/m³",
56500
57265
  deadband: 1
56501
57266
  },
56502
57267
  aqi: {
@@ -56514,22 +57279,13 @@ var airQualitySensorCapability = {
56514
57279
  * entries with `device_class: illuminance`.
56515
57280
  */
56516
57281
  var AmbientLightSensorStatusSchema = object({
56517
- /** Current illuminance in lux (lx). */
56518
- lux: number().min(0).meta({
56519
- unit: "lx",
57282
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57283
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57284
+ precision: 0,
56520
57285
  label: "Illuminance"
56521
- }),
57286
+ })),
56522
57287
  /** Ms epoch when the slice was last updated. */
56523
- lastFetchedAt: number(),
56524
- /** Live display unit from the upstream source (e.g. HA
56525
- * `attributes.unit_of_measurement`). The UI prefers this over the
56526
- * role's canonical unit. Absent → fall back to the canonical unit. */
56527
- unit: string().optional(),
56528
- /** Suggested decimal places for numeric display.
56529
- * Populated live from the upstream source when provided (e.g. HA
56530
- * `attributes.suggested_display_precision`). Falls back to
56531
- * auto-formatting when absent. */
56532
- precision: number().int().min(0).max(10).optional()
57288
+ lastFetchedAt: number()
56533
57289
  });
56534
57290
  var ambientLightSensorCapability = {
56535
57291
  name: "ambient-light-sensor",
@@ -56561,12 +57317,7 @@ var ambientLightSensorCapability = {
56561
57317
  } } },
56562
57318
  /** Clock fields: written, but excluded from the compare that decides
56563
57319
  * whether persisting is worth a SQLite commit. */
56564
- volatileStateFields: ["lastFetchedAt"],
56565
- customize: {
56566
- version: 1,
56567
- advanced: "derive",
56568
- targets: []
56569
- }
57320
+ volatileStateFields: ["lastFetchedAt"]
56570
57321
  };
56571
57322
  /**
56572
57323
  * Per-class audio metrics aggregated over a sliding window.
@@ -56793,10 +57544,7 @@ var BatteryStatusSchema = object({
56793
57544
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56794
57545
  * declines to band a non-finite reading, which is the correct shape.
56795
57546
  */
56796
- percentage: number().min(0).max(100).nullable().meta({
56797
- unit: "%",
56798
- label: "Charge"
56799
- }),
57547
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56800
57548
  /**
56801
57549
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56802
57550
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56823,7 +57571,14 @@ var BatteryStatusSchema = object({
56823
57571
  */
56824
57572
  sleeping: boolean().meta({ label: "Sleeping" }),
56825
57573
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56826
- lastUpdated: number(),
57574
+ lastUpdated: number().meta(readingMeta("none")),
57575
+ /**
57576
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57577
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57578
+ * that only report charge. A second reading on this cap, not a second
57579
+ * `numeric-sensor` on the same device.
57580
+ */
57581
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56827
57582
  /**
56828
57583
  * Ms epoch of the last time the device PROVED it was reachable — a
56829
57584
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56870,12 +57625,12 @@ var BATTERY_DEVICE_TYPES = [
56870
57625
  DeviceType.Switch,
56871
57626
  DeviceType.PetFeeder,
56872
57627
  DeviceType.Container,
56873
- DeviceType.Thermostat
57628
+ DeviceType.Thermostat,
57629
+ DeviceType.EventEmitter
56874
57630
  ];
56875
57631
  /** Generic Customize target for the percentage reading (D936). */
56876
57632
  var batteryCustomize = {
56877
57633
  version: 1,
56878
- advanced: "derive",
56879
57634
  targets: [{
56880
57635
  id: "percentage",
56881
57636
  cardinality: "scalar",
@@ -56884,7 +57639,7 @@ var batteryCustomize = {
56884
57639
  stateField: "percentage"
56885
57640
  },
56886
57641
  deviceTypes: BATTERY_DEVICE_TYPES,
56887
- apply: "add-or-replace",
57642
+ apply: "add",
56888
57643
  path: "percentage",
56889
57644
  sources: [{
56890
57645
  kind: "field",
@@ -57005,10 +57760,16 @@ onStatusChanged: { data: object({
57005
57760
  /**
57006
57761
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57007
57762
  */
57008
- history: { fields: { percentage: {
57009
- kind: "gauge",
57010
- unit: "%"
57011
- } } },
57763
+ history: { fields: {
57764
+ percentage: {
57765
+ kind: "gauge",
57766
+ unit: "%"
57767
+ },
57768
+ volts: {
57769
+ kind: "gauge",
57770
+ unit: "V"
57771
+ }
57772
+ } },
57012
57773
  customize: batteryCustomize,
57013
57774
  /** Clock fields: written, but excluded from the compare that decides
57014
57775
  * whether persisting is worth a SQLite commit. */
@@ -57055,12 +57816,7 @@ var binaryCapability = {
57055
57816
  history: { fields: { on: { kind: "boolean" } } },
57056
57817
  /** Clock fields: written, but excluded from the compare that decides
57057
57818
  * whether persisting is worth a SQLite commit. */
57058
- volatileStateFields: ["lastChangedAt"],
57059
- customize: {
57060
- version: 1,
57061
- advanced: "derive",
57062
- targets: []
57063
- }
57819
+ volatileStateFields: ["lastChangedAt"]
57064
57820
  };
57065
57821
  /**
57066
57822
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57076,7 +57832,7 @@ var binaryCapability = {
57076
57832
  */
57077
57833
  var BrightnessStatusSchema = object({
57078
57834
  /** Current level as 0..100 inclusive. Firmware-reported. */
57079
- percentage: number().min(0).max(100),
57835
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57080
57836
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57081
57837
  lastChangedAt: number()
57082
57838
  });
@@ -57530,21 +58286,21 @@ var ClimateControlStatusSchema = object({
57530
58286
  /** Single setpoint in Celsius. Used by single-target modes
57531
58287
  * (heat / cool / dry). Null when the device is in a range mode
57532
58288
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57533
- target: number().nullable(),
58289
+ target: number().nullable().meta(readingMeta("temperature")),
57534
58290
  /** Upper bound of the dual setpoint in Celsius. Populated only
57535
58291
  * in `heat_cool` mode. */
57536
- targetHigh: number().nullable(),
58292
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57537
58293
  /** Lower bound of the dual setpoint in Celsius. Populated only
57538
58294
  * in `heat_cool` mode. */
57539
- targetLow: number().nullable(),
58295
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57540
58296
  /** Target relative humidity (0..100). Null when the device has no
57541
58297
  * humidity control. */
57542
- targetHumidity: number().min(0).max(100).nullable(),
58298
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57543
58299
  /** Current measured temperature in Celsius. Read-only — pushed by
57544
58300
  * the device's internal sensor. */
57545
- currentTemp: number().nullable(),
58301
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57546
58302
  /** Current measured relative humidity (0..100). Read-only. */
57547
- currentHumidity: number().min(0).max(100).nullable(),
58303
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57548
58304
  /** Vertical louver swing on/off. Null when the device has no vertical
57549
58305
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57550
58306
  swingVertical: boolean().nullable(),
@@ -57552,11 +58308,11 @@ var ClimateControlStatusSchema = object({
57552
58308
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57553
58309
  swingHorizontal: boolean().nullable(),
57554
58310
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57555
- minTemp: number().nullable().optional(),
58311
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57556
58312
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57557
- maxTemp: number().nullable().optional(),
58313
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57558
58314
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57559
- tempStep: number().positive().nullable().optional(),
58315
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57560
58316
  /** Ms epoch when the slice was last updated (push or command). */
57561
58317
  lastFetchedAt: number()
57562
58318
  });
@@ -57698,17 +58454,17 @@ var climateControlCapability = {
57698
58454
  * modes to expose in the picker without a slice fetch.
57699
58455
  */
57700
58456
  var RgbTripletSchema = object({
57701
- r: number().int().min(0).max(255),
57702
- g: number().int().min(0).max(255),
57703
- b: number().int().min(0).max(255)
58457
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58458
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58459
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57704
58460
  });
57705
58461
  var HsvTripletSchema = object({
57706
58462
  /** Hue in degrees, 0..360. */
57707
- h: number().min(0).max(360),
58463
+ h: number().min(0).max(360).meta(readingMeta("none")),
57708
58464
  /** Saturation as 0..100 inclusive. */
57709
- s: number().min(0).max(100),
58465
+ s: number().min(0).max(100).meta(readingMeta("none")),
57710
58466
  /** Value/brightness as 0..100 inclusive. */
57711
- v: number().min(0).max(100)
58467
+ v: number().min(0).max(100).meta(readingMeta("none"))
57712
58468
  });
57713
58469
  var ColorInputSchema = discriminatedUnion("mode", [
57714
58470
  object({
@@ -57737,7 +58493,7 @@ var ColorStatusSchema = object({
57737
58493
  /** Populated when `mode === 'hsv'`. */
57738
58494
  hsv: HsvTripletSchema.optional(),
57739
58495
  /** Populated when `mode === 'mired'`. */
57740
- mireds: number().int().optional(),
58496
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57741
58497
  /** Ms epoch of the last operator-driven change. */
57742
58498
  lastChangedAt: number()
57743
58499
  });
@@ -57932,18 +58688,12 @@ _enum([
57932
58688
  "empty",
57933
58689
  "full"
57934
58690
  ]);
57935
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57936
- * each. */
57937
- var ConsumableRemainingUnitSchema = _enum([
57938
- "days",
57939
- "hours",
57940
- "cycles"
57941
- ]);
57942
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57943
- * days). `value` null = unknown (D393). */
58691
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58692
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58693
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57944
58694
  var ConsumableRemainingSchema = object({
57945
- value: number().min(0).nullable(),
57946
- unit: ConsumableRemainingUnitSchema
58695
+ value: number().min(0).nullable().meta(readingMeta("device")),
58696
+ kind: ConsumableRemainingKindSchema
57947
58697
  });
57948
58698
  /** A single consumable item. Either a continuous `level` (remaining
57949
58699
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57956,7 +58706,7 @@ var ConsumableItemSchema = object({
57956
58706
  /** Display name. */
57957
58707
  label: string().min(1),
57958
58708
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57959
- level: number().min(0).max(100).nullable(),
58709
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57960
58710
  /** The provider's discrete verdict when known (binary mode). */
57961
58711
  status: ConsumableStatusSchema.nullable(),
57962
58712
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58038,8 +58788,8 @@ var consumablesCustomize = {
58038
58788
  transforms: [{ kind: "identity" }]
58039
58789
  }],
58040
58790
  literals: [{
58041
- path: "remaining.unit",
58042
- values: [...ConsumableRemainingUnitSchema.options]
58791
+ path: "remaining.kind",
58792
+ values: [...ConsumableRemainingKindSchema.options]
58043
58793
  }]
58044
58794
  },
58045
58795
  {
@@ -58168,7 +58918,6 @@ reset: method(object({
58168
58918
  },
58169
58919
  "remaining.value": {
58170
58920
  kind: "gauge",
58171
- unitFrom: "remaining.unit",
58172
58921
  deadband: 1,
58173
58922
  displayName: "Remaining"
58174
58923
  }
@@ -58238,10 +58987,9 @@ var contactCapability = {
58238
58987
  * (empty array for the other kinds). It lives in the slice because
58239
58988
  * HA `select.*` entities may update their options at runtime.
58240
58989
  *
58241
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58242
- * `kind === 'numeric'` and are absent (undefined) for all other
58243
- * kinds. They are populated live from HA attributes on each state
58244
- * push — consistent with `options` and the sensor-unit approach.
58990
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58991
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58992
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58245
58993
  */
58246
58994
  var ControlStatusSchema = object({
58247
58995
  kind: _enum([
@@ -58253,39 +59001,17 @@ var ControlStatusSchema = object({
58253
59001
  /** Current value. Numeric kind → number; select/text/datetime → string.
58254
59002
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58255
59003
  * or time-only — HA accepts all three forms). */
58256
- value: union([number(), string()]),
59004
+ value: union([number().meta(readingMeta("device")), string()]),
58257
59005
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58258
59006
  options: array(string()),
58259
59007
  /** Ms epoch when the slice was last updated. */
58260
59008
  lastChangedAt: number(),
58261
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58262
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58263
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58264
- unit: string().optional(),
58265
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58266
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58267
- min: number().optional(),
58268
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58269
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58270
- max: number().optional(),
58271
- /** Step increment. Populated live from HA `attributes.step`.
58272
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58273
- step: number().optional(),
58274
- /** Suggested decimal places for numeric display. Populated live from HA
58275
- * `attributes.suggested_display_precision`, or derived from `step` when the
58276
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58277
- * other kinds. Falls back to auto-formatting when absent. */
58278
- precision: number().int().min(0).max(10).optional(),
58279
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58280
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58281
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58282
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58283
- * `has_date`/`has_time` attributes on each push. */
58284
- format: _enum([
58285
- "date",
58286
- "time",
58287
- "datetime"
58288
- ]).optional()
59009
+ /** Minimum allowed value, in the device quantity's base unit. */
59010
+ min: number().optional().meta(readingMeta("device")),
59011
+ /** Maximum allowed value, in the device quantity's base unit. */
59012
+ max: number().optional().meta(readingMeta("device")),
59013
+ /** Step increment, in the device quantity's base unit. */
59014
+ step: number().optional().meta(readingMeta("device"))
58289
59015
  });
58290
59016
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58291
59017
  object({
@@ -58351,9 +59077,9 @@ var CoverStatusSchema = object({
58351
59077
  ]),
58352
59078
  /** 0 = fully closed, 100 = fully open. Null when the device has
58353
59079
  * no intermediate position surface. */
58354
- position: number().min(0).max(100).nullable(),
59080
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58355
59081
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58356
- tiltPosition: number().min(0).max(100).nullable(),
59082
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58357
59083
  /** Ms epoch when the slice was last updated. */
58358
59084
  lastChangedAt: number()
58359
59085
  });
@@ -58456,9 +59182,9 @@ var NormalizedRangeSchema$1 = object({
58456
59182
  var DayNightStatusSchema = object({
58457
59183
  mode: DayNightModeSchema,
58458
59184
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58459
- sensitivity: number().optional(),
59185
+ sensitivity: number().optional().meta(readingMeta("none")),
58460
59186
  /** Delay before the IR-cut filter flips, in seconds. */
58461
- switchDelaySec: number().optional(),
59187
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58462
59188
  lastFetchedAt: number()
58463
59189
  });
58464
59190
  /**
@@ -58676,11 +59402,11 @@ var DishwasherStatusSchema = object({
58676
59402
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58677
59403
  availablePrograms: array(string()),
58678
59404
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58679
- remainingSeconds: number().int().nonnegative().nullable(),
59405
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58680
59406
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58681
- delayedStartSeconds: number().int().nonnegative().nullable(),
59407
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58682
59408
  /** Cycle completion 0..100, or null when not applicable. */
58683
- progressPercent: number().min(0).max(100).nullable(),
59409
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58684
59410
  /** True when the door is reported open; null when the door state is not known. */
58685
59411
  doorOpen: boolean().nullable(),
58686
59412
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58704,7 +59430,7 @@ var DishwasherStatusSchema = object({
58704
59430
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58705
59431
  machineCareDue: boolean().nullable(),
58706
59432
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58707
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59433
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58708
59434
  /** Option states: null when the appliance does not expose the option or has not said. */
58709
59435
  halfLoad: boolean().nullable(),
58710
59436
  hygienePlus: boolean().nullable(),
@@ -58719,7 +59445,7 @@ var DishwasherStatusSchema = object({
58719
59445
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58720
59446
  activeEvents: array(string()),
58721
59447
  /** Numeric error code when the appliance reports one; null otherwise. */
58722
- errorCode: number().nullable(),
59448
+ errorCode: number().nullable().meta(readingMeta("none")),
58723
59449
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58724
59450
  errorLabel: string().nullable(),
58725
59451
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58950,7 +59676,7 @@ var DoorbellStatusSchema = object({
58950
59676
  /** Ms epoch of the last press. null = never observed since this provider started. */
58951
59677
  lastPressedAt: number().nullable(),
58952
59678
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58953
- pressCountSinceStart: number()
59679
+ pressCountSinceStart: number().meta(readingMeta("count"))
58954
59680
  });
58955
59681
  var DoorbellPressEventSchema = object({
58956
59682
  deviceId: number(),
@@ -59015,17 +59741,7 @@ onPressed: {
59015
59741
  history: "none",
59016
59742
  customize: doorbellCustomize
59017
59743
  };
59018
- /**
59019
- * Enum-state sensor — a string value picked from a finite option set.
59020
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59021
- * states, weather conditions, contact-source channels, mode strings).
59022
- * The option set is stable across observations; it is persisted in the
59023
- * device config blob (via the control slice at adoption) and is NOT
59024
- * stored in `sourceInfo`.
59025
- */
59026
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59027
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59028
- var EnumSensorDateTimeFormatSchema = _enum([
59744
+ _enum([
59029
59745
  "date",
59030
59746
  "time",
59031
59747
  "datetime"
@@ -59033,12 +59749,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59033
59749
  var EnumSensorStatusSchema = object({
59034
59750
  value: string(),
59035
59751
  /**
59036
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59037
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59038
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59039
- * a time-only sensor → `'time'`.
59752
+ * Set when `value` is a timestamp rather than an enum state, so history
59753
+ * does not learn ISO strings as states. The display shape (date / time /
59754
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59040
59755
  */
59041
- format: EnumSensorDateTimeFormatSchema.optional(),
59756
+ timestamp: boolean().optional(),
59042
59757
  /** Ms epoch when the slice was last updated. */
59043
59758
  lastFetchedAt: number()
59044
59759
  });
@@ -59065,11 +59780,11 @@ var enumSensorCapability = {
59065
59780
  * History (D850): the value as an `enum` series (an appliance's program or
59066
59781
  * operation state, an HVAC action). The option set is per device, so the
59067
59782
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59068
- * `format` carries a timestamp, not a state, and is not recorded.
59783
+ * `timestamp` carries a clock, not a state, and is not recorded.
59069
59784
  */
59070
59785
  history: { fields: { value: {
59071
59786
  kind: "enum",
59072
- skipWhenSet: "format"
59787
+ skipWhenSet: "timestamp"
59073
59788
  } } },
59074
59789
  /** Clock fields: written, but excluded from the compare that decides
59075
59790
  * whether persisting is worth a SQLite commit. */
@@ -59089,13 +59804,13 @@ var EventFireSchema = object({
59089
59804
  deviceId: number(),
59090
59805
  eventType: string(),
59091
59806
  data: record(string(), unknown()).nullable(),
59092
- timestamp: number(),
59093
- seq: number()
59807
+ timestamp: number().meta(readingMeta("none")),
59808
+ seq: number().meta(readingMeta("count"))
59094
59809
  });
59095
59810
  var EventEmitterStatusSchema = object({
59096
59811
  eventTypes: array(string()),
59097
59812
  lastEvent: EventFireSchema.nullable(),
59098
- eventCountSinceStart: number()
59813
+ eventCountSinceStart: number().meta(readingMeta("count"))
59099
59814
  });
59100
59815
  var eventEmitterCapability = {
59101
59816
  name: "event-emitter",
@@ -59508,10 +60223,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59508
60223
  var FanControlStatusSchema = object({
59509
60224
  /** Active speed as 0..100 inclusive. Null when the device has no
59510
60225
  * speed surface (single-speed fan). */
59511
- percentage: number().min(0).max(100).nullable(),
60226
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59512
60227
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59513
60228
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59514
- percentageStep: number().positive().optional(),
60229
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59515
60230
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59516
60231
  * — empty string when no preset is active or supported. */
59517
60232
  preset: string(),
@@ -59632,7 +60347,7 @@ var FeatureProbeStatusSchema = object({
59632
60347
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59633
60348
  model: string().nullable(),
59634
60349
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59635
- channelCount: number().nullable(),
60350
+ channelCount: number().nullable().meta(readingMeta("count")),
59636
60351
  /**
59637
60352
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59638
60353
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59755,9 +60470,151 @@ var gasCapability = {
59755
60470
  * whether persisting is worth a SQLite commit. */
59756
60471
  volatileStateFields: ["lastChangedAt"]
59757
60472
  };
60473
+ var GroupCommandsSchema = object({
60474
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60475
+ results: array(object({
60476
+ deviceId: number().int().nonnegative(),
60477
+ cap: string().min(1),
60478
+ method: string().min(1),
60479
+ ok: boolean(),
60480
+ error: string().optional(),
60481
+ /** Ms epoch of the attempt. */
60482
+ at: number()
60483
+ })) });
60484
+ var groupCommandsCapability = {
60485
+ name: "group-commands",
60486
+ scope: "device",
60487
+ deviceNative: true,
60488
+ mode: "singleton",
60489
+ deviceTypes: [
60490
+ DeviceType.Switch,
60491
+ DeviceType.Light,
60492
+ DeviceType.Fan,
60493
+ DeviceType.Cover,
60494
+ DeviceType.Valve,
60495
+ DeviceType.Lock,
60496
+ DeviceType.MediaPlayer,
60497
+ DeviceType.Thermostat,
60498
+ DeviceType.Climate
60499
+ ],
60500
+ methods: {},
60501
+ status: {
60502
+ schema: GroupCommandsSchema,
60503
+ kind: "push"
60504
+ },
60505
+ runtimeState: GroupCommandsSchema,
60506
+ /**
60507
+ * Runtime-state durability: **session** — the last attempt. A restored
60508
+ * failure would outlive the command that caused it.
60509
+ *
60510
+ * See `RuntimeStateDurability`. Enforced by
60511
+ * `scripts/check-runtime-state-durability.ts`.
60512
+ */
60513
+ durability: "session",
60514
+ history: "none"
60515
+ };
60516
+ /**
60517
+ * Whether a helper actuator group counts as active (D944): at least one
60518
+ * included member is, or every included member is. What "active" means is the
60519
+ * type's: a cover is open, a lock is locked, a media player is playing, a
60520
+ * climate device is not off. A group of lights, switches and fans reports
60521
+ * `switch.on` instead; this cap is for the types whose own state is not a
60522
+ * yes/no. Unknown members are left out (`group-status` counts them) and an
60523
+ * empty included set takes the device offline, never `active: false`.
60524
+ */
60525
+ var GroupActiveSchema = object({
60526
+ active: boolean().meta({ label: "Active" }),
60527
+ /** Ms epoch when the slice was last updated. */
60528
+ lastChangedAt: number()
60529
+ });
60530
+ var groupActiveCapability = {
60531
+ name: "group-active",
60532
+ scope: "device",
60533
+ deviceNative: true,
60534
+ mode: "singleton",
60535
+ deviceTypes: [
60536
+ DeviceType.Cover,
60537
+ DeviceType.Valve,
60538
+ DeviceType.Lock,
60539
+ DeviceType.MediaPlayer,
60540
+ DeviceType.Climate,
60541
+ DeviceType.Thermostat
60542
+ ],
60543
+ methods: {},
60544
+ status: {
60545
+ schema: GroupActiveSchema,
60546
+ kind: "push"
60547
+ },
60548
+ runtimeState: GroupActiveSchema,
60549
+ /**
60550
+ * Runtime-state durability: **session** — recomputed from the members on the
60551
+ * next source event; a restored value would outlive the members it named.
60552
+ *
60553
+ * See `RuntimeStateDurability`. Enforced by
60554
+ * `scripts/check-runtime-state-durability.ts`.
60555
+ */
60556
+ durability: "session",
60557
+ history: "none"
60558
+ };
60559
+ /**
60560
+ * How many members of a helper group answered (D944). Counts are integers,
60561
+ * not measurements: zero included is a real count, never a stand-in reading.
60562
+ * The reading itself stays on the output capability. History is none — the
60563
+ * series that later statistics read is the output cap's own history block.
60564
+ */
60565
+ var GroupStatusSchema = object({
60566
+ /** Members whose current value entered the aggregate. */
60567
+ included: number().int().nonnegative().meta(readingMeta("count", {
60568
+ precision: 0,
60569
+ label: "Reporting"
60570
+ })),
60571
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60572
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60573
+ precision: 0,
60574
+ label: "Excluded"
60575
+ })),
60576
+ /** `included` + `excluded`. */
60577
+ total: number().int().nonnegative().meta(readingMeta("count", {
60578
+ precision: 0,
60579
+ label: "Members"
60580
+ }))
60581
+ });
60582
+ var groupStatusCapability = {
60583
+ name: "group-status",
60584
+ scope: "device",
60585
+ deviceNative: true,
60586
+ mode: "singleton",
60587
+ deviceTypes: [
60588
+ DeviceType.Sensor,
60589
+ DeviceType.Switch,
60590
+ DeviceType.Light,
60591
+ DeviceType.Fan,
60592
+ DeviceType.Cover,
60593
+ DeviceType.Valve,
60594
+ DeviceType.Lock,
60595
+ DeviceType.MediaPlayer,
60596
+ DeviceType.Thermostat,
60597
+ DeviceType.Climate
60598
+ ],
60599
+ methods: {},
60600
+ status: {
60601
+ schema: GroupStatusSchema,
60602
+ kind: "push"
60603
+ },
60604
+ runtimeState: GroupStatusSchema,
60605
+ /**
60606
+ * Runtime-state durability: **session** — recomputed from members on the
60607
+ * next source event; a restored count would outlive the readings it named.
60608
+ *
60609
+ * See `RuntimeStateDurability`. Enforced by
60610
+ * `scripts/check-runtime-state-durability.ts`.
60611
+ */
60612
+ durability: "session",
60613
+ history: "none"
60614
+ };
59758
60615
  var HeatingValveStatusSchema = object({
59759
60616
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59760
- position: number().min(0).max(100).nullable(),
60617
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59761
60618
  /** Ms epoch when the slice was last updated (push or poll). */
59762
60619
  lastFetchedAt: number()
59763
60620
  });
@@ -59800,9 +60657,9 @@ var HumidifierStatusSchema = object({
59800
60657
  /** Whether the humidifier is currently on. */
59801
60658
  on: boolean(),
59802
60659
  /** Current measured relative humidity (0..100). Null when not reported. */
59803
- currentHumidity: number().min(0).max(100).nullable(),
60660
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59804
60661
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59805
- targetHumidity: number().min(0).max(100).nullable(),
60662
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59806
60663
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59807
60664
  * device has no mode surface. */
59808
60665
  mode: string().nullable(),
@@ -59812,9 +60669,9 @@ var HumidifierStatusSchema = object({
59812
60669
  * `idle` / `off`). Null when the device doesn't report it. */
59813
60670
  action: string().nullable(),
59814
60671
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59815
- minHumidity: number().nullable(),
60672
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59816
60673
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59817
- maxHumidity: number().nullable(),
60674
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59818
60675
  /** Ms epoch when the slice was last updated. */
59819
60676
  lastChangedAt: number()
59820
60677
  });
@@ -59884,22 +60741,13 @@ var humidifierCapability = {
59884
60741
  * `sourceInfo`.
59885
60742
  */
59886
60743
  var HumiditySensorStatusSchema = object({
59887
- /** Current relative humidity, 0..100. */
59888
- percent: number().min(0).max(100).meta({
59889
- unit: "%",
60744
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60745
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60746
+ precision: 0,
59890
60747
  label: "Humidity"
59891
- }),
60748
+ })),
59892
60749
  /** Ms epoch when the slice was last updated. */
59893
- lastFetchedAt: number(),
59894
- /** Live display unit from the upstream source (e.g. HA
59895
- * `attributes.unit_of_measurement`). The UI prefers this over the
59896
- * role's canonical unit. Absent → fall back to the canonical unit. */
59897
- unit: string().optional(),
59898
- /** Suggested decimal places for numeric display.
59899
- * Populated live from the upstream source when provided (e.g. HA
59900
- * `attributes.suggested_display_precision`). Falls back to
59901
- * auto-formatting when absent. */
59902
- precision: number().int().min(0).max(10).optional()
60750
+ lastFetchedAt: number()
59903
60751
  });
59904
60752
  var humiditySensorCapability = {
59905
60753
  name: "humidity-sensor",
@@ -59930,12 +60778,7 @@ var humiditySensorCapability = {
59930
60778
  } } },
59931
60779
  /** Clock fields: written, but excluded from the compare that decides
59932
60780
  * whether persisting is worth a SQLite commit. */
59933
- volatileStateFields: ["lastFetchedAt"],
59934
- customize: {
59935
- version: 1,
59936
- advanced: "derive",
59937
- targets: []
59938
- }
60781
+ volatileStateFields: ["lastFetchedAt"]
59939
60782
  };
59940
60783
  /**
59941
60784
  * Image display cap. Models a single still image exposed by an integration —
@@ -59956,7 +60799,7 @@ var ImageStatusSchema = object({
59956
60799
  * entity exposes no `entity_picture` (yet). */
59957
60800
  url: string().nullable(),
59958
60801
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59959
- lastUpdated: number().nullable()
60802
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59960
60803
  });
59961
60804
  var imageCapability = {
59962
60805
  name: "image",
@@ -60044,19 +60887,19 @@ var NormalizedRangeSchema = object({
60044
60887
  */
60045
60888
  var ImageSettingsStatusSchema = object({
60046
60889
  /** Normalized 0–100. */
60047
- brightness: number().optional(),
60890
+ brightness: number().optional().meta(readingMeta("none")),
60048
60891
  /** Normalized 0–100. */
60049
- contrast: number().optional(),
60892
+ contrast: number().optional().meta(readingMeta("none")),
60050
60893
  /** Normalized 0–100. */
60051
- saturation: number().optional(),
60894
+ saturation: number().optional().meta(readingMeta("none")),
60052
60895
  /** Normalized 0–100. */
60053
- sharpness: number().optional(),
60896
+ sharpness: number().optional().meta(readingMeta("none")),
60054
60897
  mirror: boolean().optional(),
60055
60898
  flip: boolean().optional(),
60056
60899
  rotate: ImageRotateSchema.optional(),
60057
60900
  whiteBalance: WhiteBalanceModeSchema.optional(),
60058
60901
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60059
- warmth: number().optional(),
60902
+ warmth: number().optional().meta(readingMeta("none")),
60060
60903
  exposureMode: ExposureModeSchema.optional(),
60061
60904
  backlightMode: BacklightModeSchema.optional(),
60062
60905
  lastFetchedAt: number()
@@ -60335,10 +61178,10 @@ var IntercomAbilitySchema = object({
60335
61178
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60336
61179
  codecs: array(string()),
60337
61180
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60338
- sampleRate: number(),
61181
+ sampleRate: number().meta(readingMeta("none")),
60339
61182
  duplex: _enum(["half", "full"]),
60340
61183
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60341
- maxBacklogMs: number()
61184
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60342
61185
  });
60343
61186
  var IntercomStatusSchema = object({
60344
61187
  /** True while a talk session is open. */
@@ -60565,13 +61408,13 @@ var LawnMowerControlStatusSchema = object({
60565
61408
  activity: LawnMowerActivitySchema,
60566
61409
  /** 0..100 battery percentage. Null when the device has no battery
60567
61410
  * reading. */
60568
- batteryLevel: number().min(0).max(100).nullable(),
61411
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60569
61412
  /** 0..100 mowing-completion percentage of the current task, or null when no
60570
61413
  * task is active / progress is unavailable. */
60571
- progressPercent: number().min(0).max(100).nullable(),
61414
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60572
61415
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60573
61416
  * or null when unknown. */
60574
- currentCode: number().nullable(),
61417
+ currentCode: number().nullable().meta(readingMeta("none")),
60575
61418
  /** Human label for {@link currentCode}, or null when undecodable. */
60576
61419
  currentCodeLabel: string().nullable(),
60577
61420
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61024,7 +61867,14 @@ var lockControlCapability = {
61024
61867
  }),
61025
61868
  unlock: method(object({
61026
61869
  deviceId: number().int().nonnegative(),
61027
- code: string().min(1).optional()
61870
+ code: string().min(1).optional(),
61871
+ /**
61872
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
61873
+ * with one call is refused unless the caller says it was confirmed. A
61874
+ * single lock ignores it. The refusal lives in the callee, so no
61875
+ * automation or API caller reaches the doors by skipping a dialog.
61876
+ */
61877
+ confirm: boolean().optional()
61028
61878
  }), _void(), {
61029
61879
  kind: "mutation",
61030
61880
  auth: "protected"
@@ -61061,6 +61911,44 @@ var lockControlCapability = {
61061
61911
  volatileStateFields: ["lastChangedAt"]
61062
61912
  };
61063
61913
  /**
61914
+ * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
61915
+ * so dashboards keep one series; every other instantaneous reading lands here.
61916
+ * `value` is the time integral in the source quantity's accumulated base
61917
+ * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
61918
+ */
61919
+ var MeterStatusSchema = object({
61920
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
61921
+ lastFetchedAt: number()
61922
+ });
61923
+ var meterCapability = {
61924
+ name: "meter",
61925
+ scope: "device",
61926
+ deviceNative: true,
61927
+ mode: "singleton",
61928
+ deviceTypes: [DeviceType.Sensor],
61929
+ methods: {},
61930
+ status: {
61931
+ schema: MeterStatusSchema,
61932
+ kind: "push"
61933
+ },
61934
+ runtimeState: MeterStatusSchema,
61935
+ /**
61936
+ * Runtime-state durability: **restored** — the total is a cache of the
61937
+ * history integral. A restart shows it only until the history pass
61938
+ * recomputes the open period; it is not an in-memory buffer.
61939
+ *
61940
+ * See `RuntimeStateDurability`. Enforced by
61941
+ * `scripts/check-runtime-state-durability.ts`.
61942
+ */
61943
+ durability: "restored",
61944
+ history: { fields: { value: {
61945
+ kind: "counter",
61946
+ deadband: .001,
61947
+ minIntervalMs: 6e4
61948
+ } } },
61949
+ volatileStateFields: ["lastFetchedAt"]
61950
+ };
61951
+ /**
61064
61952
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
61065
61953
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
61066
61954
  * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
@@ -61104,7 +61992,7 @@ var MediaPlayerStatusSchema = object({
61104
61992
  state: MediaPlayerStateSchema,
61105
61993
  /** Volume as 0..100 inclusive. Null when the device has no volume
61106
61994
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61107
- volumeLevel: number().min(0).max(100).nullable(),
61995
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61108
61996
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61109
61997
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61110
61998
  isMuted: boolean().nullable(),
@@ -61117,10 +62005,10 @@ var MediaPlayerStatusSchema = object({
61117
62005
  currentMedia: MediaInfoSchema.nullable(),
61118
62006
  /** Current playback position in ms. Null when not seekable or
61119
62007
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61120
- positionMs: number().int().nonnegative().nullable(),
62008
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61121
62009
  /** Total duration of the current media in ms. Null when unknown
61122
62010
  * (live stream) or nothing is playing. */
61123
- durationMs: number().int().nonnegative().nullable(),
62011
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61124
62012
  /** Shuffle toggle. Null when no shuffle surface.
61125
62013
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61126
62014
  shuffle: boolean().nullable(),
@@ -61429,7 +62317,7 @@ var motionCustomize = {
61429
62317
  eventName: "onMotionChanged"
61430
62318
  },
61431
62319
  deviceTypes: [DeviceType.Camera],
61432
- apply: "replace",
62320
+ apply: "add",
61433
62321
  applyAuthority: {
61434
62322
  kind: "camera-sensors",
61435
62323
  timeoutField: "motionCooldownMs"
@@ -61501,7 +62389,7 @@ var MotionStatusSchema = object({
61501
62389
  * it, every provider seeded it at boot, and every session left it
61502
62390
  * `detected: true` after its detach.
61503
62391
  */
61504
- autoClearAfterMs: number().nullable()
62392
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61505
62393
  });
61506
62394
  /**
61507
62395
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61538,12 +62426,18 @@ var motionCapability = {
61538
62426
  methods: {
61539
62427
  /**
61540
62428
  * Pull the current motion state synchronously. Convenience shortcut
61541
- * for consumers that don't need the full status object; equivalent
61542
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61543
- * `getStatus` once the auto-injected status surface lands in every
61544
- * consumer.
62429
+ * for consumers that don't need the full status object. A composed
62430
+ * group answers from its slice (D944): unknown throws, it is not off.
61545
62431
  */
61546
62432
  isDetected: method(object({ deviceId: number() }), boolean()) },
62433
+ /**
62434
+ * A composed motion group serves `isDetected` from `detected` (D944).
62435
+ * Without this the cap stays uncomposable (`has-commands`).
62436
+ */
62437
+ composedMethods: { isDetected: {
62438
+ kind: "slice",
62439
+ field: "detected"
62440
+ } },
61547
62441
  events: {
61548
62442
  /**
61549
62443
  * Fires every time the runner transitions a camera between
@@ -61683,7 +62577,7 @@ var MotionZoneRegionSchema = object({
61683
62577
  * the grid region(s). */
61684
62578
  var MotionZoneStatusSchema = object({
61685
62579
  enabled: boolean(),
61686
- sensitivity: number(),
62580
+ sensitivity: number().meta(readingMeta("none")),
61687
62581
  /** Grid region(s). Today exactly one `grid` shape. */
61688
62582
  regions: array(MotionZoneRegionSchema),
61689
62583
  lastFetchedAt: number()
@@ -62053,7 +62947,7 @@ var NavigationStatusSchema = object({
62053
62947
  following: boolean(),
62054
62948
  flash: boolean(),
62055
62949
  lightMode: NavigationLightModeSchema,
62056
- lightLevel: number().min(40).max(100),
62950
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62057
62951
  /** Ms epoch when the slice was last updated. */
62058
62952
  lastChangedAt: number()
62059
62953
  });
@@ -62187,13 +63081,13 @@ var NetworkLinkStatusSchema = object({
62187
63081
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62188
63082
  * SKIP a null rather than coerce it.
62189
63083
  */
62190
- signalPercent: number().min(0).max(100).nullable(),
63084
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62191
63085
  /** Raw received signal strength in dBm, when the firmware reports one. */
62192
- rssiDbm: number().optional(),
63086
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62193
63087
  /** Network name of a wireless link, when the firmware reports it. */
62194
63088
  ssid: string().optional(),
62195
63089
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62196
- lastUpdated: number()
63090
+ lastUpdated: number().meta(readingMeta("none"))
62197
63091
  });
62198
63092
  var networkLinkCapability = {
62199
63093
  name: "network-link",
@@ -62578,7 +63472,7 @@ var NotifierStatusSchema = object({
62578
63472
  lastError: string().nullable(),
62579
63473
  /** Number of deliveries currently buffered server-side (rate-limited
62580
63474
  * notifiers may queue). 0 when the provider sends synchronously. */
62581
- queueDepth: number().int().nonnegative(),
63475
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62582
63476
  /** Per-feature capability matrix — authoritative source for the
62583
63477
  * compose-form field gating. */
62584
63478
  supports: NotifierSupportsSchema
@@ -62670,11 +63564,9 @@ onSent: { data: object({
62670
63564
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62671
63565
  * for: water flow, distance, weight, frequency, signal strength, …).
62672
63566
  *
62673
- * The `unit` and `precision` fields carry the live display metadata that
62674
- * the upstream source provides with each state push (HA
62675
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62676
- * The UI reads them directly from the slice — they are the single source
62677
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63567
+ * The quantity is not on this slice. It is chosen when the device is
63568
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63569
+ * that quantity's base unit. `null` is a refused sample (D393).
62678
63570
  *
62679
63571
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62680
63572
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62683,15 +63575,8 @@ onSent: { data: object({
62683
63575
  * numeric values land here.
62684
63576
  */
62685
63577
  var NumericSensorStatusSchema = object({
62686
- value: number(),
62687
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62688
- * Populated live from the upstream source on each state push.
62689
- * Absent for unitless sensors. */
62690
- unit: string().optional(),
62691
- /** Suggested decimal places for numeric display.
62692
- * Populated live from the upstream source when provided.
62693
- * Falls back to auto-formatting when absent. */
62694
- precision: number().int().min(0).max(10).optional(),
63578
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63579
+ value: number().nullable().meta(readingMeta("device")),
62695
63580
  /** Ms epoch when the slice was last updated. */
62696
63581
  lastFetchedAt: number()
62697
63582
  });
@@ -62715,11 +63600,11 @@ var numericSensorCapability = {
62715
63600
  */
62716
63601
  durability: "restored",
62717
63602
  /**
62718
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63603
+ * History (D850): recorded series. The unit is the device quantity's base,
63604
+ * resolved when the series is read — the cap has no single unit.
62719
63605
  */
62720
63606
  history: { fields: { value: {
62721
63607
  kind: "gauge",
62722
- unitFrom: "unit",
62723
63608
  minIntervalMs: 3e4
62724
63609
  } } },
62725
63610
  /** Clock fields: written, but excluded from the compare that decides
@@ -63011,23 +63896,26 @@ var PetFeederStatusSchema = object({
63011
63896
  /** Food currently in the bowl (grams). Null when the device has not
63012
63897
  * reported a reading yet. On dual-hopper models this is the combined
63013
63898
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63014
- foodLevel: number().nullable(),
63899
+ foodLevel: number().nullable().meta(readingMeta("none")),
63015
63900
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63016
63901
  * single-hopper models. */
63017
- food1: number().nullable(),
63902
+ food1: number().nullable().meta(readingMeta("none")),
63018
63903
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63019
63904
  * single-hopper models. */
63020
- food2: number().nullable(),
63905
+ food2: number().nullable().meta(readingMeta("none")),
63021
63906
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63022
63907
  * (`device_class: problem`, on = low). True when the bowl is empty /
63023
63908
  * below the feeder's low threshold. */
63024
63909
  lowFood: boolean(),
63025
63910
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63026
63911
  * device has no battery reading. */
63027
- batteryPower: number().min(0).max(100).nullable(),
63028
- /** Days of desiccant life remaining. Null when the model has no
63029
- * desiccant sensor. */
63030
- desiccantLeftDays: number().nullable(),
63912
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63913
+ /**
63914
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63915
+ * Providers convert from the days the feeder reports. Null when the
63916
+ * model has no desiccant sensor.
63917
+ */
63918
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63031
63919
  /** True while a feed is in progress. */
63032
63920
  feeding: boolean(),
63033
63921
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63038,7 +63926,7 @@ var PetFeederStatusSchema = object({
63038
63926
  * with `errorCode` for consumers that want the raw integer. */
63039
63927
  error: string().nullable(),
63040
63928
  /** Raw device error code (0 / null = no error). */
63041
- errorCode: number().nullable(),
63929
+ errorCode: number().nullable().meta(readingMeta("none")),
63042
63930
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63043
63931
  isDualHopper: boolean(),
63044
63932
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63048,7 +63936,7 @@ var PetFeederStatusSchema = object({
63048
63936
  /** Play a chime when dispensing. */
63049
63937
  feedSound: boolean(),
63050
63938
  /** Speaker / prompt volume level (device-scaled integer). */
63051
- volume: number(),
63939
+ volume: number().meta(readingMeta("none")),
63052
63940
  /** Ms epoch when the slice was last refreshed from the cloud. */
63053
63941
  lastFetchedAt: number()
63054
63942
  });
@@ -63167,7 +64055,7 @@ var petFeederCapability = {
63167
64055
  },
63168
64056
  desiccantLeftDays: {
63169
64057
  kind: "gauge",
63170
- unit: "days"
64058
+ unit: "s"
63171
64059
  }
63172
64060
  } }
63173
64061
  };
@@ -63568,43 +64456,88 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63568
64456
  */
63569
64457
  var PowerMeterStatusSchema = object({
63570
64458
  /** Instantaneous power draw in watts. */
63571
- watts: number().optional().meta({
63572
- unit: "W",
63573
- label: "Power"
63574
- }),
64459
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63575
64460
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63576
- kwhTotal: number().optional().meta({
63577
- unit: "kWh",
63578
- label: "Energy"
63579
- }),
64461
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63580
64462
  /** Voltage in volts. */
63581
- volts: number().optional(),
64463
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
63582
64464
  /** Current in amperes. */
63583
- amps: number().optional(),
63584
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63585
- va: number().optional(),
64465
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
64466
+ /** Apparent power in volt-amperes. */
64467
+ va: number().optional().meta(readingMeta("apparent-power")),
63586
64468
  /** Power factor (0..1) if reported. */
63587
- powerFactor: number().min(0).max(1).optional(),
64469
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63588
64470
  /** Ms epoch when the slice was last updated. */
63589
- lastFetchedAt: number(),
63590
- /** Live display unit of the single metric this slice carries (e.g. HA
63591
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63592
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63593
- * unit per slice is unambiguous. The UI prefers this over the role's
63594
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63595
- unit: string().optional(),
63596
- /** Suggested decimal places for numeric display.
63597
- * Populated live from the upstream source when provided (e.g. HA
63598
- * `attributes.suggested_display_precision`). Falls back to
63599
- * auto-formatting when absent. */
63600
- precision: number().int().min(0).max(10).optional()
64471
+ lastFetchedAt: number()
63601
64472
  });
64473
+ /**
64474
+ * Loads that draw metered mains power (D951). The reading is a feature of the
64475
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
64476
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
64477
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
64478
+ * camera and the like are left out: their draw is not what anyone meters.
64479
+ */
64480
+ var POWER_METER_DEVICE_TYPES = [
64481
+ DeviceType.Sensor,
64482
+ DeviceType.Switch,
64483
+ DeviceType.Light,
64484
+ DeviceType.Fan,
64485
+ DeviceType.Climate,
64486
+ DeviceType.Thermostat,
64487
+ DeviceType.Humidifier,
64488
+ DeviceType.WaterHeater,
64489
+ DeviceType.MediaPlayer,
64490
+ DeviceType.Washer,
64491
+ DeviceType.Dryer,
64492
+ DeviceType.Dishwasher,
64493
+ DeviceType.Vacuum,
64494
+ DeviceType.LawnMower,
64495
+ DeviceType.PetFeeder
64496
+ ];
64497
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
64498
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
64499
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
64500
+ function powerTarget(id, unit) {
64501
+ return {
64502
+ id,
64503
+ cardinality: "scalar",
64504
+ vocabulary: {
64505
+ cap: "power-meter",
64506
+ stateField: id
64507
+ },
64508
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
64509
+ apply: "add",
64510
+ path: id,
64511
+ sources: [{
64512
+ kind: "numeric",
64513
+ requiredUnit: unit,
64514
+ transforms: [{ kind: "identity" }]
64515
+ }],
64516
+ companions: [{
64517
+ kind: "clock",
64518
+ path: "lastFetchedAt",
64519
+ when: "always"
64520
+ }],
64521
+ sourceReport: { status: "assignment-status" },
64522
+ advancedExpression: true
64523
+ };
64524
+ }
64525
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
64526
+ var powerMeterCustomize = {
64527
+ version: 1,
64528
+ targets: [
64529
+ powerTarget("watts", "W"),
64530
+ powerTarget("kwhTotal", "kWh"),
64531
+ powerTarget("volts", "V"),
64532
+ powerTarget("amps", "A")
64533
+ ]
64534
+ };
63602
64535
  var powerMeterCapability = {
63603
64536
  name: "power-meter",
63604
64537
  scope: "device",
63605
64538
  deviceNative: true,
63606
64539
  mode: "singleton",
63607
- deviceTypes: [DeviceType.Sensor],
64540
+ deviceTypes: POWER_METER_DEVICE_TYPES,
63608
64541
  methods: {},
63609
64542
  status: {
63610
64543
  schema: PowerMeterStatusSchema,
@@ -63662,11 +64595,7 @@ var powerMeterCapability = {
63662
64595
  /** Clock fields: written, but excluded from the compare that decides
63663
64596
  * whether persisting is worth a SQLite commit. */
63664
64597
  volatileStateFields: ["lastFetchedAt"],
63665
- customize: {
63666
- version: 1,
63667
- advanced: "derive",
63668
- targets: []
63669
- }
64598
+ customize: powerMeterCustomize
63670
64599
  };
63671
64600
  /**
63672
64601
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63686,11 +64615,11 @@ var powerMeterCapability = {
63686
64615
  */
63687
64616
  var GpsLocationSchema = object({
63688
64617
  /** Latitude in decimal degrees, -90..90. */
63689
- latitude: number().min(-90).max(90),
64618
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63690
64619
  /** Longitude in decimal degrees, -180..180. */
63691
- longitude: number().min(-180).max(180),
64620
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63692
64621
  /** Reported accuracy in meters (lower = better). */
63693
- accuracyMeters: number().nonnegative()
64622
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63694
64623
  });
63695
64624
  var PresenceStatusSchema = object({
63696
64625
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63702,7 +64631,7 @@ var PresenceStatusSchema = object({
63702
64631
  gps: GpsLocationSchema.nullable(),
63703
64632
  /** Optional battery level of the tracking device (0..100). Null
63704
64633
  * when not reported. */
63705
- batteryPercent: number().min(0).max(100).nullable(),
64634
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63706
64635
  /** Ms epoch when the slice was last updated. */
63707
64636
  lastChangedAt: number()
63708
64637
  });
@@ -63752,19 +64681,10 @@ var presenceCapability = {
63752
64681
  * `sourceInfo`.
63753
64682
  */
63754
64683
  var PressureSensorStatusSchema = object({
63755
- /** Current pressure in hPa. */
63756
- hpa: number(),
64684
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64685
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63757
64686
  /** Ms epoch when the slice was last updated. */
63758
- lastFetchedAt: number(),
63759
- /** Live display unit from the upstream source (e.g. HA
63760
- * `attributes.unit_of_measurement`). The UI prefers this over the
63761
- * role's canonical unit. Absent → fall back to the canonical unit. */
63762
- unit: string().optional(),
63763
- /** Suggested decimal places for numeric display.
63764
- * Populated live from the upstream source when provided (e.g. HA
63765
- * `attributes.suggested_display_precision`). Falls back to
63766
- * auto-formatting when absent. */
63767
- precision: number().int().min(0).max(10).optional()
64687
+ lastFetchedAt: number()
63768
64688
  });
63769
64689
  var pressureSensorCapability = {
63770
64690
  name: "pressure-sensor",
@@ -64178,8 +65098,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64178
65098
  }), _void(), { kind: "mutation" });
64179
65099
  var PtzAutotrackSettingsSchema = object({
64180
65100
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64181
- stopDelaySeconds: number().int().min(0).max(300),
64182
- disappearDelaySeconds: number().int().min(0).max(300)
65101
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65102
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64183
65103
  });
64184
65104
  /**
64185
65105
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64333,7 +65253,7 @@ var RunningStateStatusSchema = object({
64333
65253
  /** null = unknown — never coerced to false (D393). */
64334
65254
  running: boolean().nullable(),
64335
65255
  /** Epoch ms the current boolean began; null while `running` is null. */
64336
- since: number().nullable(),
65256
+ since: number().nullable().meta(readingMeta("none")),
64337
65257
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64338
65258
  phase: string().nullable(),
64339
65259
  /** null until a reading has been published (D393). */
@@ -64348,7 +65268,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64348
65268
  /** Generic Customize target for the running boolean (D936). */
64349
65269
  var runningCustomize = {
64350
65270
  version: 1,
64351
- advanced: "derive",
64352
65271
  targets: [{
64353
65272
  id: "running",
64354
65273
  cardinality: "scalar",
@@ -64357,6 +65276,8 @@ var runningCustomize = {
64357
65276
  stateField: "running"
64358
65277
  },
64359
65278
  deviceTypes: [
65279
+ DeviceType.Washer,
65280
+ DeviceType.Dryer,
64360
65281
  DeviceType.Dishwasher,
64361
65282
  DeviceType.Vacuum,
64362
65283
  DeviceType.LawnMower,
@@ -64368,7 +65289,7 @@ var runningCustomize = {
64368
65289
  DeviceType.Thermostat,
64369
65290
  DeviceType.PetFeeder
64370
65291
  ],
64371
- apply: "add-or-replace",
65292
+ apply: "add",
64372
65293
  path: "running",
64373
65294
  sources: [
64374
65295
  {
@@ -65632,9 +66553,9 @@ var RecordTriggerSchema = _enum([
65632
66553
  */
65633
66554
  var RecordWindowSchema = object({
65634
66555
  trigger: RecordTriggerSchema,
65635
- day: number().int().min(0).max(6),
65636
- startMinute: number().int().min(0).max(1439),
65637
- endMinute: number().int().min(1).max(1440)
66556
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66557
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66558
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65638
66559
  });
65639
66560
  /** Status of one physical volume, as the camera itself describes it. */
65640
66561
  var OnboardStorageVolumeSchema = object({
@@ -65710,10 +66631,10 @@ var RecordingOnboardStatusSchema = object({
65710
66631
  /** Master "record to the card at all" switch. */
65711
66632
  enabled: boolean().nullable(),
65712
66633
  overwriteWhenFull: boolean().nullable(),
65713
- preRecordSec: number().nullable(),
65714
- postRecordSec: number().nullable(),
66634
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66635
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65715
66636
  /** Length of one recorded file, in minutes. */
65716
- segmentMinutes: number().nullable(),
66637
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65717
66638
  /** The primary track's weekly windows, flattened. */
65718
66639
  windows: array(RecordWindowSchema),
65719
66640
  /**
@@ -65729,7 +66650,7 @@ var RecordingOnboardStatusSchema = object({
65729
66650
  * provider reporting a non-zero count also reports the schedule as not
65730
66651
  * writable.
65731
66652
  */
65732
- unreadableWindows: number(),
66653
+ unreadableWindows: number().meta(readingMeta("count")),
65733
66654
  /**
65734
66655
  * The camera is scheduled to record and has NO usable storage.
65735
66656
  *
@@ -66574,7 +67495,7 @@ var CamStreamDescriptorSchema = object({
66574
67495
  url: string().optional(),
66575
67496
  codec: string().optional(),
66576
67497
  resolution: CamStreamResolutionSchema.optional(),
66577
- fps: number().positive().optional(),
67498
+ fps: number().positive().optional().meta(readingMeta("rate")),
66578
67499
  label: string().optional(),
66579
67500
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66580
67501
  deviceFeatures: array(string()).optional(),
@@ -66661,18 +67582,18 @@ var StreamProfileSchema = _enum([
66661
67582
  ]);
66662
67583
  /** The current encoding config of one stream profile. */
66663
67584
  var StreamProfileConfigSchema = object({
66664
- width: number(),
66665
- height: number(),
67585
+ width: number().meta(readingMeta("none")),
67586
+ height: number().meta(readingMeta("none")),
66666
67587
  codec: _enum(["h264", "h265"]),
66667
- framerate: number(),
66668
- bitrate: number(),
67588
+ framerate: number().meta(readingMeta("rate")),
67589
+ bitrate: number().meta(readingMeta("bitrate")),
66669
67590
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66670
67591
  encoderProfile: _enum([
66671
67592
  "high",
66672
67593
  "main",
66673
67594
  "baseline"
66674
67595
  ]).optional(),
66675
- gop: number().optional(),
67596
+ gop: number().optional().meta(readingMeta("none")),
66676
67597
  /**
66677
67598
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66678
67599
  *
@@ -66846,7 +67767,8 @@ var switchCapability = {
66846
67767
  deviceTypes: [
66847
67768
  DeviceType.Switch,
66848
67769
  DeviceType.Siren,
66849
- DeviceType.Light
67770
+ DeviceType.Light,
67771
+ DeviceType.Fan
66850
67772
  ],
66851
67773
  methods: { setState: method(object({
66852
67774
  deviceId: number(),
@@ -66893,12 +67815,7 @@ var switchCapability = {
66893
67815
  label: "On",
66894
67816
  kind: "boolean"
66895
67817
  }
66896
- }] },
66897
- customize: {
66898
- version: 1,
66899
- advanced: "derive",
66900
- targets: []
66901
- }
67818
+ }] }
66902
67819
  };
66903
67820
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66904
67821
  function hasControlCharacter(value) {
@@ -67481,35 +68398,21 @@ var tamperCapability = {
67481
68398
  * entries with `device_class: temperature` and any future native
67482
68399
  * thermometer.
67483
68400
  *
67484
- * Unit normalisation: providers convert to Celsius before storing.
67485
- * The slice value is always Celsius so cross-cap aggregators
67486
- * (climate-control's `currentTemp`, energy analytics) can compose
67487
- * without per-source unit fixups. The canonical display unit (`°C`) is
67488
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67489
- * `sourceInfo`.
68401
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68402
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67490
68403
  *
67491
68404
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67492
68405
  * frozen feed (provider hung) distinct from a "temperature hasn't
67493
68406
  * changed" steady state.
67494
68407
  */
67495
68408
  var TemperatureSensorStatusSchema = object({
67496
- /** Current temperature in Celsius. */
67497
- celsius: number().meta({
67498
- unit: "°C",
68409
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68410
+ celsius: number().nullable().meta(readingMeta("temperature", {
68411
+ precision: 1,
67499
68412
  label: "Temperature"
67500
- }),
68413
+ })),
67501
68414
  /** Ms epoch when the slice was last updated (push or poll). */
67502
- lastFetchedAt: number(),
67503
- /** Live display unit from the upstream source (e.g. HA
67504
- * `attributes.unit_of_measurement`). The UI prefers this over the
67505
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67506
- * Absent → the UI falls back to the role's canonical unit. */
67507
- unit: string().optional(),
67508
- /** Suggested decimal places for numeric display.
67509
- * Populated live from the upstream source when provided (e.g. HA
67510
- * `attributes.suggested_display_precision`). Falls back to
67511
- * auto-formatting when absent. */
67512
- precision: number().int().min(0).max(10).optional()
68415
+ lastFetchedAt: number()
67513
68416
  });
67514
68417
  var temperatureSensorCapability = {
67515
68418
  name: "temperature-sensor",
@@ -67540,12 +68443,7 @@ var temperatureSensorCapability = {
67540
68443
  } } },
67541
68444
  /** Clock fields: written, but excluded from the compare that decides
67542
68445
  * whether persisting is worth a SQLite commit. */
67543
- volatileStateFields: ["lastFetchedAt"],
67544
- customize: {
67545
- version: 1,
67546
- advanced: "derive",
67547
- targets: []
67548
- }
68446
+ volatileStateFields: ["lastFetchedAt"]
67549
68447
  };
67550
68448
  /**
67551
68449
  * toast — system-scoped singleton capability that streams toast
@@ -67828,16 +68726,16 @@ var VacuumControlStatusSchema = object({
67828
68726
  ]),
67829
68727
  /** 0..100 battery percentage. Null when the device has no battery
67830
68728
  * reading. */
67831
- batteryLevel: number().min(0).max(100).nullable(),
68729
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67832
68730
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67833
68731
  * the vacuum has no speed control. */
67834
68732
  fanSpeed: string().nullable(),
67835
68733
  /** Speed tokens the hardware accepts — drives the UI selector. */
67836
68734
  availableFanSpeeds: array(string()),
67837
68735
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67838
- progressPercent: number().min(0).max(100).nullable(),
68736
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67839
68737
  /** Current error code (0 / null = no error). */
67840
- errorCode: number().nullable(),
68738
+ errorCode: number().nullable().meta(readingMeta("none")),
67841
68739
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67842
68740
  errorLabel: string().nullable(),
67843
68741
  /** Ms epoch when the slice was last updated. */
@@ -67915,7 +68813,7 @@ var ValveStatusSchema = object({
67915
68813
  ]),
67916
68814
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67917
68815
  * intermediate position surface. */
67918
- position: number().min(0).max(100).nullable(),
68816
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67919
68817
  /** Ms epoch when the slice was last updated. */
67920
68818
  lastChangedAt: number()
67921
68819
  });
@@ -68023,9 +68921,9 @@ var vibrationCapability = {
68023
68921
  */
68024
68922
  var WaterHeaterStatusSchema = object({
68025
68923
  /** Current measured temperature. Null when not reported. */
68026
- currentTemp: number().nullable(),
68924
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68027
68925
  /** Target temperature setpoint. Null when no setpoint surface. */
68028
- targetTemp: number().nullable(),
68926
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68029
68927
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68030
68928
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68031
68929
  * device reports an unknown state. */
@@ -68036,9 +68934,9 @@ var WaterHeaterStatusSchema = object({
68036
68934
  * has no away surface. */
68037
68935
  away: boolean().nullable(),
68038
68936
  /** HA `min_temp` attribute. Null when not reported. */
68039
- minTemp: number().nullable(),
68937
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68040
68938
  /** HA `max_temp` attribute. Null when not reported. */
68041
- maxTemp: number().nullable(),
68939
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68042
68940
  /** Ms epoch when the slice was last updated. */
68043
68941
  lastChangedAt: number()
68044
68942
  });
@@ -68123,25 +69021,18 @@ var waterHeaterCapability = {
68123
69021
  * Forecast deferred — current conditions only for v1.
68124
69022
  */
68125
69023
  var WeatherStatusSchema = object({
68126
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68127
- * when no condition has been reported yet. */
69024
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68128
69025
  condition: string().nullable(),
68129
- /** Current temperature in the reported unit. Null when not provided. */
68130
- temperature: number().nullable(),
68131
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68132
- temperatureUnit: string().nullable(),
69026
+ /** Current temperature in Celsius. Null when not provided or refused. */
69027
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68133
69028
  /** Relative humidity (0..100). Null when not provided. */
68134
- humidity: number().min(0).max(100).nullable(),
68135
- /** Barometric pressure in the reported unit. Null when not provided. */
68136
- pressure: number().nullable(),
68137
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68138
- pressureUnit: string().nullable(),
68139
- /** Wind speed in the reported unit. Null when not provided. */
68140
- windSpeed: number().nullable(),
68141
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68142
- windSpeedUnit: string().nullable(),
69029
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
69030
+ /** Barometric pressure in hPa. Null when not provided or refused. */
69031
+ pressure: number().nullable().meta(readingMeta("pressure")),
69032
+ /** Wind speed in metres per second. Null when not provided or refused. */
69033
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68143
69034
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68144
- windBearing: number().nullable(),
69035
+ windBearing: number().nullable().meta(readingMeta("angle")),
68145
69036
  /** Ms epoch when the slice was last updated. */
68146
69037
  lastFetchedAt: number()
68147
69038
  });
@@ -68169,12 +69060,12 @@ var weatherCapability = {
68169
69060
  */
68170
69061
  durability: "session",
68171
69062
  /**
68172
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69063
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68173
69064
  */
68174
69065
  history: { fields: {
68175
69066
  temperature: {
68176
69067
  kind: "gauge",
68177
- unitFrom: "temperatureUnit",
69068
+ unit: "°C",
68178
69069
  deadband: .2
68179
69070
  },
68180
69071
  humidity: {
@@ -68184,12 +69075,12 @@ var weatherCapability = {
68184
69075
  },
68185
69076
  pressure: {
68186
69077
  kind: "gauge",
68187
- unitFrom: "pressureUnit",
69078
+ unit: "hPa",
68188
69079
  deadband: .5
68189
69080
  },
68190
69081
  windSpeed: {
68191
69082
  kind: "gauge",
68192
- unitFrom: "windSpeedUnit",
69083
+ unit: "m/s",
68193
69084
  deadband: .5
68194
69085
  }
68195
69086
  } }
@@ -69090,8 +69981,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
69090
69981
  kind: "mutation",
69091
69982
  auth: "admin"
69092
69983
  });
69984
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
69093
69985
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69094
- 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;
69986
+ 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;
69095
69987
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
69096
69988
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
69097
69989
  new Set(CAP_NAMES);
@@ -69420,6 +70312,9 @@ var DEVICE_LOCAL_STATE_CAPS = {
69420
70312
  featureProbe: featureProbeCapability,
69421
70313
  flood: floodCapability,
69422
70314
  gas: gasCapability,
70315
+ groupActive: groupActiveCapability,
70316
+ groupCommands: groupCommandsCapability,
70317
+ groupStatus: groupStatusCapability,
69423
70318
  heatingValve: heatingValveCapability,
69424
70319
  humidifier: humidifierCapability,
69425
70320
  humiditySensor: humiditySensorCapability,
@@ -69429,6 +70324,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
69429
70324
  lawnMowerControl: lawnMowerControlCapability,
69430
70325
  lockControl: lockControlCapability,
69431
70326
  mediaPlayer: mediaPlayerCapability,
70327
+ meter: meterCapability,
69432
70328
  motion: motionCapability,
69433
70329
  motionTrigger: motionTriggerCapability,
69434
70330
  motionZones: motionZonesCapability,
@@ -70205,7 +71101,7 @@ var DeclaredDevices = class {
70205
71101
  return removed;
70206
71102
  }
70207
71103
  };
70208
- 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;
71104
+ 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;
70209
71105
  new Set(Object.values(DeviceType));
70210
71106
  Object.freeze({
70211
71107
  "accessories.getStatus": {
@@ -70313,6 +71209,13 @@ Object.freeze({
70313
71209
  access: "view",
70314
71210
  role: "admin"
70315
71211
  },
71212
+ "addons.getPackageDocs": {
71213
+ capName: "addons",
71214
+ capScope: "system",
71215
+ addonId: null,
71216
+ access: "view",
71217
+ role: "admin"
71218
+ },
70316
71219
  "addons.getVersions": {
70317
71220
  capName: "addons",
70318
71221
  capScope: "system",
@@ -73421,6 +74324,27 @@ Object.freeze({
73421
74324
  access: "view",
73422
74325
  role: "viewer"
73423
74326
  },
74327
+ "groupActive.getStatus": {
74328
+ capName: "group-active",
74329
+ capScope: "device",
74330
+ addonId: null,
74331
+ access: "view",
74332
+ role: "viewer"
74333
+ },
74334
+ "groupCommands.getStatus": {
74335
+ capName: "group-commands",
74336
+ capScope: "device",
74337
+ addonId: null,
74338
+ access: "view",
74339
+ role: "viewer"
74340
+ },
74341
+ "groupStatus.getStatus": {
74342
+ capName: "group-status",
74343
+ capScope: "device",
74344
+ addonId: null,
74345
+ access: "view",
74346
+ role: "viewer"
74347
+ },
73424
74348
  "heatingValve.getStatus": {
73425
74349
  capName: "heating-valve",
73426
74350
  capScope: "device",
@@ -74373,6 +75297,13 @@ Object.freeze({
74373
75297
  access: "create",
74374
75298
  role: "admin"
74375
75299
  },
75300
+ "meter.getStatus": {
75301
+ capName: "meter",
75302
+ capScope: "device",
75303
+ addonId: null,
75304
+ access: "view",
75305
+ role: "viewer"
75306
+ },
74376
75307
  "metricsProvider.collectSnapshot": {
74377
75308
  capName: "metrics-provider",
74378
75309
  capScope: "system",
@@ -82471,7 +83402,39 @@ var APPLIANCE_CAP_VOCABULARY = {
82471
83402
  phrase: t("When {device} finishes", "Quando {device} finisce"),
82472
83403
  icon: "square"
82473
83404
  }
82474
- }
83405
+ },
83406
+ overrides: { byType: {
83407
+ [DeviceType.Washer]: {
83408
+ state: { running: {
83409
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
83410
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
83411
+ } },
83412
+ events: {
83413
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
83414
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
83415
+ }
83416
+ },
83417
+ [DeviceType.Dryer]: {
83418
+ state: { running: {
83419
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
83420
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
83421
+ } },
83422
+ events: {
83423
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
83424
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
83425
+ }
83426
+ },
83427
+ [DeviceType.Dishwasher]: {
83428
+ state: { running: {
83429
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
83430
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
83431
+ } },
83432
+ events: {
83433
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
83434
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
83435
+ }
83436
+ }
83437
+ } }
82475
83438
  },
82476
83439
  update: {
82477
83440
  scope: "device",
@@ -83243,6 +84206,41 @@ var SENSOR_CAP_VOCABULARY = {
83243
84206
  icon: "flame"
83244
84207
  }]
83245
84208
  },
84209
+ "group-active": {
84210
+ scope: "device",
84211
+ noun: t("Group state", "Stato del gruppo")
84212
+ },
84213
+ "group-commands": {
84214
+ scope: "device",
84215
+ noun: t("Group command", "Comando di gruppo")
84216
+ },
84217
+ "group-status": {
84218
+ scope: "device",
84219
+ noun: t("Group", "Gruppo"),
84220
+ state: [
84221
+ {
84222
+ kind: "numeric",
84223
+ field: "included",
84224
+ quantity: t("the members reporting", "i membri che rispondono"),
84225
+ label: t("Reporting", "Rispondono"),
84226
+ icon: "users"
84227
+ },
84228
+ {
84229
+ kind: "numeric",
84230
+ field: "excluded",
84231
+ quantity: t("the members left out", "i membri esclusi"),
84232
+ label: t("Excluded", "Esclusi"),
84233
+ icon: "users"
84234
+ },
84235
+ {
84236
+ kind: "numeric",
84237
+ field: "total",
84238
+ quantity: t("the member count", "il numero di membri"),
84239
+ label: t("Members", "Membri"),
84240
+ icon: "sigma"
84241
+ }
84242
+ ]
84243
+ },
83246
84244
  "humidity-sensor": {
83247
84245
  scope: "device",
83248
84246
  noun: t("Humidity", "Umidità"),
@@ -83268,6 +84266,17 @@ var SENSOR_CAP_VOCABULARY = {
83268
84266
  }],
83269
84267
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
83270
84268
  },
84269
+ meter: {
84270
+ scope: "device",
84271
+ noun: t("Meter", "Contatore"),
84272
+ state: [{
84273
+ kind: "numeric",
84274
+ field: "value",
84275
+ quantity: t("the total", "il totale"),
84276
+ label: t("Total", "Totale"),
84277
+ icon: "gauge"
84278
+ }]
84279
+ },
83271
84280
  "numeric-sensor": {
83272
84281
  scope: "device",
83273
84282
  noun: t("Sensor", "Sensore"),
@@ -83276,7 +84285,6 @@ var SENSOR_CAP_VOCABULARY = {
83276
84285
  field: "value",
83277
84286
  quantity: t("the reading", "il valore"),
83278
84287
  label: t("Reading", "Valore"),
83279
- unitField: "unit",
83280
84288
  icon: "gauge"
83281
84289
  }]
83282
84290
  },
@@ -83412,7 +84420,7 @@ var SENSOR_CAP_VOCABULARY = {
83412
84420
  field: "temperature",
83413
84421
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83414
84422
  label: t("Temperature", "Temperatura"),
83415
- unitField: "temperatureUnit",
84423
+ unit: "°C",
83416
84424
  icon: "thermometer"
83417
84425
  },
83418
84426
  {
@@ -83428,7 +84436,7 @@ var SENSOR_CAP_VOCABULARY = {
83428
84436
  field: "windSpeed",
83429
84437
  quantity: t("the wind speed", "la velocità del vento"),
83430
84438
  label: t("Wind", "Vento"),
83431
- unitField: "windSpeedUnit",
84439
+ unit: "m/s",
83432
84440
  icon: "wind"
83433
84441
  },
83434
84442
  {
@@ -83436,7 +84444,7 @@ var SENSOR_CAP_VOCABULARY = {
83436
84444
  field: "pressure",
83437
84445
  quantity: t("the air pressure", "la pressione atmosferica"),
83438
84446
  label: t("Pressure", "Pressione"),
83439
- unitField: "pressureUnit",
84447
+ unit: "hPa",
83440
84448
  icon: "gauge"
83441
84449
  }
83442
84450
  ]
@@ -83741,8 +84749,47 @@ function projectAction(cap, action) {
83741
84749
  };
83742
84750
  }
83743
84751
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
83744
- 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;
84752
+ 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;
83745
84753
  DeviceFeature.BatteryOperated;
84754
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
84755
+ 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.";
84756
+ 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.";
84757
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
84758
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
84759
+ 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.";
84760
+ function withheld(reason) {
84761
+ return {
84762
+ [DeviceType.Washer]: {
84763
+ exported: false,
84764
+ reason
84765
+ },
84766
+ [DeviceType.Dryer]: {
84767
+ exported: false,
84768
+ reason
84769
+ },
84770
+ [DeviceType.Dishwasher]: {
84771
+ exported: false,
84772
+ reason
84773
+ }
84774
+ };
84775
+ }
84776
+ function offered(received) {
84777
+ return {
84778
+ [DeviceType.Washer]: {
84779
+ exported: true,
84780
+ as: received
84781
+ },
84782
+ [DeviceType.Dryer]: {
84783
+ exported: true,
84784
+ as: received
84785
+ },
84786
+ [DeviceType.Dishwasher]: {
84787
+ exported: true,
84788
+ as: received
84789
+ }
84790
+ };
84791
+ }
84792
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
83746
84793
  Object.freeze({
83747
84794
  "accessories.getStatus": [{
83748
84795
  name: "deviceId",
@@ -84709,6 +85756,21 @@ Object.freeze({
84709
85756
  form: "single",
84710
85757
  optional: false
84711
85758
  }],
85759
+ "groupActive.getStatus": [{
85760
+ name: "deviceId",
85761
+ form: "single",
85762
+ optional: false
85763
+ }],
85764
+ "groupCommands.getStatus": [{
85765
+ name: "deviceId",
85766
+ form: "single",
85767
+ optional: false
85768
+ }],
85769
+ "groupStatus.getStatus": [{
85770
+ name: "deviceId",
85771
+ form: "single",
85772
+ optional: false
85773
+ }],
84712
85774
  "heatingValve.getStatus": [{
84713
85775
  name: "deviceId",
84714
85776
  form: "single",
@@ -84909,6 +85971,11 @@ Object.freeze({
84909
85971
  form: "single",
84910
85972
  optional: false
84911
85973
  }],
85974
+ "meter.getStatus": [{
85975
+ name: "deviceId",
85976
+ form: "single",
85977
+ optional: false
85978
+ }],
84912
85979
  "motion.getStatus": [{
84913
85980
  name: "deviceId",
84914
85981
  form: "single",
@@ -86516,7 +87583,6 @@ Object.freeze({
86516
87583
  "network-access": "ingress",
86517
87584
  "smtp-provider": "email"
86518
87585
  });
86519
- DeviceType.Camera;
86520
87586
  /**
86521
87587
  * A saved Customize assignment (D936). The schema is the type: callers use
86522
87588
  * `z.infer`, they do not restate the shape.
@@ -86693,6 +87759,8 @@ new Map(Object.entries({
86693
87759
  "detection-blind": "device-events",
86694
87760
  "motion-noisy": "device-events",
86695
87761
  "motion-calm": "device-events",
87762
+ "package-llm-silent": "device-events",
87763
+ "package-llm-answered": "device-events",
86696
87764
  "recording-stalled": "device-events",
86697
87765
  "recording-healthy": "device-events",
86698
87766
  "recording-degraded": "device-events",
@@ -86776,6 +87844,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
86776
87844
  label: "Motion without a subject",
86777
87845
  kinds: ["motion-noisy", "motion-calm"]
86778
87846
  },
87847
+ {
87848
+ id: "package-llm",
87849
+ family: "device-events",
87850
+ label: "Package confirm",
87851
+ kinds: ["package-llm-silent", "package-llm-answered"]
87852
+ },
86779
87853
  {
86780
87854
  id: "recording",
86781
87855
  family: "device-events",
@@ -86957,6 +88031,8 @@ var DEVICE_EVENTS = [
86957
88031
  "detection-blind",
86958
88032
  "motion-noisy",
86959
88033
  "motion-calm",
88034
+ "package-llm-silent",
88035
+ "package-llm-answered",
86960
88036
  "recording-stalled",
86961
88037
  "recording-healthy",
86962
88038
  "recording-degraded",
@@ -87225,6 +88301,15 @@ new Map([
87225
88301
  snapshotLabel: "Cameras with constant motion now",
87226
88302
  snapshotSection: null
87227
88303
  },
88304
+ {
88305
+ id: "package-llm-confirm",
88306
+ family: "device-events",
88307
+ label: "Package confirm (not answering / answering again)",
88308
+ bad: ["package-llm-silent"],
88309
+ recovery: ["package-llm-answered"],
88310
+ snapshotLabel: "Cameras whose package confirm is not answering",
88311
+ snapshotSection: null
88312
+ },
87228
88313
  {
87229
88314
  id: "recording-health",
87230
88315
  family: "device-events",