@camstack/addon-provider-rtsp 1.2.146 → 1.2.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  import net from "node:net";
2
- //#region ../types/dist/event-category-DHXxB8t9.mjs
2
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -325,6 +325,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
325
325
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
326
326
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
327
327
  /**
328
+ * A camera's package LLM confirm has failed closed three times in a row
329
+ * (D931, 2026-10-26). One edge per camera until
330
+ * {@link PipelinePackageLlmAnswered}.
331
+ */
332
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
333
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
334
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
335
+ /**
328
336
  * Per-camera pipeline config was mutated by the orchestrator
329
337
  * (a settings change via `setAgentInferenceDevices` /
330
338
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5434,7 +5442,7 @@ var ZodIssueCode = {
5434
5442
  var ZodFirstPartyTypeKind;
5435
5443
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5436
5444
  //#endregion
5437
- //#region ../types/dist/sleep-BErAxhul.mjs
5445
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5438
5446
  /**
5439
5447
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5440
5448
  * window to float samples (D455).
@@ -7988,6 +7996,99 @@ var RecordKindDeclarationSchema = object({
7988
7996
  }).strict();
7989
7997
  /** The kind names: lower-case, dash separated, short enough for an index name. */
7990
7998
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7999
+ var READING_QUANTITIES = [
8000
+ "temperature",
8001
+ "pressure",
8002
+ "speed",
8003
+ "precipitation",
8004
+ "precipitation-rate",
8005
+ "length",
8006
+ "area",
8007
+ "illuminance",
8008
+ "irradiance",
8009
+ "power",
8010
+ "energy",
8011
+ "voltage",
8012
+ "current",
8013
+ "apparent-power",
8014
+ "humidity",
8015
+ "percentage",
8016
+ "concentration-volume",
8017
+ "concentration-mass",
8018
+ "concentration-ppb",
8019
+ "volume",
8020
+ "duration",
8021
+ "duration-ms",
8022
+ "information",
8023
+ "sound-level",
8024
+ "signal-level",
8025
+ "colour-temperature",
8026
+ "angle",
8027
+ "bitrate",
8028
+ "rate",
8029
+ "mass",
8030
+ "index",
8031
+ "ratio",
8032
+ "count",
8033
+ "none",
8034
+ "device"
8035
+ ];
8036
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8037
+ new Set(READING_QUANTITIES);
8038
+ /**
8039
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8040
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8041
+ */
8042
+ var QUANTITY_BASE = {
8043
+ temperature: "°C",
8044
+ pressure: "hPa",
8045
+ speed: "m/s",
8046
+ precipitation: "mm",
8047
+ "precipitation-rate": "mm/h",
8048
+ length: "m",
8049
+ area: "m²",
8050
+ illuminance: "lx",
8051
+ irradiance: "W/m²",
8052
+ power: "W",
8053
+ energy: "kWh",
8054
+ voltage: "V",
8055
+ current: "A",
8056
+ "apparent-power": "VA",
8057
+ humidity: "%",
8058
+ percentage: "%",
8059
+ "concentration-volume": "ppm",
8060
+ "concentration-mass": "µg/m³",
8061
+ "concentration-ppb": "ppb",
8062
+ volume: "L",
8063
+ duration: "s",
8064
+ "duration-ms": "ms",
8065
+ information: "B",
8066
+ "sound-level": "dBFS",
8067
+ "signal-level": "dBm",
8068
+ "colour-temperature": "mired",
8069
+ angle: "°",
8070
+ bitrate: "kbps",
8071
+ rate: "fps",
8072
+ mass: "kg",
8073
+ index: "none",
8074
+ ratio: "none",
8075
+ count: "none",
8076
+ none: "none",
8077
+ device: "device"
8078
+ };
8079
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8080
+ function baseUnitOf(quantity) {
8081
+ const symbol = QUANTITY_BASE[quantity];
8082
+ if (symbol === "none" || symbol === "device") return void 0;
8083
+ return symbol;
8084
+ }
8085
+ function readingMeta(quantity, extras) {
8086
+ return {
8087
+ quantity,
8088
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8089
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8090
+ };
8091
+ }
7991
8092
  /** Shared Zod schemas used across streaming capabilities. */
7992
8093
  var CamProfileSchema = _enum([
7993
8094
  "high",
@@ -8015,8 +8116,8 @@ var CamStreamKindSchema = _enum([
8015
8116
  "derived"
8016
8117
  ]);
8017
8118
  var CamStreamResolutionSchema = object({
8018
- width: number().int().positive(),
8019
- height: number().int().positive()
8119
+ width: number().int().positive().meta(readingMeta("none")),
8120
+ height: number().int().positive().meta(readingMeta("none"))
8020
8121
  });
8021
8122
  var CameraStreamSchema = object({
8022
8123
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8534,6 +8635,9 @@ var CAP_NAMES = Object.freeze([
8534
8635
  "filesystem-browse",
8535
8636
  "flood",
8536
8637
  "gas",
8638
+ "group-active",
8639
+ "group-commands",
8640
+ "group-status",
8537
8641
  "heating-valve",
8538
8642
  "humidifier",
8539
8643
  "humidity-sensor",
@@ -8554,6 +8658,7 @@ var CAP_NAMES = Object.freeze([
8554
8658
  "login-method",
8555
8659
  "media-player",
8556
8660
  "mesh-network",
8661
+ "meter",
8557
8662
  "metrics-provider",
8558
8663
  "model-convert",
8559
8664
  "model-distributor",
@@ -8663,14 +8768,6 @@ Object.freeze({
8663
8768
  {
8664
8769
  "path": "aqi",
8665
8770
  "valueType": "number"
8666
- },
8667
- {
8668
- "path": "unit",
8669
- "valueType": "string"
8670
- },
8671
- {
8672
- "path": "precision",
8673
- "valueType": "number"
8674
8771
  }
8675
8772
  ],
8676
8773
  "alarm-panel": [
@@ -8706,20 +8803,10 @@ Object.freeze({
8706
8803
  "valueType": "boolean"
8707
8804
  }
8708
8805
  ],
8709
- "ambient-light-sensor": [
8710
- {
8711
- "path": "lux",
8712
- "valueType": "number"
8713
- },
8714
- {
8715
- "path": "unit",
8716
- "valueType": "string"
8717
- },
8718
- {
8719
- "path": "precision",
8720
- "valueType": "number"
8721
- }
8722
- ],
8806
+ "ambient-light-sensor": [{
8807
+ "path": "lux",
8808
+ "valueType": "number"
8809
+ }],
8723
8810
  "audio-metrics": [
8724
8811
  {
8725
8812
  "path": "ts",
@@ -8799,6 +8886,10 @@ Object.freeze({
8799
8886
  "path": "lastUpdated",
8800
8887
  "valueType": "number"
8801
8888
  },
8889
+ {
8890
+ "path": "volts",
8891
+ "valueType": "number"
8892
+ },
8802
8893
  {
8803
8894
  "path": "lastContactAt",
8804
8895
  "valueType": "number"
@@ -9041,10 +9132,6 @@ Object.freeze({
9041
9132
  "datetime"
9042
9133
  ]
9043
9134
  },
9044
- {
9045
- "path": "unit",
9046
- "valueType": "string"
9047
- },
9048
9135
  {
9049
9136
  "path": "min",
9050
9137
  "valueType": "number"
@@ -9056,19 +9143,6 @@ Object.freeze({
9056
9143
  {
9057
9144
  "path": "step",
9058
9145
  "valueType": "number"
9059
- },
9060
- {
9061
- "path": "precision",
9062
- "valueType": "number"
9063
- },
9064
- {
9065
- "path": "format",
9066
- "valueType": "enum",
9067
- "enumValues": [
9068
- "date",
9069
- "time",
9070
- "datetime"
9071
- ]
9072
9146
  }
9073
9147
  ],
9074
9148
  "cover": [
@@ -9309,13 +9383,8 @@ Object.freeze({
9309
9383
  "path": "value",
9310
9384
  "valueType": "string"
9311
9385
  }, {
9312
- "path": "format",
9313
- "valueType": "enum",
9314
- "enumValues": [
9315
- "date",
9316
- "time",
9317
- "datetime"
9318
- ]
9386
+ "path": "timestamp",
9387
+ "valueType": "boolean"
9319
9388
  }],
9320
9389
  "event-emitter": [
9321
9390
  {
@@ -9388,6 +9457,24 @@ Object.freeze({
9388
9457
  "path": "detected",
9389
9458
  "valueType": "boolean"
9390
9459
  }],
9460
+ "group-active": [{
9461
+ "path": "active",
9462
+ "valueType": "boolean"
9463
+ }],
9464
+ "group-status": [
9465
+ {
9466
+ "path": "included",
9467
+ "valueType": "number"
9468
+ },
9469
+ {
9470
+ "path": "excluded",
9471
+ "valueType": "number"
9472
+ },
9473
+ {
9474
+ "path": "total",
9475
+ "valueType": "number"
9476
+ }
9477
+ ],
9391
9478
  "heating-valve": [{
9392
9479
  "path": "position",
9393
9480
  "valueType": "number"
@@ -9422,20 +9509,10 @@ Object.freeze({
9422
9509
  "valueType": "number"
9423
9510
  }
9424
9511
  ],
9425
- "humidity-sensor": [
9426
- {
9427
- "path": "percent",
9428
- "valueType": "number"
9429
- },
9430
- {
9431
- "path": "unit",
9432
- "valueType": "string"
9433
- },
9434
- {
9435
- "path": "precision",
9436
- "valueType": "number"
9437
- }
9438
- ],
9512
+ "humidity-sensor": [{
9513
+ "path": "percent",
9514
+ "valueType": "number"
9515
+ }],
9439
9516
  "image": [{
9440
9517
  "path": "url",
9441
9518
  "valueType": "string"
@@ -9643,6 +9720,10 @@ Object.freeze({
9643
9720
  ]
9644
9721
  }
9645
9722
  ],
9723
+ "meter": [{
9724
+ "path": "value",
9725
+ "valueType": "number"
9726
+ }],
9646
9727
  "motion": [
9647
9728
  {
9648
9729
  "path": "detected",
@@ -9774,20 +9855,10 @@ Object.freeze({
9774
9855
  "valueType": "boolean"
9775
9856
  }
9776
9857
  ],
9777
- "numeric-sensor": [
9778
- {
9779
- "path": "value",
9780
- "valueType": "number"
9781
- },
9782
- {
9783
- "path": "unit",
9784
- "valueType": "string"
9785
- },
9786
- {
9787
- "path": "precision",
9788
- "valueType": "number"
9789
- }
9790
- ],
9858
+ "numeric-sensor": [{
9859
+ "path": "value",
9860
+ "valueType": "number"
9861
+ }],
9791
9862
  "pet-feeder": [
9792
9863
  {
9793
9864
  "path": "foodLevel",
@@ -9879,14 +9950,6 @@ Object.freeze({
9879
9950
  {
9880
9951
  "path": "powerFactor",
9881
9952
  "valueType": "number"
9882
- },
9883
- {
9884
- "path": "unit",
9885
- "valueType": "string"
9886
- },
9887
- {
9888
- "path": "precision",
9889
- "valueType": "number"
9890
9953
  }
9891
9954
  ],
9892
9955
  "presence": [
@@ -9915,20 +9978,10 @@ Object.freeze({
9915
9978
  "valueType": "number"
9916
9979
  }
9917
9980
  ],
9918
- "pressure-sensor": [
9919
- {
9920
- "path": "hpa",
9921
- "valueType": "number"
9922
- },
9923
- {
9924
- "path": "unit",
9925
- "valueType": "string"
9926
- },
9927
- {
9928
- "path": "precision",
9929
- "valueType": "number"
9930
- }
9931
- ],
9981
+ "pressure-sensor": [{
9982
+ "path": "hpa",
9983
+ "valueType": "number"
9984
+ }],
9932
9985
  "privacy-mask": [{
9933
9986
  "path": "enabled",
9934
9987
  "valueType": "boolean"
@@ -10239,20 +10292,10 @@ Object.freeze({
10239
10292
  "path": "tampered",
10240
10293
  "valueType": "boolean"
10241
10294
  }],
10242
- "temperature-sensor": [
10243
- {
10244
- "path": "celsius",
10245
- "valueType": "number"
10246
- },
10247
- {
10248
- "path": "unit",
10249
- "valueType": "string"
10250
- },
10251
- {
10252
- "path": "precision",
10253
- "valueType": "number"
10254
- }
10255
- ],
10295
+ "temperature-sensor": [{
10296
+ "path": "celsius",
10297
+ "valueType": "number"
10298
+ }],
10256
10299
  "update": [
10257
10300
  {
10258
10301
  "path": "currentVersion",
@@ -10363,10 +10406,6 @@ Object.freeze({
10363
10406
  "path": "temperature",
10364
10407
  "valueType": "number"
10365
10408
  },
10366
- {
10367
- "path": "temperatureUnit",
10368
- "valueType": "string"
10369
- },
10370
10409
  {
10371
10410
  "path": "humidity",
10372
10411
  "valueType": "number"
@@ -10375,18 +10414,10 @@ Object.freeze({
10375
10414
  "path": "pressure",
10376
10415
  "valueType": "number"
10377
10416
  },
10378
- {
10379
- "path": "pressureUnit",
10380
- "valueType": "string"
10381
- },
10382
10417
  {
10383
10418
  "path": "windSpeed",
10384
10419
  "valueType": "number"
10385
10420
  },
10386
- {
10387
- "path": "windSpeedUnit",
10388
- "valueType": "string"
10389
- },
10390
10421
  {
10391
10422
  "path": "windBearing",
10392
10423
  "valueType": "number"
@@ -10525,7 +10556,7 @@ function maskUrlCredentials(rawUrl) {
10525
10556
  }
10526
10557
  }
10527
10558
  //#endregion
10528
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10559
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10529
10560
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10530
10561
  DeviceType["Camera"] = "camera";
10531
10562
  DeviceType["Hub"] = "hub";
@@ -10619,8 +10650,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10619
10650
  DeviceType["PetFeeder"] = "pet-feeder";
10620
10651
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10621
10652
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10622
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10653
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10654
+ * A plug-built dishwasher carries running-state and power instead. */
10623
10655
  DeviceType["Dishwasher"] = "dishwasher";
10656
+ /** Washing machine. A plug-built one is running while its power stays
10657
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10658
+ DeviceType["Washer"] = "washer";
10659
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10660
+ DeviceType["Dryer"] = "dryer";
10624
10661
  return DeviceType;
10625
10662
  }({});
10626
10663
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -10878,7 +10915,7 @@ function errMsg(err) {
10878
10915
  return String(err);
10879
10916
  }
10880
10917
  //#endregion
10881
- //#region ../types/dist/composition-kEl9mevu.mjs
10918
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
10882
10919
  /**
10883
10920
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10884
10921
  *
@@ -11193,6 +11230,13 @@ var table = {
11193
11230
  apply: () => {
11194
11231
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11195
11232
  }
11233
+ },
11234
+ schmitt: {
11235
+ minArgs: 3,
11236
+ maxArgs: 5,
11237
+ apply: () => {
11238
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11239
+ }
11196
11240
  }
11197
11241
  };
11198
11242
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11633,15 +11677,232 @@ function validateExpressionSource(src) {
11633
11677
  }
11634
11678
  return null;
11635
11679
  }
11636
- /** A composed device's stableId is this prefix plus the block id. */
11637
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11638
- /** Longest snippet a `code` source may carry (slice 4). */
11639
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11680
+ /**
11681
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11682
+ *
11683
+ * Standby of the control board is under 5 W; a motor or a heater is far
11684
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11685
+ * a cycle, so running starts only after 30 s above the line.
11686
+ *
11687
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11688
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11689
+ * report "finished" in the middle of the program, so the stop dwell is
11690
+ * 10 minutes on every type. The number is stored per type so one machine
11691
+ * can move later without moving the others.
11692
+ */
11693
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11694
+ var APPLIANCE_KINDS = [
11695
+ "washer",
11696
+ "dryer",
11697
+ "dishwasher"
11698
+ ];
11699
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11700
+ /**
11701
+ * Statistics and integrals over a history read (D944).
11702
+ *
11703
+ * Pure: the same points always give the same answer, so a restart recomputes
11704
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11705
+ * never the mean of the samples. A hole with no held value is not integrated
11706
+ * as zero. A truncated read is not published.
11707
+ */
11708
+ var STATISTIC_STATS = [
11709
+ "min",
11710
+ "max",
11711
+ "mean",
11712
+ "sum",
11713
+ "count",
11714
+ "change"
11715
+ ];
11716
+ /**
11717
+ * The range a statistics or integral helper asks history for (D944).
11718
+ *
11719
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11720
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11721
+ * is strictly longer than the longest that range can be. Forever qualifies.
11722
+ * Equal to a tier's retention is a refusal, not a short answer.
11723
+ */
11724
+ var MINUTE_MS = 6e4;
11725
+ var HOUR_MS = 60 * MINUTE_MS;
11726
+ var DAY_MS = 24 * HOUR_MS;
11727
+ var ROLLING_SPANS = [
11728
+ "1h",
11729
+ "24h",
11730
+ "2d",
11731
+ "7d",
11732
+ "30d",
11733
+ "custom"
11734
+ ];
11735
+ var CALENDAR_PERIODS = [
11736
+ "today",
11737
+ "yesterday",
11738
+ "this-week",
11739
+ "this-month"
11740
+ ];
11741
+ var RESET_PERIODS = [
11742
+ "never",
11743
+ "daily",
11744
+ "weekly",
11745
+ "monthly"
11746
+ ];
11747
+ 5 * MINUTE_MS;
11748
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11749
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11750
+ var rollingWindowObject = object({
11751
+ kind: literal("rolling"),
11752
+ span: _enum(ROLLING_SPANS),
11753
+ customMinutes: number().int().min(5).optional()
11754
+ }).check((payload) => {
11755
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11756
+ code: "custom",
11757
+ message: "a custom range needs a duration in minutes",
11758
+ path: ["customMinutes"],
11759
+ input: payload.value
11760
+ });
11761
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11762
+ code: "custom",
11763
+ message: "only a custom range carries a duration",
11764
+ path: ["customMinutes"],
11765
+ input: payload.value
11766
+ });
11767
+ });
11768
+ var RollingWindowSchema = rollingWindowObject;
11769
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
11770
+ kind: literal("calendar"),
11771
+ period: _enum(CALENDAR_PERIODS)
11772
+ })]);
11773
+ /**
11774
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11775
+ * id. Split out so helper schemas can name it without a cycle through
11776
+ * `composition.ts` (D659).
11777
+ */
11640
11778
  var CompositionSourceRefSchema = object({
11641
11779
  /** The owning addon; `stableId` is unique only within it. */
11642
11780
  addonId: string().min(1),
11643
11781
  stableId: string().min(1)
11644
11782
  });
11783
+ var GROUP_TEMPLATE_IDS = [
11784
+ "temperature",
11785
+ "humidity",
11786
+ "pressure",
11787
+ "illuminance",
11788
+ "power",
11789
+ "energy",
11790
+ "battery",
11791
+ "numeric",
11792
+ "motion",
11793
+ "contact",
11794
+ "leak",
11795
+ "smoke",
11796
+ "occupancy",
11797
+ "binary",
11798
+ "switch",
11799
+ "light",
11800
+ "fan",
11801
+ "cover",
11802
+ "valve",
11803
+ "lock",
11804
+ "media-player",
11805
+ "climate"
11806
+ ];
11807
+ var NUMERIC_AGGREGATES = [
11808
+ "mean",
11809
+ "median",
11810
+ "min",
11811
+ "max",
11812
+ "sum"
11813
+ ];
11814
+ var BINARY_AGGREGATES = ["any", "all"];
11815
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11816
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11817
+ var AGGREGATE_REDUCERS = [
11818
+ "mean",
11819
+ "median",
11820
+ "min",
11821
+ "max",
11822
+ "sum",
11823
+ "any",
11824
+ "all"
11825
+ ];
11826
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11827
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11828
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11829
+ /** One member. The field is stored so occupancy can name motion or binary. */
11830
+ var HelperMemberSchema = object({
11831
+ source: CompositionSourceRefSchema,
11832
+ cap: string().min(1),
11833
+ fieldPath: string().min(1)
11834
+ });
11835
+ var HelperSpecSchema = discriminatedUnion("kind", [
11836
+ object({
11837
+ kind: literal("group"),
11838
+ templateId: GroupTemplateIdSchema,
11839
+ aggregate: AggregateReducerSchema,
11840
+ members: array(HelperMemberSchema).min(1).max(64),
11841
+ /** Actuator groups only. Readings leave this unset. */
11842
+ active: ActiveCriterionSchema.optional()
11843
+ }),
11844
+ object({
11845
+ kind: literal("appliance"),
11846
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
11847
+ appliance: _enum(APPLIANCE_KINDS),
11848
+ thresholdWatts: number().positive(),
11849
+ startDwellMs: number().int().nonnegative(),
11850
+ stopDwellMs: number().int().nonnegative(),
11851
+ member: HelperMemberSchema
11852
+ }),
11853
+ object({
11854
+ kind: literal("statistic"),
11855
+ stat: _enum(STATISTIC_STATS),
11856
+ window: StatisticWindowSchema,
11857
+ source: HelperMemberSchema
11858
+ }),
11859
+ object({
11860
+ kind: literal("integral"),
11861
+ reset: _enum(RESET_PERIODS),
11862
+ output: _enum(["energy", "meter"]),
11863
+ source: HelperMemberSchema
11864
+ }),
11865
+ object({
11866
+ kind: literal("threshold"),
11867
+ source: HelperMemberSchema,
11868
+ high: number(),
11869
+ low: number(),
11870
+ onForMs: number().int().nonnegative(),
11871
+ offForMs: number().int().nonnegative()
11872
+ }),
11873
+ object({
11874
+ kind: literal("arithmetic"),
11875
+ op: _enum([
11876
+ "sum",
11877
+ "difference",
11878
+ "ratio"
11879
+ ]),
11880
+ inputs: array(HelperMemberSchema).min(2).max(64)
11881
+ }),
11882
+ object({
11883
+ kind: literal("expression"),
11884
+ expr: string().min(1),
11885
+ inputs: array(object({
11886
+ name: string().min(1),
11887
+ member: HelperMemberSchema
11888
+ })).min(1).max(64),
11889
+ output: _enum(["number", "boolean"])
11890
+ }),
11891
+ object({
11892
+ kind: literal("derivative"),
11893
+ window: RollingWindowSchema,
11894
+ per: _enum([
11895
+ "second",
11896
+ "minute",
11897
+ "hour"
11898
+ ]),
11899
+ source: HelperMemberSchema
11900
+ })
11901
+ ]);
11902
+ /** A composed device's stableId is this prefix plus the block id. */
11903
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11904
+ /** Longest snippet a `code` source may carry (slice 4). */
11905
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11645
11906
  /** One field of one capability of one source device. */
11646
11907
  var CompositionFieldReadSchema = object({
11647
11908
  source: CompositionSourceRefSchema,
@@ -11685,7 +11946,59 @@ var CompositionCodeSourceSchema = object({
11685
11946
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11686
11947
  CompositionFromSourceSchema,
11687
11948
  CompositionExpressionSourceSchema,
11688
- CompositionCodeSourceSchema
11949
+ CompositionCodeSourceSchema,
11950
+ object({
11951
+ kind: literal("aggregate"),
11952
+ reducer: _enum([
11953
+ "mean",
11954
+ "median",
11955
+ "min",
11956
+ "max",
11957
+ "sum",
11958
+ "any",
11959
+ "all"
11960
+ ]),
11961
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11962
+ /**
11963
+ * Set when the reads are an enum state, not a yes/no: the listed states
11964
+ * count as active (a cover is `open`). A state outside the list is inactive;
11965
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
11966
+ */
11967
+ activeWhen: array(string().min(1)).min(1).optional(),
11968
+ /** `value` is the reading; the others are the member counts. */
11969
+ publish: _enum([
11970
+ "value",
11971
+ "included",
11972
+ "excluded",
11973
+ "total"
11974
+ ]),
11975
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11976
+ empty: _enum([
11977
+ "null",
11978
+ "omit",
11979
+ "offline"
11980
+ ])
11981
+ }),
11982
+ object({
11983
+ kind: literal("series"),
11984
+ read: CompositionFieldReadSchema,
11985
+ /** `value` is the statistic or the integral. The others are the gap counts. */
11986
+ publish: _enum([
11987
+ "value",
11988
+ "included",
11989
+ "excluded",
11990
+ "total"
11991
+ ]),
11992
+ /**
11993
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
11994
+ * device unseeded until history answers, so a restart cannot publish 0.
11995
+ */
11996
+ empty: _enum([
11997
+ "null",
11998
+ "omit",
11999
+ "offline"
12000
+ ])
12001
+ })
11689
12002
  ]);
11690
12003
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11691
12004
  kind: literal("forward"),
@@ -11731,7 +12044,13 @@ var CompositionSchema = object({
11731
12044
  kind: literal("existing"),
11732
12045
  device: CompositionSourceRefSchema
11733
12046
  })]),
11734
- features: array(CompositionFeatureSchema).min(1).max(16)
12047
+ features: array(CompositionFeatureSchema).min(1).max(16),
12048
+ /**
12049
+ * Present when the block is a helper template. Absent on a free
12050
+ * composition (the room example, device 4603). The editor reads this,
12051
+ * never the compiled features.
12052
+ */
12053
+ helper: HelperSpecSchema.optional()
11735
12054
  }).superRefine((composition, ctx) => {
11736
12055
  const seen = /* @__PURE__ */ new Set();
11737
12056
  composition.features.forEach((feature, index) => {
@@ -16870,7 +17189,22 @@ var BrokerProviderInfoSchema = object({
16870
17189
  */
16871
17190
  interactiveConnectPath: string().min(1).optional(),
16872
17191
  /** Relative hub path to disconnect the linked account (same popup pattern). */
16873
- interactiveDisconnectPath: string().min(1).optional()
17192
+ interactiveDisconnectPath: string().min(1).optional(),
17193
+ /**
17194
+ * The kind signs in through a server-held interactive session instead of
17195
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17196
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17197
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17198
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17199
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17200
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17201
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17202
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17203
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17204
+ * offers it as the default path; the fields the session fills are
17205
+ * marked `filledBySignIn` in the kind's settings schema.
17206
+ */
17207
+ signIn: object({ basePath: string().min(1) }).optional()
16874
17208
  }))
16875
17209
  });
16876
17210
  var ListInputSchema = object({
@@ -17971,7 +18305,12 @@ var CompositionProblemSchema = object({
17971
18305
  "item-field-reserved",
17972
18306
  "target-missing",
17973
18307
  "target-unreadable",
17974
- "not-replaceable"
18308
+ "not-replaceable",
18309
+ "native-capability-replace",
18310
+ "mixed-quantity",
18311
+ "helper-mismatch",
18312
+ "member-constraint",
18313
+ "retention"
17975
18314
  ]),
17976
18315
  path: string(),
17977
18316
  message: string()
@@ -17992,7 +18331,8 @@ var CompositionUnavailableModeSchema = _enum([
17992
18331
  "null",
17993
18332
  "offline",
17994
18333
  "unknown",
17995
- "release"
18334
+ "release",
18335
+ "omit"
17996
18336
  ]);
17997
18337
  /**
17998
18338
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18029,9 +18369,11 @@ var CompositionFieldPlanSchema = object({
18029
18369
  reads: array(CompositionFieldReadSchema)
18030
18370
  });
18031
18371
  /**
18032
- * What a feature does to its target (D663): `new` builds a capability on a new
18033
- * device; on an existing device it `add`s a capability the device lacks, or
18034
- * `replace`s named fields of one it has natively.
18372
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18373
+ * new device; on an existing device it `add`s a capability the device lacks.
18374
+ * Replacing a capability the device already serves is refused
18375
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18376
+ * wire for a claim the hub already holds.
18035
18377
  */
18036
18378
  var CompositionFeatureModeSchema = _enum([
18037
18379
  "new",
@@ -18202,8 +18544,392 @@ Object.freeze({
18202
18544
  latchedAt: {
18203
18545
  kind: "fixed",
18204
18546
  result: ["number", "null"]
18547
+ },
18548
+ schmitt: {
18549
+ kind: "fixed",
18550
+ result: ["boolean", "null"]
18205
18551
  }
18206
18552
  });
18553
+ /**
18554
+ * One catalog row per base reading a group helper can build (D944 §3).
18555
+ * Data, not a form default written into the block.
18556
+ */
18557
+ var NOW$2 = {
18558
+ fieldPath: "lastFetchedAt",
18559
+ expr: "now"
18560
+ };
18561
+ var CHANGED = {
18562
+ fieldPath: "lastChangedAt",
18563
+ expr: "now"
18564
+ };
18565
+ function numeric(input) {
18566
+ return {
18567
+ ...input,
18568
+ family: "numeric",
18569
+ deviceType: DeviceType.Sensor,
18570
+ memberDeviceType: DeviceType.Sensor,
18571
+ memberDeviceTypes: [DeviceType.Sensor],
18572
+ activeWhen: void 0,
18573
+ commandCaps: [],
18574
+ baseUnit: baseUnitOf(input.quantity),
18575
+ aggregates: NUMERIC_AGGREGATES,
18576
+ accept: [{
18577
+ cap: input.outputCap,
18578
+ fieldPath: input.outputField
18579
+ }]
18580
+ };
18581
+ }
18582
+ function binary(input) {
18583
+ return {
18584
+ ...input,
18585
+ family: "binary",
18586
+ deviceType: DeviceType.Sensor,
18587
+ memberDeviceType: DeviceType.Sensor,
18588
+ memberDeviceTypes: [DeviceType.Sensor],
18589
+ activeWhen: void 0,
18590
+ commandCaps: [],
18591
+ quantity: "none",
18592
+ baseUnit: void 0,
18593
+ aggregates: BINARY_AGGREGATES,
18594
+ empty: "offline"
18595
+ };
18596
+ }
18597
+ var GROUP_TEMPLATES = [
18598
+ numeric({
18599
+ id: "temperature",
18600
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18601
+ role: DeviceRole.TemperatureSensor,
18602
+ outputCap: "temperature-sensor",
18603
+ outputField: "celsius",
18604
+ quantity: "temperature",
18605
+ defaultAggregate: "mean",
18606
+ empty: "null",
18607
+ companions: [NOW$2]
18608
+ }),
18609
+ numeric({
18610
+ id: "humidity",
18611
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18612
+ role: DeviceRole.HumiditySensor,
18613
+ outputCap: "humidity-sensor",
18614
+ outputField: "percent",
18615
+ quantity: "humidity",
18616
+ defaultAggregate: "mean",
18617
+ empty: "null",
18618
+ companions: [NOW$2]
18619
+ }),
18620
+ numeric({
18621
+ id: "pressure",
18622
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18623
+ role: DeviceRole.PressureSensor,
18624
+ outputCap: "pressure-sensor",
18625
+ outputField: "hpa",
18626
+ quantity: "pressure",
18627
+ defaultAggregate: "mean",
18628
+ empty: "null",
18629
+ companions: [NOW$2]
18630
+ }),
18631
+ numeric({
18632
+ id: "illuminance",
18633
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18634
+ role: DeviceRole.AmbientLightSensor,
18635
+ outputCap: "ambient-light-sensor",
18636
+ outputField: "lux",
18637
+ quantity: "illuminance",
18638
+ defaultAggregate: "mean",
18639
+ empty: "null",
18640
+ companions: [NOW$2]
18641
+ }),
18642
+ numeric({
18643
+ id: "power",
18644
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18645
+ role: DeviceRole.PowerSensor,
18646
+ outputCap: "power-meter",
18647
+ outputField: "watts",
18648
+ quantity: "power",
18649
+ defaultAggregate: "sum",
18650
+ empty: "omit",
18651
+ companions: [NOW$2]
18652
+ }),
18653
+ numeric({
18654
+ id: "energy",
18655
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18656
+ role: DeviceRole.EnergySensor,
18657
+ outputCap: "power-meter",
18658
+ outputField: "kwhTotal",
18659
+ quantity: "energy",
18660
+ defaultAggregate: "sum",
18661
+ empty: "omit",
18662
+ companions: [NOW$2]
18663
+ }),
18664
+ numeric({
18665
+ id: "battery",
18666
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18667
+ role: DeviceRole.BatterySensor,
18668
+ outputCap: "battery",
18669
+ outputField: "percentage",
18670
+ quantity: "percentage",
18671
+ defaultAggregate: "mean",
18672
+ empty: "null",
18673
+ companions: [
18674
+ {
18675
+ fieldPath: "charging",
18676
+ expr: "\"unknown\""
18677
+ },
18678
+ {
18679
+ fieldPath: "sleeping",
18680
+ expr: "false"
18681
+ },
18682
+ {
18683
+ fieldPath: "lastUpdated",
18684
+ expr: "now"
18685
+ }
18686
+ ]
18687
+ }),
18688
+ numeric({
18689
+ id: "numeric",
18690
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18691
+ role: DeviceRole.NumericSensor,
18692
+ outputCap: "numeric-sensor",
18693
+ outputField: "value",
18694
+ quantity: "device",
18695
+ defaultAggregate: "mean",
18696
+ empty: "null",
18697
+ companions: [NOW$2]
18698
+ }),
18699
+ binary({
18700
+ id: "motion",
18701
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18702
+ role: DeviceRole.MotionSensor,
18703
+ outputCap: "motion",
18704
+ outputField: "detected",
18705
+ defaultAggregate: "any",
18706
+ accept: [{
18707
+ cap: "motion",
18708
+ fieldPath: "detected"
18709
+ }],
18710
+ companions: [{
18711
+ fieldPath: "lastDetectedAt",
18712
+ expr: "null"
18713
+ }, {
18714
+ fieldPath: "autoClearAfterMs",
18715
+ expr: "null"
18716
+ }]
18717
+ }),
18718
+ binary({
18719
+ id: "contact",
18720
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18721
+ role: DeviceRole.ContactSensor,
18722
+ outputCap: "contact",
18723
+ outputField: "entryOpen",
18724
+ defaultAggregate: "any",
18725
+ accept: [{
18726
+ cap: "contact",
18727
+ fieldPath: "entryOpen"
18728
+ }],
18729
+ companions: [CHANGED]
18730
+ }),
18731
+ binary({
18732
+ id: "leak",
18733
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18734
+ role: DeviceRole.LeakSensor,
18735
+ outputCap: "flood",
18736
+ outputField: "flooded",
18737
+ defaultAggregate: "any",
18738
+ accept: [{
18739
+ cap: "flood",
18740
+ fieldPath: "flooded"
18741
+ }],
18742
+ companions: [CHANGED]
18743
+ }),
18744
+ binary({
18745
+ id: "smoke",
18746
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18747
+ role: DeviceRole.SmokeSensor,
18748
+ outputCap: "smoke",
18749
+ outputField: "detected",
18750
+ defaultAggregate: "any",
18751
+ accept: [{
18752
+ cap: "smoke",
18753
+ fieldPath: "detected"
18754
+ }],
18755
+ companions: [CHANGED]
18756
+ }),
18757
+ binary({
18758
+ id: "occupancy",
18759
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18760
+ role: DeviceRole.BinarySensor,
18761
+ outputCap: "binary",
18762
+ outputField: "on",
18763
+ defaultAggregate: "any",
18764
+ accept: [{
18765
+ cap: "motion",
18766
+ fieldPath: "detected"
18767
+ }, {
18768
+ cap: "binary",
18769
+ fieldPath: "on"
18770
+ }],
18771
+ companions: [CHANGED]
18772
+ }),
18773
+ binary({
18774
+ id: "binary",
18775
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18776
+ role: DeviceRole.BinarySensor,
18777
+ outputCap: "binary",
18778
+ outputField: "on",
18779
+ defaultAggregate: "any",
18780
+ accept: [{
18781
+ cap: "binary",
18782
+ fieldPath: "on"
18783
+ }],
18784
+ companions: [CHANGED]
18785
+ }),
18786
+ actuator({
18787
+ id: "switch",
18788
+ 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.",
18789
+ deviceType: DeviceType.Switch,
18790
+ outputCap: "switch",
18791
+ outputField: "on",
18792
+ member: {
18793
+ cap: "switch",
18794
+ fieldPath: "on"
18795
+ },
18796
+ commandCaps: ["switch"]
18797
+ }),
18798
+ actuator({
18799
+ id: "light",
18800
+ 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.",
18801
+ deviceType: DeviceType.Light,
18802
+ outputCap: "switch",
18803
+ outputField: "on",
18804
+ member: {
18805
+ cap: "switch",
18806
+ fieldPath: "on"
18807
+ },
18808
+ commandCaps: [
18809
+ "switch",
18810
+ "brightness",
18811
+ "color"
18812
+ ]
18813
+ }),
18814
+ actuator({
18815
+ id: "fan",
18816
+ 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.",
18817
+ deviceType: DeviceType.Fan,
18818
+ outputCap: "switch",
18819
+ outputField: "on",
18820
+ member: {
18821
+ cap: "switch",
18822
+ fieldPath: "on"
18823
+ },
18824
+ commandCaps: ["switch", "fan-control"]
18825
+ }),
18826
+ actuator({
18827
+ id: "cover",
18828
+ 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.",
18829
+ deviceType: DeviceType.Cover,
18830
+ outputCap: "group-active",
18831
+ outputField: "active",
18832
+ member: {
18833
+ cap: "cover",
18834
+ fieldPath: "state"
18835
+ },
18836
+ activeWhen: [
18837
+ "open",
18838
+ "opening",
18839
+ "stopped"
18840
+ ],
18841
+ commandCaps: ["cover"]
18842
+ }),
18843
+ actuator({
18844
+ id: "valve",
18845
+ 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.",
18846
+ deviceType: DeviceType.Valve,
18847
+ outputCap: "group-active",
18848
+ outputField: "active",
18849
+ member: {
18850
+ cap: "valve",
18851
+ fieldPath: "state"
18852
+ },
18853
+ activeWhen: [
18854
+ "open",
18855
+ "opening",
18856
+ "stopped"
18857
+ ],
18858
+ commandCaps: ["valve"]
18859
+ }),
18860
+ actuator({
18861
+ id: "lock",
18862
+ 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.",
18863
+ deviceType: DeviceType.Lock,
18864
+ outputCap: "group-active",
18865
+ outputField: "active",
18866
+ member: {
18867
+ cap: "lock-control",
18868
+ fieldPath: "state"
18869
+ },
18870
+ activeWhen: ["locked"],
18871
+ commandCaps: ["lock-control"]
18872
+ }),
18873
+ actuator({
18874
+ id: "media-player",
18875
+ 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.",
18876
+ deviceType: DeviceType.MediaPlayer,
18877
+ outputCap: "group-active",
18878
+ outputField: "active",
18879
+ member: {
18880
+ cap: "media-player",
18881
+ fieldPath: "state"
18882
+ },
18883
+ activeWhen: ["playing", "buffering"],
18884
+ commandCaps: ["media-player"]
18885
+ }),
18886
+ actuator({
18887
+ id: "climate",
18888
+ 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.",
18889
+ deviceType: DeviceType.Thermostat,
18890
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
18891
+ outputCap: "group-active",
18892
+ outputField: "active",
18893
+ member: {
18894
+ cap: "climate-control",
18895
+ fieldPath: "mode"
18896
+ },
18897
+ activeWhen: [
18898
+ "heat",
18899
+ "cool",
18900
+ "auto",
18901
+ "heat_cool",
18902
+ "fan_only",
18903
+ "dry"
18904
+ ],
18905
+ commandCaps: ["climate-control"]
18906
+ })
18907
+ ];
18908
+ function actuator(input) {
18909
+ return {
18910
+ id: input.id,
18911
+ summary: input.summary,
18912
+ deviceType: input.deviceType,
18913
+ outputCap: input.outputCap,
18914
+ outputField: input.outputField,
18915
+ commandCaps: input.commandCaps,
18916
+ family: "actuator",
18917
+ role: void 0,
18918
+ memberDeviceType: input.deviceType,
18919
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
18920
+ activeWhen: input.activeWhen,
18921
+ quantity: "none",
18922
+ baseUnit: void 0,
18923
+ aggregates: BINARY_AGGREGATES,
18924
+ defaultAggregate: "any",
18925
+ empty: "offline",
18926
+ accept: [input.member],
18927
+ companions: [CHANGED]
18928
+ };
18929
+ }
18930
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18931
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18932
+ DeviceType.Camera;
18207
18933
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18208
18934
  /**
18209
18935
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19917,12 +20643,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19917
20643
  * providers detect renames by matching `uniqueId` and patch `id`
19918
20644
  * in-place so dispatch keeps working.
19919
20645
  *
19920
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19921
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19922
- * state push and are the single source of truth. The `device_class` tag is
19923
- * an HA-specific input that is consumed at the discovery boundary
19924
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19925
- * stored after that point.
20646
+ * **Display unit and precision are NOT stored here.** They are a read-time
20647
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20648
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20649
+ * neither is repeated on each sample. The `device_class` tag is an
20650
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20651
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19926
20652
  *
19927
20653
  * The `raw` escape hatch carries provider-specific attributes that don't
19928
20654
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19936,6 +20662,14 @@ var SourceInfoSchema = object({
19936
20662
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19937
20663
  * Used to detect rename when `id` changes. */
19938
20664
  uniqueId: string().optional(),
20665
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20666
+ quantity: ReadingQuantitySchema.optional(),
20667
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20668
+ dateTimeShape: _enum([
20669
+ "date",
20670
+ "time",
20671
+ "datetime"
20672
+ ]).optional(),
19939
20673
  /** Provider-specific extras that don't fit the structured slots.
19940
20674
  * Whitelist what goes here per provider — do NOT dump entire HA
19941
20675
  * attribute blobs (would explode the meta JSON column). */
@@ -41040,7 +41774,8 @@ method(object({
41040
41774
  "enum"
41041
41775
  ]),
41042
41776
  enumValues: array(string()).optional(),
41043
- item: boolean().optional()
41777
+ item: boolean().optional(),
41778
+ quantity: ReadingQuantitySchema.optional()
41044
41779
  })).readonly(),
41045
41780
  itemArray: object({
41046
41781
  path: string(),
@@ -43401,7 +44136,7 @@ var NetworkAccessStatusSchema = object({
43401
44136
  });
43402
44137
  /**
43403
44138
  * Optional, richer endpoint shape returned by providers that expose
43404
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44139
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43405
44140
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43406
44141
  * the originating provider config (mode + sourcePort) so the
43407
44142
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44884,8 +45619,8 @@ var MaskPolygonShapeSchema = object({
44884
45619
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44885
45620
  var MaskGridShapeSchema = object({
44886
45621
  kind: literal("grid"),
44887
- gridWidth: number(),
44888
- gridHeight: number(),
45622
+ gridWidth: number().meta(readingMeta("none")),
45623
+ gridHeight: number().meta(readingMeta("none")),
44889
45624
  cells: array(boolean())
44890
45625
  });
44891
45626
  discriminatedUnion("kind", [
@@ -44997,6 +45732,8 @@ var NcSystemEventKindSchema = _enum([
44997
45732
  "detection-blind",
44998
45733
  "motion-noisy",
44999
45734
  "motion-calm",
45735
+ "package-llm-silent",
45736
+ "package-llm-answered",
45000
45737
  "recording-stalled",
45001
45738
  "recording-healthy",
45002
45739
  "recording-degraded",
@@ -46086,7 +46823,20 @@ var NcRuleInputSchema = object({
46086
46823
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46087
46824
  * path does not run Zod defaults.
46088
46825
  */
46089
- groupingSec: number().int().min(5).max(60).optional()
46826
+ groupingSec: number().int().min(5).max(60).optional(),
46827
+ /**
46828
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46829
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46830
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46831
+ * same mark the Library writes, and records whose notification it was and
46832
+ * when. A channel with no buttons gets the same two choices as links.
46833
+ *
46834
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46835
+ * cap path): absent and `false` both mean off, which is what every rule
46836
+ * authored before this field meant. An editor turns it off by writing
46837
+ * `false`, so it needs no `clear` key.
46838
+ */
46839
+ askVerdict: boolean().optional()
46090
46840
  });
46091
46841
  /**
46092
46842
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -56192,6 +56942,28 @@ var PackageVersionInfoSchema = object({
56192
56942
  deprecated: string().optional(),
56193
56943
  distTags: array(string()).readonly()
56194
56944
  });
56945
+ /**
56946
+ * One published document. `ok` is markdown from the registry. `missing` means
56947
+ * the registry answered and the file was not there. `error` is a registry or
56948
+ * tarball failure — never an empty string standing in for either.
56949
+ */
56950
+ var PackageDocBodySchema = discriminatedUnion("status", [
56951
+ object({
56952
+ status: literal("ok"),
56953
+ markdown: string()
56954
+ }),
56955
+ object({ status: literal("missing") }),
56956
+ object({
56957
+ status: literal("error"),
56958
+ message: string()
56959
+ })
56960
+ ]);
56961
+ var PackageDocsSchema = object({
56962
+ name: string(),
56963
+ version: string(),
56964
+ readme: PackageDocBodySchema,
56965
+ changelog: PackageDocBodySchema
56966
+ });
56195
56967
  var SearchResultSchema = object({
56196
56968
  name: string(),
56197
56969
  version: string(),
@@ -56410,7 +57182,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56410
57182
  }), object({ success: literal(true) }), {
56411
57183
  kind: "mutation",
56412
57184
  auth: "admin"
56413
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57185
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57186
+ name: string().min(1),
57187
+ version: string().min(1).optional()
57188
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56414
57189
  kind: "mutation",
56415
57190
  auth: "admin"
56416
57191
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56469,27 +57244,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56469
57244
  */
56470
57245
  var AirQualitySensorStatusSchema = object({
56471
57246
  /** Carbon dioxide concentration in ppm. */
56472
- co2Ppm: number().min(0).optional(),
57247
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56473
57248
  /** Total volatile organic compounds in ppb. */
56474
- vocPpb: number().min(0).optional(),
57249
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56475
57250
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56476
- pm25: number().min(0).optional(),
57251
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56477
57252
  /** Particulate matter ≤ 10 μm in µg/m³. */
56478
- pm10: number().min(0).optional(),
57253
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56479
57254
  /** Composite AQI value (typically 0..500). */
56480
- aqi: number().optional(),
57255
+ aqi: number().optional().meta(readingMeta("index")),
56481
57256
  /** Ms epoch when the slice was last updated. */
56482
- lastFetchedAt: number(),
56483
- /** Live display unit of the single metric this slice carries (e.g. HA
56484
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56485
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56486
- * per slice is unambiguous. */
56487
- unit: string().optional(),
56488
- /** Suggested decimal places for numeric display.
56489
- * Populated live from the upstream source when provided (e.g. HA
56490
- * `attributes.suggested_display_precision`). Falls back to
56491
- * auto-formatting when absent. */
56492
- precision: number().int().min(0).max(10).optional()
57257
+ lastFetchedAt: number()
56493
57258
  });
56494
57259
  var airQualitySensorCapability = {
56495
57260
  name: "air-quality-sensor",
@@ -56526,12 +57291,12 @@ var airQualitySensorCapability = {
56526
57291
  },
56527
57292
  pm25: {
56528
57293
  kind: "gauge",
56529
- unit: "ug/m3",
57294
+ unit: "µg/m³",
56530
57295
  deadband: 1
56531
57296
  },
56532
57297
  pm10: {
56533
57298
  kind: "gauge",
56534
- unit: "ug/m3",
57299
+ unit: "µg/m³",
56535
57300
  deadband: 1
56536
57301
  },
56537
57302
  aqi: {
@@ -56549,22 +57314,13 @@ var airQualitySensorCapability = {
56549
57314
  * entries with `device_class: illuminance`.
56550
57315
  */
56551
57316
  var AmbientLightSensorStatusSchema = object({
56552
- /** Current illuminance in lux (lx). */
56553
- lux: number().min(0).meta({
56554
- unit: "lx",
57317
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57318
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57319
+ precision: 0,
56555
57320
  label: "Illuminance"
56556
- }),
57321
+ })),
56557
57322
  /** Ms epoch when the slice was last updated. */
56558
- lastFetchedAt: number(),
56559
- /** Live display unit from the upstream source (e.g. HA
56560
- * `attributes.unit_of_measurement`). The UI prefers this over the
56561
- * role's canonical unit. Absent → fall back to the canonical unit. */
56562
- unit: string().optional(),
56563
- /** Suggested decimal places for numeric display.
56564
- * Populated live from the upstream source when provided (e.g. HA
56565
- * `attributes.suggested_display_precision`). Falls back to
56566
- * auto-formatting when absent. */
56567
- precision: number().int().min(0).max(10).optional()
57323
+ lastFetchedAt: number()
56568
57324
  });
56569
57325
  var ambientLightSensorCapability = {
56570
57326
  name: "ambient-light-sensor",
@@ -56596,12 +57352,7 @@ var ambientLightSensorCapability = {
56596
57352
  } } },
56597
57353
  /** Clock fields: written, but excluded from the compare that decides
56598
57354
  * whether persisting is worth a SQLite commit. */
56599
- volatileStateFields: ["lastFetchedAt"],
56600
- customize: {
56601
- version: 1,
56602
- advanced: "derive",
56603
- targets: []
56604
- }
57355
+ volatileStateFields: ["lastFetchedAt"]
56605
57356
  };
56606
57357
  /**
56607
57358
  * Per-class audio metrics aggregated over a sliding window.
@@ -56828,10 +57579,7 @@ var BatteryStatusSchema = object({
56828
57579
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56829
57580
  * declines to band a non-finite reading, which is the correct shape.
56830
57581
  */
56831
- percentage: number().min(0).max(100).nullable().meta({
56832
- unit: "%",
56833
- label: "Charge"
56834
- }),
57582
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56835
57583
  /**
56836
57584
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56837
57585
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56858,7 +57606,14 @@ var BatteryStatusSchema = object({
56858
57606
  */
56859
57607
  sleeping: boolean().meta({ label: "Sleeping" }),
56860
57608
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56861
- lastUpdated: number(),
57609
+ lastUpdated: number().meta(readingMeta("none")),
57610
+ /**
57611
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57612
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57613
+ * that only report charge. A second reading on this cap, not a second
57614
+ * `numeric-sensor` on the same device.
57615
+ */
57616
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56862
57617
  /**
56863
57618
  * Ms epoch of the last time the device PROVED it was reachable — a
56864
57619
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56905,12 +57660,12 @@ var BATTERY_DEVICE_TYPES = [
56905
57660
  DeviceType.Switch,
56906
57661
  DeviceType.PetFeeder,
56907
57662
  DeviceType.Container,
56908
- DeviceType.Thermostat
57663
+ DeviceType.Thermostat,
57664
+ DeviceType.EventEmitter
56909
57665
  ];
56910
57666
  /** Generic Customize target for the percentage reading (D936). */
56911
57667
  var batteryCustomize = {
56912
57668
  version: 1,
56913
- advanced: "derive",
56914
57669
  targets: [{
56915
57670
  id: "percentage",
56916
57671
  cardinality: "scalar",
@@ -56919,7 +57674,7 @@ var batteryCustomize = {
56919
57674
  stateField: "percentage"
56920
57675
  },
56921
57676
  deviceTypes: BATTERY_DEVICE_TYPES,
56922
- apply: "add-or-replace",
57677
+ apply: "add",
56923
57678
  path: "percentage",
56924
57679
  sources: [{
56925
57680
  kind: "field",
@@ -57040,10 +57795,16 @@ onStatusChanged: { data: object({
57040
57795
  /**
57041
57796
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57042
57797
  */
57043
- history: { fields: { percentage: {
57044
- kind: "gauge",
57045
- unit: "%"
57046
- } } },
57798
+ history: { fields: {
57799
+ percentage: {
57800
+ kind: "gauge",
57801
+ unit: "%"
57802
+ },
57803
+ volts: {
57804
+ kind: "gauge",
57805
+ unit: "V"
57806
+ }
57807
+ } },
57047
57808
  customize: batteryCustomize,
57048
57809
  /** Clock fields: written, but excluded from the compare that decides
57049
57810
  * whether persisting is worth a SQLite commit. */
@@ -57090,12 +57851,7 @@ var binaryCapability = {
57090
57851
  history: { fields: { on: { kind: "boolean" } } },
57091
57852
  /** Clock fields: written, but excluded from the compare that decides
57092
57853
  * whether persisting is worth a SQLite commit. */
57093
- volatileStateFields: ["lastChangedAt"],
57094
- customize: {
57095
- version: 1,
57096
- advanced: "derive",
57097
- targets: []
57098
- }
57854
+ volatileStateFields: ["lastChangedAt"]
57099
57855
  };
57100
57856
  /**
57101
57857
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57111,7 +57867,7 @@ var binaryCapability = {
57111
57867
  */
57112
57868
  var BrightnessStatusSchema = object({
57113
57869
  /** Current level as 0..100 inclusive. Firmware-reported. */
57114
- percentage: number().min(0).max(100),
57870
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57115
57871
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57116
57872
  lastChangedAt: number()
57117
57873
  });
@@ -57565,21 +58321,21 @@ var ClimateControlStatusSchema = object({
57565
58321
  /** Single setpoint in Celsius. Used by single-target modes
57566
58322
  * (heat / cool / dry). Null when the device is in a range mode
57567
58323
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57568
- target: number().nullable(),
58324
+ target: number().nullable().meta(readingMeta("temperature")),
57569
58325
  /** Upper bound of the dual setpoint in Celsius. Populated only
57570
58326
  * in `heat_cool` mode. */
57571
- targetHigh: number().nullable(),
58327
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57572
58328
  /** Lower bound of the dual setpoint in Celsius. Populated only
57573
58329
  * in `heat_cool` mode. */
57574
- targetLow: number().nullable(),
58330
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57575
58331
  /** Target relative humidity (0..100). Null when the device has no
57576
58332
  * humidity control. */
57577
- targetHumidity: number().min(0).max(100).nullable(),
58333
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57578
58334
  /** Current measured temperature in Celsius. Read-only — pushed by
57579
58335
  * the device's internal sensor. */
57580
- currentTemp: number().nullable(),
58336
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57581
58337
  /** Current measured relative humidity (0..100). Read-only. */
57582
- currentHumidity: number().min(0).max(100).nullable(),
58338
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57583
58339
  /** Vertical louver swing on/off. Null when the device has no vertical
57584
58340
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57585
58341
  swingVertical: boolean().nullable(),
@@ -57587,11 +58343,11 @@ var ClimateControlStatusSchema = object({
57587
58343
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57588
58344
  swingHorizontal: boolean().nullable(),
57589
58345
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57590
- minTemp: number().nullable().optional(),
58346
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57591
58347
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57592
- maxTemp: number().nullable().optional(),
58348
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57593
58349
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57594
- tempStep: number().positive().nullable().optional(),
58350
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57595
58351
  /** Ms epoch when the slice was last updated (push or command). */
57596
58352
  lastFetchedAt: number()
57597
58353
  });
@@ -57733,17 +58489,17 @@ var climateControlCapability = {
57733
58489
  * modes to expose in the picker without a slice fetch.
57734
58490
  */
57735
58491
  var RgbTripletSchema = object({
57736
- r: number().int().min(0).max(255),
57737
- g: number().int().min(0).max(255),
57738
- b: number().int().min(0).max(255)
58492
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58493
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58494
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57739
58495
  });
57740
58496
  var HsvTripletSchema = object({
57741
58497
  /** Hue in degrees, 0..360. */
57742
- h: number().min(0).max(360),
58498
+ h: number().min(0).max(360).meta(readingMeta("none")),
57743
58499
  /** Saturation as 0..100 inclusive. */
57744
- s: number().min(0).max(100),
58500
+ s: number().min(0).max(100).meta(readingMeta("none")),
57745
58501
  /** Value/brightness as 0..100 inclusive. */
57746
- v: number().min(0).max(100)
58502
+ v: number().min(0).max(100).meta(readingMeta("none"))
57747
58503
  });
57748
58504
  var ColorInputSchema = discriminatedUnion("mode", [
57749
58505
  object({
@@ -57772,7 +58528,7 @@ var ColorStatusSchema = object({
57772
58528
  /** Populated when `mode === 'hsv'`. */
57773
58529
  hsv: HsvTripletSchema.optional(),
57774
58530
  /** Populated when `mode === 'mired'`. */
57775
- mireds: number().int().optional(),
58531
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57776
58532
  /** Ms epoch of the last operator-driven change. */
57777
58533
  lastChangedAt: number()
57778
58534
  });
@@ -57975,18 +58731,12 @@ _enum([
57975
58731
  "empty",
57976
58732
  "full"
57977
58733
  ]);
57978
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57979
- * each. */
57980
- var ConsumableRemainingUnitSchema = _enum([
57981
- "days",
57982
- "hours",
57983
- "cycles"
57984
- ]);
57985
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57986
- * days). `value` null = unknown (D393). */
58734
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58735
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58736
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57987
58737
  var ConsumableRemainingSchema = object({
57988
- value: number().min(0).nullable(),
57989
- unit: ConsumableRemainingUnitSchema
58738
+ value: number().min(0).nullable().meta(readingMeta("device")),
58739
+ kind: ConsumableRemainingKindSchema
57990
58740
  });
57991
58741
  /** A single consumable item. Either a continuous `level` (remaining
57992
58742
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57999,7 +58749,7 @@ var ConsumableItemSchema = object({
57999
58749
  /** Display name. */
58000
58750
  label: string().min(1),
58001
58751
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58002
- level: number().min(0).max(100).nullable(),
58752
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58003
58753
  /** The provider's discrete verdict when known (binary mode). */
58004
58754
  status: ConsumableStatusSchema.nullable(),
58005
58755
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58081,8 +58831,8 @@ var consumablesCustomize = {
58081
58831
  transforms: [{ kind: "identity" }]
58082
58832
  }],
58083
58833
  literals: [{
58084
- path: "remaining.unit",
58085
- values: [...ConsumableRemainingUnitSchema.options]
58834
+ path: "remaining.kind",
58835
+ values: [...ConsumableRemainingKindSchema.options]
58086
58836
  }]
58087
58837
  },
58088
58838
  {
@@ -58211,7 +58961,6 @@ reset: method(object({
58211
58961
  },
58212
58962
  "remaining.value": {
58213
58963
  kind: "gauge",
58214
- unitFrom: "remaining.unit",
58215
58964
  deadband: 1,
58216
58965
  displayName: "Remaining"
58217
58966
  }
@@ -58281,10 +59030,9 @@ var contactCapability = {
58281
59030
  * (empty array for the other kinds). It lives in the slice because
58282
59031
  * HA `select.*` entities may update their options at runtime.
58283
59032
  *
58284
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58285
- * `kind === 'numeric'` and are absent (undefined) for all other
58286
- * kinds. They are populated live from HA attributes on each state
58287
- * push — consistent with `options` and the sensor-unit approach.
59033
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59034
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59035
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58288
59036
  */
58289
59037
  var ControlStatusSchema = object({
58290
59038
  kind: _enum([
@@ -58296,39 +59044,17 @@ var ControlStatusSchema = object({
58296
59044
  /** Current value. Numeric kind → number; select/text/datetime → string.
58297
59045
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58298
59046
  * or time-only — HA accepts all three forms). */
58299
- value: union([number(), string()]),
59047
+ value: union([number().meta(readingMeta("device")), string()]),
58300
59048
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58301
59049
  options: array(string()),
58302
59050
  /** Ms epoch when the slice was last updated. */
58303
59051
  lastChangedAt: number(),
58304
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58305
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58306
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58307
- unit: string().optional(),
58308
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58309
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58310
- min: number().optional(),
58311
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58312
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58313
- max: number().optional(),
58314
- /** Step increment. Populated live from HA `attributes.step`.
58315
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58316
- step: number().optional(),
58317
- /** Suggested decimal places for numeric display. Populated live from HA
58318
- * `attributes.suggested_display_precision`, or derived from `step` when the
58319
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58320
- * other kinds. Falls back to auto-formatting when absent. */
58321
- precision: number().int().min(0).max(10).optional(),
58322
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58323
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58324
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58325
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58326
- * `has_date`/`has_time` attributes on each push. */
58327
- format: _enum([
58328
- "date",
58329
- "time",
58330
- "datetime"
58331
- ]).optional()
59052
+ /** Minimum allowed value, in the device quantity's base unit. */
59053
+ min: number().optional().meta(readingMeta("device")),
59054
+ /** Maximum allowed value, in the device quantity's base unit. */
59055
+ max: number().optional().meta(readingMeta("device")),
59056
+ /** Step increment, in the device quantity's base unit. */
59057
+ step: number().optional().meta(readingMeta("device"))
58332
59058
  });
58333
59059
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58334
59060
  object({
@@ -58394,9 +59120,9 @@ var CoverStatusSchema = object({
58394
59120
  ]),
58395
59121
  /** 0 = fully closed, 100 = fully open. Null when the device has
58396
59122
  * no intermediate position surface. */
58397
- position: number().min(0).max(100).nullable(),
59123
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58398
59124
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58399
- tiltPosition: number().min(0).max(100).nullable(),
59125
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58400
59126
  /** Ms epoch when the slice was last updated. */
58401
59127
  lastChangedAt: number()
58402
59128
  });
@@ -58499,9 +59225,9 @@ var NormalizedRangeSchema$1 = object({
58499
59225
  var DayNightStatusSchema = object({
58500
59226
  mode: DayNightModeSchema,
58501
59227
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58502
- sensitivity: number().optional(),
59228
+ sensitivity: number().optional().meta(readingMeta("none")),
58503
59229
  /** Delay before the IR-cut filter flips, in seconds. */
58504
- switchDelaySec: number().optional(),
59230
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58505
59231
  lastFetchedAt: number()
58506
59232
  });
58507
59233
  /**
@@ -58719,11 +59445,11 @@ var DishwasherStatusSchema = object({
58719
59445
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58720
59446
  availablePrograms: array(string()),
58721
59447
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58722
- remainingSeconds: number().int().nonnegative().nullable(),
59448
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58723
59449
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58724
- delayedStartSeconds: number().int().nonnegative().nullable(),
59450
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58725
59451
  /** Cycle completion 0..100, or null when not applicable. */
58726
- progressPercent: number().min(0).max(100).nullable(),
59452
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58727
59453
  /** True when the door is reported open; null when the door state is not known. */
58728
59454
  doorOpen: boolean().nullable(),
58729
59455
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58747,7 +59473,7 @@ var DishwasherStatusSchema = object({
58747
59473
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58748
59474
  machineCareDue: boolean().nullable(),
58749
59475
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58750
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59476
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58751
59477
  /** Option states: null when the appliance does not expose the option or has not said. */
58752
59478
  halfLoad: boolean().nullable(),
58753
59479
  hygienePlus: boolean().nullable(),
@@ -58762,7 +59488,7 @@ var DishwasherStatusSchema = object({
58762
59488
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58763
59489
  activeEvents: array(string()),
58764
59490
  /** Numeric error code when the appliance reports one; null otherwise. */
58765
- errorCode: number().nullable(),
59491
+ errorCode: number().nullable().meta(readingMeta("none")),
58766
59492
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58767
59493
  errorLabel: string().nullable(),
58768
59494
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58993,7 +59719,7 @@ var DoorbellStatusSchema = object({
58993
59719
  /** Ms epoch of the last press. null = never observed since this provider started. */
58994
59720
  lastPressedAt: number().nullable(),
58995
59721
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58996
- pressCountSinceStart: number()
59722
+ pressCountSinceStart: number().meta(readingMeta("count"))
58997
59723
  });
58998
59724
  var DoorbellPressEventSchema = object({
58999
59725
  deviceId: number(),
@@ -59058,17 +59784,7 @@ onPressed: {
59058
59784
  history: "none",
59059
59785
  customize: doorbellCustomize
59060
59786
  };
59061
- /**
59062
- * Enum-state sensor — a string value picked from a finite option set.
59063
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59064
- * states, weather conditions, contact-source channels, mode strings).
59065
- * The option set is stable across observations; it is persisted in the
59066
- * device config blob (via the control slice at adoption) and is NOT
59067
- * stored in `sourceInfo`.
59068
- */
59069
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59070
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59071
- var EnumSensorDateTimeFormatSchema = _enum([
59787
+ _enum([
59072
59788
  "date",
59073
59789
  "time",
59074
59790
  "datetime"
@@ -59076,12 +59792,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59076
59792
  var EnumSensorStatusSchema = object({
59077
59793
  value: string(),
59078
59794
  /**
59079
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59080
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59081
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59082
- * a time-only sensor → `'time'`.
59795
+ * Set when `value` is a timestamp rather than an enum state, so history
59796
+ * does not learn ISO strings as states. The display shape (date / time /
59797
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59083
59798
  */
59084
- format: EnumSensorDateTimeFormatSchema.optional(),
59799
+ timestamp: boolean().optional(),
59085
59800
  /** Ms epoch when the slice was last updated. */
59086
59801
  lastFetchedAt: number()
59087
59802
  });
@@ -59108,11 +59823,11 @@ var enumSensorCapability = {
59108
59823
  * History (D850): the value as an `enum` series (an appliance's program or
59109
59824
  * operation state, an HVAC action). The option set is per device, so the
59110
59825
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59111
- * `format` carries a timestamp, not a state, and is not recorded.
59826
+ * `timestamp` carries a clock, not a state, and is not recorded.
59112
59827
  */
59113
59828
  history: { fields: { value: {
59114
59829
  kind: "enum",
59115
- skipWhenSet: "format"
59830
+ skipWhenSet: "timestamp"
59116
59831
  } } },
59117
59832
  /** Clock fields: written, but excluded from the compare that decides
59118
59833
  * whether persisting is worth a SQLite commit. */
@@ -59132,13 +59847,13 @@ var EventFireSchema = object({
59132
59847
  deviceId: number(),
59133
59848
  eventType: string(),
59134
59849
  data: record(string(), unknown()).nullable(),
59135
- timestamp: number(),
59136
- seq: number()
59850
+ timestamp: number().meta(readingMeta("none")),
59851
+ seq: number().meta(readingMeta("count"))
59137
59852
  });
59138
59853
  var EventEmitterStatusSchema = object({
59139
59854
  eventTypes: array(string()),
59140
59855
  lastEvent: EventFireSchema.nullable(),
59141
- eventCountSinceStart: number()
59856
+ eventCountSinceStart: number().meta(readingMeta("count"))
59142
59857
  });
59143
59858
  var eventEmitterCapability = {
59144
59859
  name: "event-emitter",
@@ -59551,10 +60266,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59551
60266
  var FanControlStatusSchema = object({
59552
60267
  /** Active speed as 0..100 inclusive. Null when the device has no
59553
60268
  * speed surface (single-speed fan). */
59554
- percentage: number().min(0).max(100).nullable(),
60269
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59555
60270
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59556
60271
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59557
- percentageStep: number().positive().optional(),
60272
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59558
60273
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59559
60274
  * — empty string when no preset is active or supported. */
59560
60275
  preset: string(),
@@ -59675,7 +60390,7 @@ var FeatureProbeStatusSchema = object({
59675
60390
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59676
60391
  model: string().nullable(),
59677
60392
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59678
- channelCount: number().nullable(),
60393
+ channelCount: number().nullable().meta(readingMeta("count")),
59679
60394
  /**
59680
60395
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59681
60396
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59798,9 +60513,151 @@ var gasCapability = {
59798
60513
  * whether persisting is worth a SQLite commit. */
59799
60514
  volatileStateFields: ["lastChangedAt"]
59800
60515
  };
60516
+ var GroupCommandsSchema = object({
60517
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60518
+ results: array(object({
60519
+ deviceId: number().int().nonnegative(),
60520
+ cap: string().min(1),
60521
+ method: string().min(1),
60522
+ ok: boolean(),
60523
+ error: string().optional(),
60524
+ /** Ms epoch of the attempt. */
60525
+ at: number()
60526
+ })) });
60527
+ var groupCommandsCapability = {
60528
+ name: "group-commands",
60529
+ scope: "device",
60530
+ deviceNative: true,
60531
+ mode: "singleton",
60532
+ deviceTypes: [
60533
+ DeviceType.Switch,
60534
+ DeviceType.Light,
60535
+ DeviceType.Fan,
60536
+ DeviceType.Cover,
60537
+ DeviceType.Valve,
60538
+ DeviceType.Lock,
60539
+ DeviceType.MediaPlayer,
60540
+ DeviceType.Thermostat,
60541
+ DeviceType.Climate
60542
+ ],
60543
+ methods: {},
60544
+ status: {
60545
+ schema: GroupCommandsSchema,
60546
+ kind: "push"
60547
+ },
60548
+ runtimeState: GroupCommandsSchema,
60549
+ /**
60550
+ * Runtime-state durability: **session** — the last attempt. A restored
60551
+ * failure would outlive the command that caused it.
60552
+ *
60553
+ * See `RuntimeStateDurability`. Enforced by
60554
+ * `scripts/check-runtime-state-durability.ts`.
60555
+ */
60556
+ durability: "session",
60557
+ history: "none"
60558
+ };
60559
+ /**
60560
+ * Whether a helper actuator group counts as active (D944): at least one
60561
+ * included member is, or every included member is. What "active" means is the
60562
+ * type's: a cover is open, a lock is locked, a media player is playing, a
60563
+ * climate device is not off. A group of lights, switches and fans reports
60564
+ * `switch.on` instead; this cap is for the types whose own state is not a
60565
+ * yes/no. Unknown members are left out (`group-status` counts them) and an
60566
+ * empty included set takes the device offline, never `active: false`.
60567
+ */
60568
+ var GroupActiveSchema = object({
60569
+ active: boolean().meta({ label: "Active" }),
60570
+ /** Ms epoch when the slice was last updated. */
60571
+ lastChangedAt: number()
60572
+ });
60573
+ var groupActiveCapability = {
60574
+ name: "group-active",
60575
+ scope: "device",
60576
+ deviceNative: true,
60577
+ mode: "singleton",
60578
+ deviceTypes: [
60579
+ DeviceType.Cover,
60580
+ DeviceType.Valve,
60581
+ DeviceType.Lock,
60582
+ DeviceType.MediaPlayer,
60583
+ DeviceType.Climate,
60584
+ DeviceType.Thermostat
60585
+ ],
60586
+ methods: {},
60587
+ status: {
60588
+ schema: GroupActiveSchema,
60589
+ kind: "push"
60590
+ },
60591
+ runtimeState: GroupActiveSchema,
60592
+ /**
60593
+ * Runtime-state durability: **session** — recomputed from the members on the
60594
+ * next source event; a restored value would outlive the members it named.
60595
+ *
60596
+ * See `RuntimeStateDurability`. Enforced by
60597
+ * `scripts/check-runtime-state-durability.ts`.
60598
+ */
60599
+ durability: "session",
60600
+ history: "none"
60601
+ };
60602
+ /**
60603
+ * How many members of a helper group answered (D944). Counts are integers,
60604
+ * not measurements: zero included is a real count, never a stand-in reading.
60605
+ * The reading itself stays on the output capability. History is none — the
60606
+ * series that later statistics read is the output cap's own history block.
60607
+ */
60608
+ var GroupStatusSchema = object({
60609
+ /** Members whose current value entered the aggregate. */
60610
+ included: number().int().nonnegative().meta(readingMeta("count", {
60611
+ precision: 0,
60612
+ label: "Reporting"
60613
+ })),
60614
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60615
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60616
+ precision: 0,
60617
+ label: "Excluded"
60618
+ })),
60619
+ /** `included` + `excluded`. */
60620
+ total: number().int().nonnegative().meta(readingMeta("count", {
60621
+ precision: 0,
60622
+ label: "Members"
60623
+ }))
60624
+ });
60625
+ var groupStatusCapability = {
60626
+ name: "group-status",
60627
+ scope: "device",
60628
+ deviceNative: true,
60629
+ mode: "singleton",
60630
+ deviceTypes: [
60631
+ DeviceType.Sensor,
60632
+ DeviceType.Switch,
60633
+ DeviceType.Light,
60634
+ DeviceType.Fan,
60635
+ DeviceType.Cover,
60636
+ DeviceType.Valve,
60637
+ DeviceType.Lock,
60638
+ DeviceType.MediaPlayer,
60639
+ DeviceType.Thermostat,
60640
+ DeviceType.Climate
60641
+ ],
60642
+ methods: {},
60643
+ status: {
60644
+ schema: GroupStatusSchema,
60645
+ kind: "push"
60646
+ },
60647
+ runtimeState: GroupStatusSchema,
60648
+ /**
60649
+ * Runtime-state durability: **session** — recomputed from members on the
60650
+ * next source event; a restored count would outlive the readings it named.
60651
+ *
60652
+ * See `RuntimeStateDurability`. Enforced by
60653
+ * `scripts/check-runtime-state-durability.ts`.
60654
+ */
60655
+ durability: "session",
60656
+ history: "none"
60657
+ };
59801
60658
  var HeatingValveStatusSchema = object({
59802
60659
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59803
- position: number().min(0).max(100).nullable(),
60660
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59804
60661
  /** Ms epoch when the slice was last updated (push or poll). */
59805
60662
  lastFetchedAt: number()
59806
60663
  });
@@ -59843,9 +60700,9 @@ var HumidifierStatusSchema = object({
59843
60700
  /** Whether the humidifier is currently on. */
59844
60701
  on: boolean(),
59845
60702
  /** Current measured relative humidity (0..100). Null when not reported. */
59846
- currentHumidity: number().min(0).max(100).nullable(),
60703
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59847
60704
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59848
- targetHumidity: number().min(0).max(100).nullable(),
60705
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59849
60706
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59850
60707
  * device has no mode surface. */
59851
60708
  mode: string().nullable(),
@@ -59855,9 +60712,9 @@ var HumidifierStatusSchema = object({
59855
60712
  * `idle` / `off`). Null when the device doesn't report it. */
59856
60713
  action: string().nullable(),
59857
60714
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59858
- minHumidity: number().nullable(),
60715
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59859
60716
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59860
- maxHumidity: number().nullable(),
60717
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59861
60718
  /** Ms epoch when the slice was last updated. */
59862
60719
  lastChangedAt: number()
59863
60720
  });
@@ -59927,22 +60784,13 @@ var humidifierCapability = {
59927
60784
  * `sourceInfo`.
59928
60785
  */
59929
60786
  var HumiditySensorStatusSchema = object({
59930
- /** Current relative humidity, 0..100. */
59931
- percent: number().min(0).max(100).meta({
59932
- unit: "%",
60787
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60788
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60789
+ precision: 0,
59933
60790
  label: "Humidity"
59934
- }),
60791
+ })),
59935
60792
  /** Ms epoch when the slice was last updated. */
59936
- lastFetchedAt: number(),
59937
- /** Live display unit from the upstream source (e.g. HA
59938
- * `attributes.unit_of_measurement`). The UI prefers this over the
59939
- * role's canonical unit. Absent → fall back to the canonical unit. */
59940
- unit: string().optional(),
59941
- /** Suggested decimal places for numeric display.
59942
- * Populated live from the upstream source when provided (e.g. HA
59943
- * `attributes.suggested_display_precision`). Falls back to
59944
- * auto-formatting when absent. */
59945
- precision: number().int().min(0).max(10).optional()
60793
+ lastFetchedAt: number()
59946
60794
  });
59947
60795
  var humiditySensorCapability = {
59948
60796
  name: "humidity-sensor",
@@ -59973,12 +60821,7 @@ var humiditySensorCapability = {
59973
60821
  } } },
59974
60822
  /** Clock fields: written, but excluded from the compare that decides
59975
60823
  * whether persisting is worth a SQLite commit. */
59976
- volatileStateFields: ["lastFetchedAt"],
59977
- customize: {
59978
- version: 1,
59979
- advanced: "derive",
59980
- targets: []
59981
- }
60824
+ volatileStateFields: ["lastFetchedAt"]
59982
60825
  };
59983
60826
  /**
59984
60827
  * Image display cap. Models a single still image exposed by an integration —
@@ -59999,7 +60842,7 @@ var ImageStatusSchema = object({
59999
60842
  * entity exposes no `entity_picture` (yet). */
60000
60843
  url: string().nullable(),
60001
60844
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60002
- lastUpdated: number().nullable()
60845
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60003
60846
  });
60004
60847
  var imageCapability = {
60005
60848
  name: "image",
@@ -60087,19 +60930,19 @@ var NormalizedRangeSchema = object({
60087
60930
  */
60088
60931
  var ImageSettingsStatusSchema = object({
60089
60932
  /** Normalized 0–100. */
60090
- brightness: number().optional(),
60933
+ brightness: number().optional().meta(readingMeta("none")),
60091
60934
  /** Normalized 0–100. */
60092
- contrast: number().optional(),
60935
+ contrast: number().optional().meta(readingMeta("none")),
60093
60936
  /** Normalized 0–100. */
60094
- saturation: number().optional(),
60937
+ saturation: number().optional().meta(readingMeta("none")),
60095
60938
  /** Normalized 0–100. */
60096
- sharpness: number().optional(),
60939
+ sharpness: number().optional().meta(readingMeta("none")),
60097
60940
  mirror: boolean().optional(),
60098
60941
  flip: boolean().optional(),
60099
60942
  rotate: ImageRotateSchema.optional(),
60100
60943
  whiteBalance: WhiteBalanceModeSchema.optional(),
60101
60944
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60102
- warmth: number().optional(),
60945
+ warmth: number().optional().meta(readingMeta("none")),
60103
60946
  exposureMode: ExposureModeSchema.optional(),
60104
60947
  backlightMode: BacklightModeSchema.optional(),
60105
60948
  lastFetchedAt: number()
@@ -60378,10 +61221,10 @@ var IntercomAbilitySchema = object({
60378
61221
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60379
61222
  codecs: array(string()),
60380
61223
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60381
- sampleRate: number(),
61224
+ sampleRate: number().meta(readingMeta("none")),
60382
61225
  duplex: _enum(["half", "full"]),
60383
61226
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60384
- maxBacklogMs: number()
61227
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60385
61228
  });
60386
61229
  var IntercomStatusSchema = object({
60387
61230
  /** True while a talk session is open. */
@@ -60608,13 +61451,13 @@ var LawnMowerControlStatusSchema = object({
60608
61451
  activity: LawnMowerActivitySchema,
60609
61452
  /** 0..100 battery percentage. Null when the device has no battery
60610
61453
  * reading. */
60611
- batteryLevel: number().min(0).max(100).nullable(),
61454
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60612
61455
  /** 0..100 mowing-completion percentage of the current task, or null when no
60613
61456
  * task is active / progress is unavailable. */
60614
- progressPercent: number().min(0).max(100).nullable(),
61457
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60615
61458
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60616
61459
  * or null when unknown. */
60617
- currentCode: number().nullable(),
61460
+ currentCode: number().nullable().meta(readingMeta("none")),
60618
61461
  /** Human label for {@link currentCode}, or null when undecodable. */
60619
61462
  currentCodeLabel: string().nullable(),
60620
61463
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61067,7 +61910,14 @@ var lockControlCapability = {
61067
61910
  }),
61068
61911
  unlock: method(object({
61069
61912
  deviceId: number().int().nonnegative(),
61070
- code: string().min(1).optional()
61913
+ code: string().min(1).optional(),
61914
+ /**
61915
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
61916
+ * with one call is refused unless the caller says it was confirmed. A
61917
+ * single lock ignores it. The refusal lives in the callee, so no
61918
+ * automation or API caller reaches the doors by skipping a dialog.
61919
+ */
61920
+ confirm: boolean().optional()
61071
61921
  }), _void(), {
61072
61922
  kind: "mutation",
61073
61923
  auth: "protected"
@@ -61104,6 +61954,44 @@ var lockControlCapability = {
61104
61954
  volatileStateFields: ["lastChangedAt"]
61105
61955
  };
61106
61956
  /**
61957
+ * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
61958
+ * so dashboards keep one series; every other instantaneous reading lands here.
61959
+ * `value` is the time integral in the source quantity's accumulated base
61960
+ * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
61961
+ */
61962
+ var MeterStatusSchema = object({
61963
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
61964
+ lastFetchedAt: number()
61965
+ });
61966
+ var meterCapability = {
61967
+ name: "meter",
61968
+ scope: "device",
61969
+ deviceNative: true,
61970
+ mode: "singleton",
61971
+ deviceTypes: [DeviceType.Sensor],
61972
+ methods: {},
61973
+ status: {
61974
+ schema: MeterStatusSchema,
61975
+ kind: "push"
61976
+ },
61977
+ runtimeState: MeterStatusSchema,
61978
+ /**
61979
+ * Runtime-state durability: **restored** — the total is a cache of the
61980
+ * history integral. A restart shows it only until the history pass
61981
+ * recomputes the open period; it is not an in-memory buffer.
61982
+ *
61983
+ * See `RuntimeStateDurability`. Enforced by
61984
+ * `scripts/check-runtime-state-durability.ts`.
61985
+ */
61986
+ durability: "restored",
61987
+ history: { fields: { value: {
61988
+ kind: "counter",
61989
+ deadband: .001,
61990
+ minIntervalMs: 6e4
61991
+ } } },
61992
+ volatileStateFields: ["lastFetchedAt"]
61993
+ };
61994
+ /**
61107
61995
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
61108
61996
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
61109
61997
  * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
@@ -61147,7 +62035,7 @@ var MediaPlayerStatusSchema = object({
61147
62035
  state: MediaPlayerStateSchema,
61148
62036
  /** Volume as 0..100 inclusive. Null when the device has no volume
61149
62037
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61150
- volumeLevel: number().min(0).max(100).nullable(),
62038
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61151
62039
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61152
62040
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61153
62041
  isMuted: boolean().nullable(),
@@ -61160,10 +62048,10 @@ var MediaPlayerStatusSchema = object({
61160
62048
  currentMedia: MediaInfoSchema.nullable(),
61161
62049
  /** Current playback position in ms. Null when not seekable or
61162
62050
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61163
- positionMs: number().int().nonnegative().nullable(),
62051
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61164
62052
  /** Total duration of the current media in ms. Null when unknown
61165
62053
  * (live stream) or nothing is playing. */
61166
- durationMs: number().int().nonnegative().nullable(),
62054
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61167
62055
  /** Shuffle toggle. Null when no shuffle surface.
61168
62056
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61169
62057
  shuffle: boolean().nullable(),
@@ -61472,7 +62360,7 @@ var motionCustomize = {
61472
62360
  eventName: "onMotionChanged"
61473
62361
  },
61474
62362
  deviceTypes: [DeviceType.Camera],
61475
- apply: "replace",
62363
+ apply: "add",
61476
62364
  applyAuthority: {
61477
62365
  kind: "camera-sensors",
61478
62366
  timeoutField: "motionCooldownMs"
@@ -61544,7 +62432,7 @@ var MotionStatusSchema = object({
61544
62432
  * it, every provider seeded it at boot, and every session left it
61545
62433
  * `detected: true` after its detach.
61546
62434
  */
61547
- autoClearAfterMs: number().nullable()
62435
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61548
62436
  });
61549
62437
  /**
61550
62438
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61581,12 +62469,18 @@ var motionCapability = {
61581
62469
  methods: {
61582
62470
  /**
61583
62471
  * Pull the current motion state synchronously. Convenience shortcut
61584
- * for consumers that don't need the full status object; equivalent
61585
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61586
- * `getStatus` once the auto-injected status surface lands in every
61587
- * consumer.
62472
+ * for consumers that don't need the full status object. A composed
62473
+ * group answers from its slice (D944): unknown throws, it is not off.
61588
62474
  */
61589
62475
  isDetected: method(object({ deviceId: number() }), boolean()) },
62476
+ /**
62477
+ * A composed motion group serves `isDetected` from `detected` (D944).
62478
+ * Without this the cap stays uncomposable (`has-commands`).
62479
+ */
62480
+ composedMethods: { isDetected: {
62481
+ kind: "slice",
62482
+ field: "detected"
62483
+ } },
61590
62484
  events: {
61591
62485
  /**
61592
62486
  * Fires every time the runner transitions a camera between
@@ -61726,7 +62620,7 @@ var MotionZoneRegionSchema = object({
61726
62620
  * the grid region(s). */
61727
62621
  var MotionZoneStatusSchema = object({
61728
62622
  enabled: boolean(),
61729
- sensitivity: number(),
62623
+ sensitivity: number().meta(readingMeta("none")),
61730
62624
  /** Grid region(s). Today exactly one `grid` shape. */
61731
62625
  regions: array(MotionZoneRegionSchema),
61732
62626
  lastFetchedAt: number()
@@ -62096,7 +62990,7 @@ var NavigationStatusSchema = object({
62096
62990
  following: boolean(),
62097
62991
  flash: boolean(),
62098
62992
  lightMode: NavigationLightModeSchema,
62099
- lightLevel: number().min(40).max(100),
62993
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62100
62994
  /** Ms epoch when the slice was last updated. */
62101
62995
  lastChangedAt: number()
62102
62996
  });
@@ -62230,13 +63124,13 @@ var NetworkLinkStatusSchema = object({
62230
63124
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62231
63125
  * SKIP a null rather than coerce it.
62232
63126
  */
62233
- signalPercent: number().min(0).max(100).nullable(),
63127
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62234
63128
  /** Raw received signal strength in dBm, when the firmware reports one. */
62235
- rssiDbm: number().optional(),
63129
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62236
63130
  /** Network name of a wireless link, when the firmware reports it. */
62237
63131
  ssid: string().optional(),
62238
63132
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62239
- lastUpdated: number()
63133
+ lastUpdated: number().meta(readingMeta("none"))
62240
63134
  });
62241
63135
  var networkLinkCapability = {
62242
63136
  name: "network-link",
@@ -62621,7 +63515,7 @@ var NotifierStatusSchema = object({
62621
63515
  lastError: string().nullable(),
62622
63516
  /** Number of deliveries currently buffered server-side (rate-limited
62623
63517
  * notifiers may queue). 0 when the provider sends synchronously. */
62624
- queueDepth: number().int().nonnegative(),
63518
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62625
63519
  /** Per-feature capability matrix — authoritative source for the
62626
63520
  * compose-form field gating. */
62627
63521
  supports: NotifierSupportsSchema
@@ -62713,11 +63607,9 @@ onSent: { data: object({
62713
63607
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62714
63608
  * for: water flow, distance, weight, frequency, signal strength, …).
62715
63609
  *
62716
- * The `unit` and `precision` fields carry the live display metadata that
62717
- * the upstream source provides with each state push (HA
62718
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62719
- * The UI reads them directly from the slice — they are the single source
62720
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63610
+ * The quantity is not on this slice. It is chosen when the device is
63611
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63612
+ * that quantity's base unit. `null` is a refused sample (D393).
62721
63613
  *
62722
63614
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62723
63615
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62726,15 +63618,8 @@ onSent: { data: object({
62726
63618
  * numeric values land here.
62727
63619
  */
62728
63620
  var NumericSensorStatusSchema = object({
62729
- value: number(),
62730
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62731
- * Populated live from the upstream source on each state push.
62732
- * Absent for unitless sensors. */
62733
- unit: string().optional(),
62734
- /** Suggested decimal places for numeric display.
62735
- * Populated live from the upstream source when provided.
62736
- * Falls back to auto-formatting when absent. */
62737
- precision: number().int().min(0).max(10).optional(),
63621
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63622
+ value: number().nullable().meta(readingMeta("device")),
62738
63623
  /** Ms epoch when the slice was last updated. */
62739
63624
  lastFetchedAt: number()
62740
63625
  });
@@ -62758,11 +63643,11 @@ var numericSensorCapability = {
62758
63643
  */
62759
63644
  durability: "restored",
62760
63645
  /**
62761
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63646
+ * History (D850): recorded series. The unit is the device quantity's base,
63647
+ * resolved when the series is read — the cap has no single unit.
62762
63648
  */
62763
63649
  history: { fields: { value: {
62764
63650
  kind: "gauge",
62765
- unitFrom: "unit",
62766
63651
  minIntervalMs: 3e4
62767
63652
  } } },
62768
63653
  /** Clock fields: written, but excluded from the compare that decides
@@ -63054,23 +63939,26 @@ var PetFeederStatusSchema = object({
63054
63939
  /** Food currently in the bowl (grams). Null when the device has not
63055
63940
  * reported a reading yet. On dual-hopper models this is the combined
63056
63941
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63057
- foodLevel: number().nullable(),
63942
+ foodLevel: number().nullable().meta(readingMeta("none")),
63058
63943
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63059
63944
  * single-hopper models. */
63060
- food1: number().nullable(),
63945
+ food1: number().nullable().meta(readingMeta("none")),
63061
63946
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63062
63947
  * single-hopper models. */
63063
- food2: number().nullable(),
63948
+ food2: number().nullable().meta(readingMeta("none")),
63064
63949
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63065
63950
  * (`device_class: problem`, on = low). True when the bowl is empty /
63066
63951
  * below the feeder's low threshold. */
63067
63952
  lowFood: boolean(),
63068
63953
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63069
63954
  * device has no battery reading. */
63070
- batteryPower: number().min(0).max(100).nullable(),
63071
- /** Days of desiccant life remaining. Null when the model has no
63072
- * desiccant sensor. */
63073
- desiccantLeftDays: number().nullable(),
63955
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63956
+ /**
63957
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63958
+ * Providers convert from the days the feeder reports. Null when the
63959
+ * model has no desiccant sensor.
63960
+ */
63961
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63074
63962
  /** True while a feed is in progress. */
63075
63963
  feeding: boolean(),
63076
63964
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63081,7 +63969,7 @@ var PetFeederStatusSchema = object({
63081
63969
  * with `errorCode` for consumers that want the raw integer. */
63082
63970
  error: string().nullable(),
63083
63971
  /** Raw device error code (0 / null = no error). */
63084
- errorCode: number().nullable(),
63972
+ errorCode: number().nullable().meta(readingMeta("none")),
63085
63973
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63086
63974
  isDualHopper: boolean(),
63087
63975
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63091,7 +63979,7 @@ var PetFeederStatusSchema = object({
63091
63979
  /** Play a chime when dispensing. */
63092
63980
  feedSound: boolean(),
63093
63981
  /** Speaker / prompt volume level (device-scaled integer). */
63094
- volume: number(),
63982
+ volume: number().meta(readingMeta("none")),
63095
63983
  /** Ms epoch when the slice was last refreshed from the cloud. */
63096
63984
  lastFetchedAt: number()
63097
63985
  });
@@ -63210,7 +64098,7 @@ var petFeederCapability = {
63210
64098
  },
63211
64099
  desiccantLeftDays: {
63212
64100
  kind: "gauge",
63213
- unit: "days"
64101
+ unit: "s"
63214
64102
  }
63215
64103
  } }
63216
64104
  };
@@ -63611,43 +64499,88 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63611
64499
  */
63612
64500
  var PowerMeterStatusSchema = object({
63613
64501
  /** Instantaneous power draw in watts. */
63614
- watts: number().optional().meta({
63615
- unit: "W",
63616
- label: "Power"
63617
- }),
64502
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63618
64503
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63619
- kwhTotal: number().optional().meta({
63620
- unit: "kWh",
63621
- label: "Energy"
63622
- }),
64504
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63623
64505
  /** Voltage in volts. */
63624
- volts: number().optional(),
64506
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
63625
64507
  /** Current in amperes. */
63626
- amps: number().optional(),
63627
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63628
- va: number().optional(),
64508
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
64509
+ /** Apparent power in volt-amperes. */
64510
+ va: number().optional().meta(readingMeta("apparent-power")),
63629
64511
  /** Power factor (0..1) if reported. */
63630
- powerFactor: number().min(0).max(1).optional(),
64512
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63631
64513
  /** Ms epoch when the slice was last updated. */
63632
- lastFetchedAt: number(),
63633
- /** Live display unit of the single metric this slice carries (e.g. HA
63634
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63635
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63636
- * unit per slice is unambiguous. The UI prefers this over the role's
63637
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63638
- unit: string().optional(),
63639
- /** Suggested decimal places for numeric display.
63640
- * Populated live from the upstream source when provided (e.g. HA
63641
- * `attributes.suggested_display_precision`). Falls back to
63642
- * auto-formatting when absent. */
63643
- precision: number().int().min(0).max(10).optional()
64514
+ lastFetchedAt: number()
63644
64515
  });
64516
+ /**
64517
+ * Loads that draw metered mains power (D951). The reading is a feature of the
64518
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
64519
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
64520
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
64521
+ * camera and the like are left out: their draw is not what anyone meters.
64522
+ */
64523
+ var POWER_METER_DEVICE_TYPES = [
64524
+ DeviceType.Sensor,
64525
+ DeviceType.Switch,
64526
+ DeviceType.Light,
64527
+ DeviceType.Fan,
64528
+ DeviceType.Climate,
64529
+ DeviceType.Thermostat,
64530
+ DeviceType.Humidifier,
64531
+ DeviceType.WaterHeater,
64532
+ DeviceType.MediaPlayer,
64533
+ DeviceType.Washer,
64534
+ DeviceType.Dryer,
64535
+ DeviceType.Dishwasher,
64536
+ DeviceType.Vacuum,
64537
+ DeviceType.LawnMower,
64538
+ DeviceType.PetFeeder
64539
+ ];
64540
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
64541
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
64542
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
64543
+ function powerTarget(id, unit) {
64544
+ return {
64545
+ id,
64546
+ cardinality: "scalar",
64547
+ vocabulary: {
64548
+ cap: "power-meter",
64549
+ stateField: id
64550
+ },
64551
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
64552
+ apply: "add",
64553
+ path: id,
64554
+ sources: [{
64555
+ kind: "numeric",
64556
+ requiredUnit: unit,
64557
+ transforms: [{ kind: "identity" }]
64558
+ }],
64559
+ companions: [{
64560
+ kind: "clock",
64561
+ path: "lastFetchedAt",
64562
+ when: "always"
64563
+ }],
64564
+ sourceReport: { status: "assignment-status" },
64565
+ advancedExpression: true
64566
+ };
64567
+ }
64568
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
64569
+ var powerMeterCustomize = {
64570
+ version: 1,
64571
+ targets: [
64572
+ powerTarget("watts", "W"),
64573
+ powerTarget("kwhTotal", "kWh"),
64574
+ powerTarget("volts", "V"),
64575
+ powerTarget("amps", "A")
64576
+ ]
64577
+ };
63645
64578
  var powerMeterCapability = {
63646
64579
  name: "power-meter",
63647
64580
  scope: "device",
63648
64581
  deviceNative: true,
63649
64582
  mode: "singleton",
63650
- deviceTypes: [DeviceType.Sensor],
64583
+ deviceTypes: POWER_METER_DEVICE_TYPES,
63651
64584
  methods: {},
63652
64585
  status: {
63653
64586
  schema: PowerMeterStatusSchema,
@@ -63705,11 +64638,7 @@ var powerMeterCapability = {
63705
64638
  /** Clock fields: written, but excluded from the compare that decides
63706
64639
  * whether persisting is worth a SQLite commit. */
63707
64640
  volatileStateFields: ["lastFetchedAt"],
63708
- customize: {
63709
- version: 1,
63710
- advanced: "derive",
63711
- targets: []
63712
- }
64641
+ customize: powerMeterCustomize
63713
64642
  };
63714
64643
  /**
63715
64644
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63729,11 +64658,11 @@ var powerMeterCapability = {
63729
64658
  */
63730
64659
  var GpsLocationSchema = object({
63731
64660
  /** Latitude in decimal degrees, -90..90. */
63732
- latitude: number().min(-90).max(90),
64661
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63733
64662
  /** Longitude in decimal degrees, -180..180. */
63734
- longitude: number().min(-180).max(180),
64663
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63735
64664
  /** Reported accuracy in meters (lower = better). */
63736
- accuracyMeters: number().nonnegative()
64665
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63737
64666
  });
63738
64667
  var PresenceStatusSchema = object({
63739
64668
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63745,7 +64674,7 @@ var PresenceStatusSchema = object({
63745
64674
  gps: GpsLocationSchema.nullable(),
63746
64675
  /** Optional battery level of the tracking device (0..100). Null
63747
64676
  * when not reported. */
63748
- batteryPercent: number().min(0).max(100).nullable(),
64677
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63749
64678
  /** Ms epoch when the slice was last updated. */
63750
64679
  lastChangedAt: number()
63751
64680
  });
@@ -63795,19 +64724,10 @@ var presenceCapability = {
63795
64724
  * `sourceInfo`.
63796
64725
  */
63797
64726
  var PressureSensorStatusSchema = object({
63798
- /** Current pressure in hPa. */
63799
- hpa: number(),
64727
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64728
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63800
64729
  /** Ms epoch when the slice was last updated. */
63801
- lastFetchedAt: number(),
63802
- /** Live display unit from the upstream source (e.g. HA
63803
- * `attributes.unit_of_measurement`). The UI prefers this over the
63804
- * role's canonical unit. Absent → fall back to the canonical unit. */
63805
- unit: string().optional(),
63806
- /** Suggested decimal places for numeric display.
63807
- * Populated live from the upstream source when provided (e.g. HA
63808
- * `attributes.suggested_display_precision`). Falls back to
63809
- * auto-formatting when absent. */
63810
- precision: number().int().min(0).max(10).optional()
64730
+ lastFetchedAt: number()
63811
64731
  });
63812
64732
  var pressureSensorCapability = {
63813
64733
  name: "pressure-sensor",
@@ -64221,8 +65141,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64221
65141
  }), _void(), { kind: "mutation" });
64222
65142
  var PtzAutotrackSettingsSchema = object({
64223
65143
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64224
- stopDelaySeconds: number().int().min(0).max(300),
64225
- disappearDelaySeconds: number().int().min(0).max(300)
65144
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65145
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64226
65146
  });
64227
65147
  /**
64228
65148
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64376,7 +65296,7 @@ var RunningStateStatusSchema = object({
64376
65296
  /** null = unknown — never coerced to false (D393). */
64377
65297
  running: boolean().nullable(),
64378
65298
  /** Epoch ms the current boolean began; null while `running` is null. */
64379
- since: number().nullable(),
65299
+ since: number().nullable().meta(readingMeta("none")),
64380
65300
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64381
65301
  phase: string().nullable(),
64382
65302
  /** null until a reading has been published (D393). */
@@ -64391,7 +65311,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64391
65311
  /** Generic Customize target for the running boolean (D936). */
64392
65312
  var runningCustomize = {
64393
65313
  version: 1,
64394
- advanced: "derive",
64395
65314
  targets: [{
64396
65315
  id: "running",
64397
65316
  cardinality: "scalar",
@@ -64400,6 +65319,8 @@ var runningCustomize = {
64400
65319
  stateField: "running"
64401
65320
  },
64402
65321
  deviceTypes: [
65322
+ DeviceType.Washer,
65323
+ DeviceType.Dryer,
64403
65324
  DeviceType.Dishwasher,
64404
65325
  DeviceType.Vacuum,
64405
65326
  DeviceType.LawnMower,
@@ -64411,7 +65332,7 @@ var runningCustomize = {
64411
65332
  DeviceType.Thermostat,
64412
65333
  DeviceType.PetFeeder
64413
65334
  ],
64414
- apply: "add-or-replace",
65335
+ apply: "add",
64415
65336
  path: "running",
64416
65337
  sources: [
64417
65338
  {
@@ -65675,9 +66596,9 @@ var RecordTriggerSchema = _enum([
65675
66596
  */
65676
66597
  var RecordWindowSchema = object({
65677
66598
  trigger: RecordTriggerSchema,
65678
- day: number().int().min(0).max(6),
65679
- startMinute: number().int().min(0).max(1439),
65680
- endMinute: number().int().min(1).max(1440)
66599
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66600
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66601
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65681
66602
  });
65682
66603
  /** Status of one physical volume, as the camera itself describes it. */
65683
66604
  var OnboardStorageVolumeSchema = object({
@@ -65753,10 +66674,10 @@ var RecordingOnboardStatusSchema = object({
65753
66674
  /** Master "record to the card at all" switch. */
65754
66675
  enabled: boolean().nullable(),
65755
66676
  overwriteWhenFull: boolean().nullable(),
65756
- preRecordSec: number().nullable(),
65757
- postRecordSec: number().nullable(),
66677
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66678
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65758
66679
  /** Length of one recorded file, in minutes. */
65759
- segmentMinutes: number().nullable(),
66680
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65760
66681
  /** The primary track's weekly windows, flattened. */
65761
66682
  windows: array(RecordWindowSchema),
65762
66683
  /**
@@ -65772,7 +66693,7 @@ var RecordingOnboardStatusSchema = object({
65772
66693
  * provider reporting a non-zero count also reports the schedule as not
65773
66694
  * writable.
65774
66695
  */
65775
- unreadableWindows: number(),
66696
+ unreadableWindows: number().meta(readingMeta("count")),
65776
66697
  /**
65777
66698
  * The camera is scheduled to record and has NO usable storage.
65778
66699
  *
@@ -66617,7 +67538,7 @@ var CamStreamDescriptorSchema = object({
66617
67538
  url: string().optional(),
66618
67539
  codec: string().optional(),
66619
67540
  resolution: CamStreamResolutionSchema.optional(),
66620
- fps: number().positive().optional(),
67541
+ fps: number().positive().optional().meta(readingMeta("rate")),
66621
67542
  label: string().optional(),
66622
67543
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66623
67544
  deviceFeatures: array(string()).optional(),
@@ -66704,18 +67625,18 @@ var StreamProfileSchema = _enum([
66704
67625
  ]);
66705
67626
  /** The current encoding config of one stream profile. */
66706
67627
  var StreamProfileConfigSchema = object({
66707
- width: number(),
66708
- height: number(),
67628
+ width: number().meta(readingMeta("none")),
67629
+ height: number().meta(readingMeta("none")),
66709
67630
  codec: _enum(["h264", "h265"]),
66710
- framerate: number(),
66711
- bitrate: number(),
67631
+ framerate: number().meta(readingMeta("rate")),
67632
+ bitrate: number().meta(readingMeta("bitrate")),
66712
67633
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66713
67634
  encoderProfile: _enum([
66714
67635
  "high",
66715
67636
  "main",
66716
67637
  "baseline"
66717
67638
  ]).optional(),
66718
- gop: number().optional(),
67639
+ gop: number().optional().meta(readingMeta("none")),
66719
67640
  /**
66720
67641
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66721
67642
  *
@@ -66889,7 +67810,8 @@ var switchCapability = {
66889
67810
  deviceTypes: [
66890
67811
  DeviceType.Switch,
66891
67812
  DeviceType.Siren,
66892
- DeviceType.Light
67813
+ DeviceType.Light,
67814
+ DeviceType.Fan
66893
67815
  ],
66894
67816
  methods: { setState: method(object({
66895
67817
  deviceId: number(),
@@ -66936,12 +67858,7 @@ var switchCapability = {
66936
67858
  label: "On",
66937
67859
  kind: "boolean"
66938
67860
  }
66939
- }] },
66940
- customize: {
66941
- version: 1,
66942
- advanced: "derive",
66943
- targets: []
66944
- }
67861
+ }] }
66945
67862
  };
66946
67863
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66947
67864
  function hasControlCharacter(value) {
@@ -67524,35 +68441,21 @@ var tamperCapability = {
67524
68441
  * entries with `device_class: temperature` and any future native
67525
68442
  * thermometer.
67526
68443
  *
67527
- * Unit normalisation: providers convert to Celsius before storing.
67528
- * The slice value is always Celsius so cross-cap aggregators
67529
- * (climate-control's `currentTemp`, energy analytics) can compose
67530
- * without per-source unit fixups. The canonical display unit (`°C`) is
67531
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67532
- * `sourceInfo`.
68444
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68445
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67533
68446
  *
67534
68447
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67535
68448
  * frozen feed (provider hung) distinct from a "temperature hasn't
67536
68449
  * changed" steady state.
67537
68450
  */
67538
68451
  var TemperatureSensorStatusSchema = object({
67539
- /** Current temperature in Celsius. */
67540
- celsius: number().meta({
67541
- unit: "°C",
68452
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68453
+ celsius: number().nullable().meta(readingMeta("temperature", {
68454
+ precision: 1,
67542
68455
  label: "Temperature"
67543
- }),
68456
+ })),
67544
68457
  /** Ms epoch when the slice was last updated (push or poll). */
67545
- lastFetchedAt: number(),
67546
- /** Live display unit from the upstream source (e.g. HA
67547
- * `attributes.unit_of_measurement`). The UI prefers this over the
67548
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67549
- * Absent → the UI falls back to the role's canonical unit. */
67550
- unit: string().optional(),
67551
- /** Suggested decimal places for numeric display.
67552
- * Populated live from the upstream source when provided (e.g. HA
67553
- * `attributes.suggested_display_precision`). Falls back to
67554
- * auto-formatting when absent. */
67555
- precision: number().int().min(0).max(10).optional()
68458
+ lastFetchedAt: number()
67556
68459
  });
67557
68460
  var temperatureSensorCapability = {
67558
68461
  name: "temperature-sensor",
@@ -67583,12 +68486,7 @@ var temperatureSensorCapability = {
67583
68486
  } } },
67584
68487
  /** Clock fields: written, but excluded from the compare that decides
67585
68488
  * whether persisting is worth a SQLite commit. */
67586
- volatileStateFields: ["lastFetchedAt"],
67587
- customize: {
67588
- version: 1,
67589
- advanced: "derive",
67590
- targets: []
67591
- }
68489
+ volatileStateFields: ["lastFetchedAt"]
67592
68490
  };
67593
68491
  /**
67594
68492
  * toast — system-scoped singleton capability that streams toast
@@ -67871,16 +68769,16 @@ var VacuumControlStatusSchema = object({
67871
68769
  ]),
67872
68770
  /** 0..100 battery percentage. Null when the device has no battery
67873
68771
  * reading. */
67874
- batteryLevel: number().min(0).max(100).nullable(),
68772
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67875
68773
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67876
68774
  * the vacuum has no speed control. */
67877
68775
  fanSpeed: string().nullable(),
67878
68776
  /** Speed tokens the hardware accepts — drives the UI selector. */
67879
68777
  availableFanSpeeds: array(string()),
67880
68778
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67881
- progressPercent: number().min(0).max(100).nullable(),
68779
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67882
68780
  /** Current error code (0 / null = no error). */
67883
- errorCode: number().nullable(),
68781
+ errorCode: number().nullable().meta(readingMeta("none")),
67884
68782
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67885
68783
  errorLabel: string().nullable(),
67886
68784
  /** Ms epoch when the slice was last updated. */
@@ -67958,7 +68856,7 @@ var ValveStatusSchema = object({
67958
68856
  ]),
67959
68857
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67960
68858
  * intermediate position surface. */
67961
- position: number().min(0).max(100).nullable(),
68859
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67962
68860
  /** Ms epoch when the slice was last updated. */
67963
68861
  lastChangedAt: number()
67964
68862
  });
@@ -68066,9 +68964,9 @@ var vibrationCapability = {
68066
68964
  */
68067
68965
  var WaterHeaterStatusSchema = object({
68068
68966
  /** Current measured temperature. Null when not reported. */
68069
- currentTemp: number().nullable(),
68967
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68070
68968
  /** Target temperature setpoint. Null when no setpoint surface. */
68071
- targetTemp: number().nullable(),
68969
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68072
68970
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68073
68971
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68074
68972
  * device reports an unknown state. */
@@ -68079,9 +68977,9 @@ var WaterHeaterStatusSchema = object({
68079
68977
  * has no away surface. */
68080
68978
  away: boolean().nullable(),
68081
68979
  /** HA `min_temp` attribute. Null when not reported. */
68082
- minTemp: number().nullable(),
68980
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68083
68981
  /** HA `max_temp` attribute. Null when not reported. */
68084
- maxTemp: number().nullable(),
68982
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68085
68983
  /** Ms epoch when the slice was last updated. */
68086
68984
  lastChangedAt: number()
68087
68985
  });
@@ -68166,25 +69064,18 @@ var waterHeaterCapability = {
68166
69064
  * Forecast deferred — current conditions only for v1.
68167
69065
  */
68168
69066
  var WeatherStatusSchema = object({
68169
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68170
- * when no condition has been reported yet. */
69067
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68171
69068
  condition: string().nullable(),
68172
- /** Current temperature in the reported unit. Null when not provided. */
68173
- temperature: number().nullable(),
68174
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68175
- temperatureUnit: string().nullable(),
69069
+ /** Current temperature in Celsius. Null when not provided or refused. */
69070
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68176
69071
  /** Relative humidity (0..100). Null when not provided. */
68177
- humidity: number().min(0).max(100).nullable(),
68178
- /** Barometric pressure in the reported unit. Null when not provided. */
68179
- pressure: number().nullable(),
68180
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68181
- pressureUnit: string().nullable(),
68182
- /** Wind speed in the reported unit. Null when not provided. */
68183
- windSpeed: number().nullable(),
68184
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68185
- windSpeedUnit: string().nullable(),
69072
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
69073
+ /** Barometric pressure in hPa. Null when not provided or refused. */
69074
+ pressure: number().nullable().meta(readingMeta("pressure")),
69075
+ /** Wind speed in metres per second. Null when not provided or refused. */
69076
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68186
69077
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68187
- windBearing: number().nullable(),
69078
+ windBearing: number().nullable().meta(readingMeta("angle")),
68188
69079
  /** Ms epoch when the slice was last updated. */
68189
69080
  lastFetchedAt: number()
68190
69081
  });
@@ -68212,12 +69103,12 @@ var weatherCapability = {
68212
69103
  */
68213
69104
  durability: "session",
68214
69105
  /**
68215
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69106
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68216
69107
  */
68217
69108
  history: { fields: {
68218
69109
  temperature: {
68219
69110
  kind: "gauge",
68220
- unitFrom: "temperatureUnit",
69111
+ unit: "°C",
68221
69112
  deadband: .2
68222
69113
  },
68223
69114
  humidity: {
@@ -68227,12 +69118,12 @@ var weatherCapability = {
68227
69118
  },
68228
69119
  pressure: {
68229
69120
  kind: "gauge",
68230
- unitFrom: "pressureUnit",
69121
+ unit: "hPa",
68231
69122
  deadband: .5
68232
69123
  },
68233
69124
  windSpeed: {
68234
69125
  kind: "gauge",
68235
- unitFrom: "windSpeedUnit",
69126
+ unit: "m/s",
68236
69127
  deadband: .5
68237
69128
  }
68238
69129
  } }
@@ -69133,8 +70024,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
69133
70024
  kind: "mutation",
69134
70025
  auth: "admin"
69135
70026
  });
70027
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
69136
70028
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69137
- 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;
70029
+ 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;
69138
70030
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
69139
70031
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
69140
70032
  new Set(CAP_NAMES);
@@ -69463,6 +70355,9 @@ var DEVICE_LOCAL_STATE_CAPS = {
69463
70355
  featureProbe: featureProbeCapability,
69464
70356
  flood: floodCapability,
69465
70357
  gas: gasCapability,
70358
+ groupActive: groupActiveCapability,
70359
+ groupCommands: groupCommandsCapability,
70360
+ groupStatus: groupStatusCapability,
69466
70361
  heatingValve: heatingValveCapability,
69467
70362
  humidifier: humidifierCapability,
69468
70363
  humiditySensor: humiditySensorCapability,
@@ -69472,6 +70367,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
69472
70367
  lawnMowerControl: lawnMowerControlCapability,
69473
70368
  lockControl: lockControlCapability,
69474
70369
  mediaPlayer: mediaPlayerCapability,
70370
+ meter: meterCapability,
69475
70371
  motion: motionCapability,
69476
70372
  motionTrigger: motionTriggerCapability,
69477
70373
  motionZones: motionZonesCapability,
@@ -70731,7 +71627,7 @@ var BaseDeviceProvider = class extends BaseAddon {
70731
71627
  return toDeviceSummary(device, this.addonId);
70732
71628
  }
70733
71629
  };
70734
- 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;
71630
+ 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;
70735
71631
  new Set(Object.values(DeviceType));
70736
71632
  Object.freeze({
70737
71633
  "accessories.getStatus": {
@@ -70839,6 +71735,13 @@ Object.freeze({
70839
71735
  access: "view",
70840
71736
  role: "admin"
70841
71737
  },
71738
+ "addons.getPackageDocs": {
71739
+ capName: "addons",
71740
+ capScope: "system",
71741
+ addonId: null,
71742
+ access: "view",
71743
+ role: "admin"
71744
+ },
70842
71745
  "addons.getVersions": {
70843
71746
  capName: "addons",
70844
71747
  capScope: "system",
@@ -73947,6 +74850,27 @@ Object.freeze({
73947
74850
  access: "view",
73948
74851
  role: "viewer"
73949
74852
  },
74853
+ "groupActive.getStatus": {
74854
+ capName: "group-active",
74855
+ capScope: "device",
74856
+ addonId: null,
74857
+ access: "view",
74858
+ role: "viewer"
74859
+ },
74860
+ "groupCommands.getStatus": {
74861
+ capName: "group-commands",
74862
+ capScope: "device",
74863
+ addonId: null,
74864
+ access: "view",
74865
+ role: "viewer"
74866
+ },
74867
+ "groupStatus.getStatus": {
74868
+ capName: "group-status",
74869
+ capScope: "device",
74870
+ addonId: null,
74871
+ access: "view",
74872
+ role: "viewer"
74873
+ },
73950
74874
  "heatingValve.getStatus": {
73951
74875
  capName: "heating-valve",
73952
74876
  capScope: "device",
@@ -74899,6 +75823,13 @@ Object.freeze({
74899
75823
  access: "create",
74900
75824
  role: "admin"
74901
75825
  },
75826
+ "meter.getStatus": {
75827
+ capName: "meter",
75828
+ capScope: "device",
75829
+ addonId: null,
75830
+ access: "view",
75831
+ role: "viewer"
75832
+ },
74902
75833
  "metricsProvider.collectSnapshot": {
74903
75834
  capName: "metrics-provider",
74904
75835
  capScope: "system",
@@ -82997,7 +83928,39 @@ var APPLIANCE_CAP_VOCABULARY = {
82997
83928
  phrase: t("When {device} finishes", "Quando {device} finisce"),
82998
83929
  icon: "square"
82999
83930
  }
83000
- }
83931
+ },
83932
+ overrides: { byType: {
83933
+ [DeviceType.Washer]: {
83934
+ state: { running: {
83935
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
83936
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
83937
+ } },
83938
+ events: {
83939
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
83940
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
83941
+ }
83942
+ },
83943
+ [DeviceType.Dryer]: {
83944
+ state: { running: {
83945
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
83946
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
83947
+ } },
83948
+ events: {
83949
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
83950
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
83951
+ }
83952
+ },
83953
+ [DeviceType.Dishwasher]: {
83954
+ state: { running: {
83955
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
83956
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
83957
+ } },
83958
+ events: {
83959
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
83960
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
83961
+ }
83962
+ }
83963
+ } }
83001
83964
  },
83002
83965
  update: {
83003
83966
  scope: "device",
@@ -83769,6 +84732,41 @@ var SENSOR_CAP_VOCABULARY = {
83769
84732
  icon: "flame"
83770
84733
  }]
83771
84734
  },
84735
+ "group-active": {
84736
+ scope: "device",
84737
+ noun: t("Group state", "Stato del gruppo")
84738
+ },
84739
+ "group-commands": {
84740
+ scope: "device",
84741
+ noun: t("Group command", "Comando di gruppo")
84742
+ },
84743
+ "group-status": {
84744
+ scope: "device",
84745
+ noun: t("Group", "Gruppo"),
84746
+ state: [
84747
+ {
84748
+ kind: "numeric",
84749
+ field: "included",
84750
+ quantity: t("the members reporting", "i membri che rispondono"),
84751
+ label: t("Reporting", "Rispondono"),
84752
+ icon: "users"
84753
+ },
84754
+ {
84755
+ kind: "numeric",
84756
+ field: "excluded",
84757
+ quantity: t("the members left out", "i membri esclusi"),
84758
+ label: t("Excluded", "Esclusi"),
84759
+ icon: "users"
84760
+ },
84761
+ {
84762
+ kind: "numeric",
84763
+ field: "total",
84764
+ quantity: t("the member count", "il numero di membri"),
84765
+ label: t("Members", "Membri"),
84766
+ icon: "sigma"
84767
+ }
84768
+ ]
84769
+ },
83772
84770
  "humidity-sensor": {
83773
84771
  scope: "device",
83774
84772
  noun: t("Humidity", "Umidità"),
@@ -83794,6 +84792,17 @@ var SENSOR_CAP_VOCABULARY = {
83794
84792
  }],
83795
84793
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
83796
84794
  },
84795
+ meter: {
84796
+ scope: "device",
84797
+ noun: t("Meter", "Contatore"),
84798
+ state: [{
84799
+ kind: "numeric",
84800
+ field: "value",
84801
+ quantity: t("the total", "il totale"),
84802
+ label: t("Total", "Totale"),
84803
+ icon: "gauge"
84804
+ }]
84805
+ },
83797
84806
  "numeric-sensor": {
83798
84807
  scope: "device",
83799
84808
  noun: t("Sensor", "Sensore"),
@@ -83802,7 +84811,6 @@ var SENSOR_CAP_VOCABULARY = {
83802
84811
  field: "value",
83803
84812
  quantity: t("the reading", "il valore"),
83804
84813
  label: t("Reading", "Valore"),
83805
- unitField: "unit",
83806
84814
  icon: "gauge"
83807
84815
  }]
83808
84816
  },
@@ -83938,7 +84946,7 @@ var SENSOR_CAP_VOCABULARY = {
83938
84946
  field: "temperature",
83939
84947
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83940
84948
  label: t("Temperature", "Temperatura"),
83941
- unitField: "temperatureUnit",
84949
+ unit: "°C",
83942
84950
  icon: "thermometer"
83943
84951
  },
83944
84952
  {
@@ -83954,7 +84962,7 @@ var SENSOR_CAP_VOCABULARY = {
83954
84962
  field: "windSpeed",
83955
84963
  quantity: t("the wind speed", "la velocità del vento"),
83956
84964
  label: t("Wind", "Vento"),
83957
- unitField: "windSpeedUnit",
84965
+ unit: "m/s",
83958
84966
  icon: "wind"
83959
84967
  },
83960
84968
  {
@@ -83962,7 +84970,7 @@ var SENSOR_CAP_VOCABULARY = {
83962
84970
  field: "pressure",
83963
84971
  quantity: t("the air pressure", "la pressione atmosferica"),
83964
84972
  label: t("Pressure", "Pressione"),
83965
- unitField: "pressureUnit",
84973
+ unit: "hPa",
83966
84974
  icon: "gauge"
83967
84975
  }
83968
84976
  ]
@@ -84267,8 +85275,47 @@ function projectAction(cap, action) {
84267
85275
  };
84268
85276
  }
84269
85277
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
84270
- 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;
85278
+ 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;
84271
85279
  DeviceFeature.BatteryOperated;
85280
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
85281
+ 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.";
85282
+ 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.";
85283
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
85284
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
85285
+ 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.";
85286
+ function withheld(reason) {
85287
+ return {
85288
+ [DeviceType.Washer]: {
85289
+ exported: false,
85290
+ reason
85291
+ },
85292
+ [DeviceType.Dryer]: {
85293
+ exported: false,
85294
+ reason
85295
+ },
85296
+ [DeviceType.Dishwasher]: {
85297
+ exported: false,
85298
+ reason
85299
+ }
85300
+ };
85301
+ }
85302
+ function offered(received) {
85303
+ return {
85304
+ [DeviceType.Washer]: {
85305
+ exported: true,
85306
+ as: received
85307
+ },
85308
+ [DeviceType.Dryer]: {
85309
+ exported: true,
85310
+ as: received
85311
+ },
85312
+ [DeviceType.Dishwasher]: {
85313
+ exported: true,
85314
+ as: received
85315
+ }
85316
+ };
85317
+ }
85318
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
84272
85319
  /** Reject after `ms`; always clears its own timer. */
84273
85320
  async function withTimeout$1(promise, ms, label) {
84274
85321
  let timer;
@@ -85301,6 +86348,21 @@ Object.freeze({
85301
86348
  form: "single",
85302
86349
  optional: false
85303
86350
  }],
86351
+ "groupActive.getStatus": [{
86352
+ name: "deviceId",
86353
+ form: "single",
86354
+ optional: false
86355
+ }],
86356
+ "groupCommands.getStatus": [{
86357
+ name: "deviceId",
86358
+ form: "single",
86359
+ optional: false
86360
+ }],
86361
+ "groupStatus.getStatus": [{
86362
+ name: "deviceId",
86363
+ form: "single",
86364
+ optional: false
86365
+ }],
85304
86366
  "heatingValve.getStatus": [{
85305
86367
  name: "deviceId",
85306
86368
  form: "single",
@@ -85501,6 +86563,11 @@ Object.freeze({
85501
86563
  form: "single",
85502
86564
  optional: false
85503
86565
  }],
86566
+ "meter.getStatus": [{
86567
+ name: "deviceId",
86568
+ form: "single",
86569
+ optional: false
86570
+ }],
85504
86571
  "motion.getStatus": [{
85505
86572
  name: "deviceId",
85506
86573
  form: "single",
@@ -87108,7 +88175,6 @@ Object.freeze({
87108
88175
  "network-access": "ingress",
87109
88176
  "smtp-provider": "email"
87110
88177
  });
87111
- DeviceType.Camera;
87112
88178
  /**
87113
88179
  * A saved Customize assignment (D936). The schema is the type: callers use
87114
88180
  * `z.infer`, they do not restate the shape.
@@ -87285,6 +88351,8 @@ new Map(Object.entries({
87285
88351
  "detection-blind": "device-events",
87286
88352
  "motion-noisy": "device-events",
87287
88353
  "motion-calm": "device-events",
88354
+ "package-llm-silent": "device-events",
88355
+ "package-llm-answered": "device-events",
87288
88356
  "recording-stalled": "device-events",
87289
88357
  "recording-healthy": "device-events",
87290
88358
  "recording-degraded": "device-events",
@@ -87368,6 +88436,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87368
88436
  label: "Motion without a subject",
87369
88437
  kinds: ["motion-noisy", "motion-calm"]
87370
88438
  },
88439
+ {
88440
+ id: "package-llm",
88441
+ family: "device-events",
88442
+ label: "Package confirm",
88443
+ kinds: ["package-llm-silent", "package-llm-answered"]
88444
+ },
87371
88445
  {
87372
88446
  id: "recording",
87373
88447
  family: "device-events",
@@ -87549,6 +88623,8 @@ var DEVICE_EVENTS = [
87549
88623
  "detection-blind",
87550
88624
  "motion-noisy",
87551
88625
  "motion-calm",
88626
+ "package-llm-silent",
88627
+ "package-llm-answered",
87552
88628
  "recording-stalled",
87553
88629
  "recording-healthy",
87554
88630
  "recording-degraded",
@@ -87817,6 +88893,15 @@ new Map([
87817
88893
  snapshotLabel: "Cameras with constant motion now",
87818
88894
  snapshotSection: null
87819
88895
  },
88896
+ {
88897
+ id: "package-llm-confirm",
88898
+ family: "device-events",
88899
+ label: "Package confirm (not answering / answering again)",
88900
+ bad: ["package-llm-silent"],
88901
+ recovery: ["package-llm-answered"],
88902
+ snapshotLabel: "Cameras whose package confirm is not answering",
88903
+ snapshotSection: null
88904
+ },
87820
88905
  {
87821
88906
  id: "recording-health",
87822
88907
  family: "device-events",