@camstack/addon-provider-reolink 1.2.210 → 1.2.212

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -36,7 +36,7 @@ let node_os = require("node:os");
36
36
  node_os = require_chunk.__toESM(node_os, 1);
37
37
  let node_fs = require("node:fs");
38
38
  node_fs = require_chunk.__toESM(node_fs, 1);
39
- //#region ../types/dist/event-category-DHXxB8t9.mjs
39
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
40
40
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
41
41
  EventCategory["SystemBoot"] = "system.boot";
42
42
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -362,6 +362,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
362
362
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
363
363
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
364
364
  /**
365
+ * A camera's package LLM confirm has failed closed three times in a row
366
+ * (D931, 2026-10-26). One edge per camera until
367
+ * {@link PipelinePackageLlmAnswered}.
368
+ */
369
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
370
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
371
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
372
+ /**
365
373
  * Per-camera pipeline config was mutated by the orchestrator
366
374
  * (a settings change via `setAgentInferenceDevices` /
367
375
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5471,7 +5479,7 @@ var ZodIssueCode = {
5471
5479
  var ZodFirstPartyTypeKind;
5472
5480
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5473
5481
  //#endregion
5474
- //#region ../types/dist/sleep-BErAxhul.mjs
5482
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5475
5483
  /**
5476
5484
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5477
5485
  * window to float samples (D455).
@@ -8029,6 +8037,99 @@ var RecordKindDeclarationSchema = object({
8029
8037
  }).strict();
8030
8038
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8031
8039
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8040
+ var READING_QUANTITIES = [
8041
+ "temperature",
8042
+ "pressure",
8043
+ "speed",
8044
+ "precipitation",
8045
+ "precipitation-rate",
8046
+ "length",
8047
+ "area",
8048
+ "illuminance",
8049
+ "irradiance",
8050
+ "power",
8051
+ "energy",
8052
+ "voltage",
8053
+ "current",
8054
+ "apparent-power",
8055
+ "humidity",
8056
+ "percentage",
8057
+ "concentration-volume",
8058
+ "concentration-mass",
8059
+ "concentration-ppb",
8060
+ "volume",
8061
+ "duration",
8062
+ "duration-ms",
8063
+ "information",
8064
+ "sound-level",
8065
+ "signal-level",
8066
+ "colour-temperature",
8067
+ "angle",
8068
+ "bitrate",
8069
+ "rate",
8070
+ "mass",
8071
+ "index",
8072
+ "ratio",
8073
+ "count",
8074
+ "none",
8075
+ "device"
8076
+ ];
8077
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8078
+ new Set(READING_QUANTITIES);
8079
+ /**
8080
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8081
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8082
+ */
8083
+ var QUANTITY_BASE = {
8084
+ temperature: "°C",
8085
+ pressure: "hPa",
8086
+ speed: "m/s",
8087
+ precipitation: "mm",
8088
+ "precipitation-rate": "mm/h",
8089
+ length: "m",
8090
+ area: "m²",
8091
+ illuminance: "lx",
8092
+ irradiance: "W/m²",
8093
+ power: "W",
8094
+ energy: "kWh",
8095
+ voltage: "V",
8096
+ current: "A",
8097
+ "apparent-power": "VA",
8098
+ humidity: "%",
8099
+ percentage: "%",
8100
+ "concentration-volume": "ppm",
8101
+ "concentration-mass": "µg/m³",
8102
+ "concentration-ppb": "ppb",
8103
+ volume: "L",
8104
+ duration: "s",
8105
+ "duration-ms": "ms",
8106
+ information: "B",
8107
+ "sound-level": "dBFS",
8108
+ "signal-level": "dBm",
8109
+ "colour-temperature": "mired",
8110
+ angle: "°",
8111
+ bitrate: "kbps",
8112
+ rate: "fps",
8113
+ mass: "kg",
8114
+ index: "none",
8115
+ ratio: "none",
8116
+ count: "none",
8117
+ none: "none",
8118
+ device: "device"
8119
+ };
8120
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8121
+ function baseUnitOf(quantity) {
8122
+ const symbol = QUANTITY_BASE[quantity];
8123
+ if (symbol === "none" || symbol === "device") return void 0;
8124
+ return symbol;
8125
+ }
8126
+ function readingMeta(quantity, extras) {
8127
+ return {
8128
+ quantity,
8129
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8130
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8131
+ };
8132
+ }
8032
8133
  /** Shared Zod schemas used across streaming capabilities. */
8033
8134
  var CamProfileSchema = _enum([
8034
8135
  "high",
@@ -8056,8 +8157,8 @@ var CamStreamKindSchema = _enum([
8056
8157
  "derived"
8057
8158
  ]);
8058
8159
  var CamStreamResolutionSchema = object({
8059
- width: number().int().positive(),
8060
- height: number().int().positive()
8160
+ width: number().int().positive().meta(readingMeta("none")),
8161
+ height: number().int().positive().meta(readingMeta("none"))
8061
8162
  });
8062
8163
  var CameraStreamSchema = object({
8063
8164
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8609,6 +8710,9 @@ var CAP_NAMES = Object.freeze([
8609
8710
  "filesystem-browse",
8610
8711
  "flood",
8611
8712
  "gas",
8713
+ "group-active",
8714
+ "group-commands",
8715
+ "group-status",
8612
8716
  "heating-valve",
8613
8717
  "humidifier",
8614
8718
  "humidity-sensor",
@@ -8629,6 +8733,7 @@ var CAP_NAMES = Object.freeze([
8629
8733
  "login-method",
8630
8734
  "media-player",
8631
8735
  "mesh-network",
8736
+ "meter",
8632
8737
  "metrics-provider",
8633
8738
  "model-convert",
8634
8739
  "model-distributor",
@@ -8738,14 +8843,6 @@ Object.freeze({
8738
8843
  {
8739
8844
  "path": "aqi",
8740
8845
  "valueType": "number"
8741
- },
8742
- {
8743
- "path": "unit",
8744
- "valueType": "string"
8745
- },
8746
- {
8747
- "path": "precision",
8748
- "valueType": "number"
8749
8846
  }
8750
8847
  ],
8751
8848
  "alarm-panel": [
@@ -8781,20 +8878,10 @@ Object.freeze({
8781
8878
  "valueType": "boolean"
8782
8879
  }
8783
8880
  ],
8784
- "ambient-light-sensor": [
8785
- {
8786
- "path": "lux",
8787
- "valueType": "number"
8788
- },
8789
- {
8790
- "path": "unit",
8791
- "valueType": "string"
8792
- },
8793
- {
8794
- "path": "precision",
8795
- "valueType": "number"
8796
- }
8797
- ],
8881
+ "ambient-light-sensor": [{
8882
+ "path": "lux",
8883
+ "valueType": "number"
8884
+ }],
8798
8885
  "audio-metrics": [
8799
8886
  {
8800
8887
  "path": "ts",
@@ -8874,6 +8961,10 @@ Object.freeze({
8874
8961
  "path": "lastUpdated",
8875
8962
  "valueType": "number"
8876
8963
  },
8964
+ {
8965
+ "path": "volts",
8966
+ "valueType": "number"
8967
+ },
8877
8968
  {
8878
8969
  "path": "lastContactAt",
8879
8970
  "valueType": "number"
@@ -9116,10 +9207,6 @@ Object.freeze({
9116
9207
  "datetime"
9117
9208
  ]
9118
9209
  },
9119
- {
9120
- "path": "unit",
9121
- "valueType": "string"
9122
- },
9123
9210
  {
9124
9211
  "path": "min",
9125
9212
  "valueType": "number"
@@ -9131,19 +9218,6 @@ Object.freeze({
9131
9218
  {
9132
9219
  "path": "step",
9133
9220
  "valueType": "number"
9134
- },
9135
- {
9136
- "path": "precision",
9137
- "valueType": "number"
9138
- },
9139
- {
9140
- "path": "format",
9141
- "valueType": "enum",
9142
- "enumValues": [
9143
- "date",
9144
- "time",
9145
- "datetime"
9146
- ]
9147
9221
  }
9148
9222
  ],
9149
9223
  "cover": [
@@ -9384,13 +9458,8 @@ Object.freeze({
9384
9458
  "path": "value",
9385
9459
  "valueType": "string"
9386
9460
  }, {
9387
- "path": "format",
9388
- "valueType": "enum",
9389
- "enumValues": [
9390
- "date",
9391
- "time",
9392
- "datetime"
9393
- ]
9461
+ "path": "timestamp",
9462
+ "valueType": "boolean"
9394
9463
  }],
9395
9464
  "event-emitter": [
9396
9465
  {
@@ -9463,6 +9532,24 @@ Object.freeze({
9463
9532
  "path": "detected",
9464
9533
  "valueType": "boolean"
9465
9534
  }],
9535
+ "group-active": [{
9536
+ "path": "active",
9537
+ "valueType": "boolean"
9538
+ }],
9539
+ "group-status": [
9540
+ {
9541
+ "path": "included",
9542
+ "valueType": "number"
9543
+ },
9544
+ {
9545
+ "path": "excluded",
9546
+ "valueType": "number"
9547
+ },
9548
+ {
9549
+ "path": "total",
9550
+ "valueType": "number"
9551
+ }
9552
+ ],
9466
9553
  "heating-valve": [{
9467
9554
  "path": "position",
9468
9555
  "valueType": "number"
@@ -9497,20 +9584,10 @@ Object.freeze({
9497
9584
  "valueType": "number"
9498
9585
  }
9499
9586
  ],
9500
- "humidity-sensor": [
9501
- {
9502
- "path": "percent",
9503
- "valueType": "number"
9504
- },
9505
- {
9506
- "path": "unit",
9507
- "valueType": "string"
9508
- },
9509
- {
9510
- "path": "precision",
9511
- "valueType": "number"
9512
- }
9513
- ],
9587
+ "humidity-sensor": [{
9588
+ "path": "percent",
9589
+ "valueType": "number"
9590
+ }],
9514
9591
  "image": [{
9515
9592
  "path": "url",
9516
9593
  "valueType": "string"
@@ -9718,6 +9795,10 @@ Object.freeze({
9718
9795
  ]
9719
9796
  }
9720
9797
  ],
9798
+ "meter": [{
9799
+ "path": "value",
9800
+ "valueType": "number"
9801
+ }],
9721
9802
  "motion": [
9722
9803
  {
9723
9804
  "path": "detected",
@@ -9849,20 +9930,10 @@ Object.freeze({
9849
9930
  "valueType": "boolean"
9850
9931
  }
9851
9932
  ],
9852
- "numeric-sensor": [
9853
- {
9854
- "path": "value",
9855
- "valueType": "number"
9856
- },
9857
- {
9858
- "path": "unit",
9859
- "valueType": "string"
9860
- },
9861
- {
9862
- "path": "precision",
9863
- "valueType": "number"
9864
- }
9865
- ],
9933
+ "numeric-sensor": [{
9934
+ "path": "value",
9935
+ "valueType": "number"
9936
+ }],
9866
9937
  "pet-feeder": [
9867
9938
  {
9868
9939
  "path": "foodLevel",
@@ -9954,14 +10025,6 @@ Object.freeze({
9954
10025
  {
9955
10026
  "path": "powerFactor",
9956
10027
  "valueType": "number"
9957
- },
9958
- {
9959
- "path": "unit",
9960
- "valueType": "string"
9961
- },
9962
- {
9963
- "path": "precision",
9964
- "valueType": "number"
9965
10028
  }
9966
10029
  ],
9967
10030
  "presence": [
@@ -9990,20 +10053,10 @@ Object.freeze({
9990
10053
  "valueType": "number"
9991
10054
  }
9992
10055
  ],
9993
- "pressure-sensor": [
9994
- {
9995
- "path": "hpa",
9996
- "valueType": "number"
9997
- },
9998
- {
9999
- "path": "unit",
10000
- "valueType": "string"
10001
- },
10002
- {
10003
- "path": "precision",
10004
- "valueType": "number"
10005
- }
10006
- ],
10056
+ "pressure-sensor": [{
10057
+ "path": "hpa",
10058
+ "valueType": "number"
10059
+ }],
10007
10060
  "privacy-mask": [{
10008
10061
  "path": "enabled",
10009
10062
  "valueType": "boolean"
@@ -10314,20 +10367,10 @@ Object.freeze({
10314
10367
  "path": "tampered",
10315
10368
  "valueType": "boolean"
10316
10369
  }],
10317
- "temperature-sensor": [
10318
- {
10319
- "path": "celsius",
10320
- "valueType": "number"
10321
- },
10322
- {
10323
- "path": "unit",
10324
- "valueType": "string"
10325
- },
10326
- {
10327
- "path": "precision",
10328
- "valueType": "number"
10329
- }
10330
- ],
10370
+ "temperature-sensor": [{
10371
+ "path": "celsius",
10372
+ "valueType": "number"
10373
+ }],
10331
10374
  "update": [
10332
10375
  {
10333
10376
  "path": "currentVersion",
@@ -10438,10 +10481,6 @@ Object.freeze({
10438
10481
  "path": "temperature",
10439
10482
  "valueType": "number"
10440
10483
  },
10441
- {
10442
- "path": "temperatureUnit",
10443
- "valueType": "string"
10444
- },
10445
10484
  {
10446
10485
  "path": "humidity",
10447
10486
  "valueType": "number"
@@ -10450,18 +10489,10 @@ Object.freeze({
10450
10489
  "path": "pressure",
10451
10490
  "valueType": "number"
10452
10491
  },
10453
- {
10454
- "path": "pressureUnit",
10455
- "valueType": "string"
10456
- },
10457
10492
  {
10458
10493
  "path": "windSpeed",
10459
10494
  "valueType": "number"
10460
10495
  },
10461
- {
10462
- "path": "windSpeedUnit",
10463
- "valueType": "string"
10464
- },
10465
10496
  {
10466
10497
  "path": "windBearing",
10467
10498
  "valueType": "number"
@@ -10549,7 +10580,7 @@ function sleep$2(ms) {
10549
10580
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
10550
10581
  }
10551
10582
  //#endregion
10552
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10583
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10553
10584
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10554
10585
  DeviceType["Camera"] = "camera";
10555
10586
  DeviceType["Hub"] = "hub";
@@ -10643,8 +10674,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10643
10674
  DeviceType["PetFeeder"] = "pet-feeder";
10644
10675
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10645
10676
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10646
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10677
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10678
+ * A plug-built dishwasher carries running-state and power instead. */
10647
10679
  DeviceType["Dishwasher"] = "dishwasher";
10680
+ /** Washing machine. A plug-built one is running while its power stays
10681
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10682
+ DeviceType["Washer"] = "washer";
10683
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10684
+ DeviceType["Dryer"] = "dryer";
10648
10685
  return DeviceType;
10649
10686
  }({});
10650
10687
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -10892,7 +10929,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
10892
10929
  return DeviceRole;
10893
10930
  }({});
10894
10931
  //#endregion
10895
- //#region ../types/dist/composition-kEl9mevu.mjs
10932
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
10896
10933
  /**
10897
10934
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10898
10935
  *
@@ -11207,6 +11244,13 @@ var table$1 = {
11207
11244
  apply: () => {
11208
11245
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11209
11246
  }
11247
+ },
11248
+ schmitt: {
11249
+ minArgs: 3,
11250
+ maxArgs: 5,
11251
+ apply: () => {
11252
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11253
+ }
11210
11254
  }
11211
11255
  };
11212
11256
  Object.freeze(Object.assign(Object.create(null), table$1));
@@ -11647,15 +11691,232 @@ function validateExpressionSource(src) {
11647
11691
  }
11648
11692
  return null;
11649
11693
  }
11650
- /** A composed device's stableId is this prefix plus the block id. */
11651
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11652
- /** Longest snippet a `code` source may carry (slice 4). */
11653
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11694
+ /**
11695
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11696
+ *
11697
+ * Standby of the control board is under 5 W; a motor or a heater is far
11698
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11699
+ * a cycle, so running starts only after 30 s above the line.
11700
+ *
11701
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11702
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11703
+ * report "finished" in the middle of the program, so the stop dwell is
11704
+ * 10 minutes on every type. The number is stored per type so one machine
11705
+ * can move later without moving the others.
11706
+ */
11707
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11708
+ var APPLIANCE_KINDS = [
11709
+ "washer",
11710
+ "dryer",
11711
+ "dishwasher"
11712
+ ];
11713
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11714
+ /**
11715
+ * Statistics and integrals over a history read (D944).
11716
+ *
11717
+ * Pure: the same points always give the same answer, so a restart recomputes
11718
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11719
+ * never the mean of the samples. A hole with no held value is not integrated
11720
+ * as zero. A truncated read is not published.
11721
+ */
11722
+ var STATISTIC_STATS = [
11723
+ "min",
11724
+ "max",
11725
+ "mean",
11726
+ "sum",
11727
+ "count",
11728
+ "change"
11729
+ ];
11730
+ /**
11731
+ * The range a statistics or integral helper asks history for (D944).
11732
+ *
11733
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11734
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11735
+ * is strictly longer than the longest that range can be. Forever qualifies.
11736
+ * Equal to a tier's retention is a refusal, not a short answer.
11737
+ */
11738
+ var MINUTE_MS = 6e4;
11739
+ var HOUR_MS = 60 * MINUTE_MS;
11740
+ var DAY_MS = 24 * HOUR_MS;
11741
+ var ROLLING_SPANS = [
11742
+ "1h",
11743
+ "24h",
11744
+ "2d",
11745
+ "7d",
11746
+ "30d",
11747
+ "custom"
11748
+ ];
11749
+ var CALENDAR_PERIODS = [
11750
+ "today",
11751
+ "yesterday",
11752
+ "this-week",
11753
+ "this-month"
11754
+ ];
11755
+ var RESET_PERIODS = [
11756
+ "never",
11757
+ "daily",
11758
+ "weekly",
11759
+ "monthly"
11760
+ ];
11761
+ 5 * MINUTE_MS;
11762
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11763
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11764
+ var rollingWindowObject = object({
11765
+ kind: literal("rolling"),
11766
+ span: _enum(ROLLING_SPANS),
11767
+ customMinutes: number().int().min(5).optional()
11768
+ }).check((payload) => {
11769
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11770
+ code: "custom",
11771
+ message: "a custom range needs a duration in minutes",
11772
+ path: ["customMinutes"],
11773
+ input: payload.value
11774
+ });
11775
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11776
+ code: "custom",
11777
+ message: "only a custom range carries a duration",
11778
+ path: ["customMinutes"],
11779
+ input: payload.value
11780
+ });
11781
+ });
11782
+ var RollingWindowSchema = rollingWindowObject;
11783
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
11784
+ kind: literal("calendar"),
11785
+ period: _enum(CALENDAR_PERIODS)
11786
+ })]);
11787
+ /**
11788
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11789
+ * id. Split out so helper schemas can name it without a cycle through
11790
+ * `composition.ts` (D659).
11791
+ */
11654
11792
  var CompositionSourceRefSchema = object({
11655
11793
  /** The owning addon; `stableId` is unique only within it. */
11656
11794
  addonId: string().min(1),
11657
11795
  stableId: string().min(1)
11658
11796
  });
11797
+ var GROUP_TEMPLATE_IDS = [
11798
+ "temperature",
11799
+ "humidity",
11800
+ "pressure",
11801
+ "illuminance",
11802
+ "power",
11803
+ "energy",
11804
+ "battery",
11805
+ "numeric",
11806
+ "motion",
11807
+ "contact",
11808
+ "leak",
11809
+ "smoke",
11810
+ "occupancy",
11811
+ "binary",
11812
+ "switch",
11813
+ "light",
11814
+ "fan",
11815
+ "cover",
11816
+ "valve",
11817
+ "lock",
11818
+ "media-player",
11819
+ "climate"
11820
+ ];
11821
+ var NUMERIC_AGGREGATES = [
11822
+ "mean",
11823
+ "median",
11824
+ "min",
11825
+ "max",
11826
+ "sum"
11827
+ ];
11828
+ var BINARY_AGGREGATES = ["any", "all"];
11829
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11830
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11831
+ var AGGREGATE_REDUCERS = [
11832
+ "mean",
11833
+ "median",
11834
+ "min",
11835
+ "max",
11836
+ "sum",
11837
+ "any",
11838
+ "all"
11839
+ ];
11840
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11841
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11842
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11843
+ /** One member. The field is stored so occupancy can name motion or binary. */
11844
+ var HelperMemberSchema = object({
11845
+ source: CompositionSourceRefSchema,
11846
+ cap: string().min(1),
11847
+ fieldPath: string().min(1)
11848
+ });
11849
+ var HelperSpecSchema = discriminatedUnion("kind", [
11850
+ object({
11851
+ kind: literal("group"),
11852
+ templateId: GroupTemplateIdSchema,
11853
+ aggregate: AggregateReducerSchema,
11854
+ members: array(HelperMemberSchema).min(1).max(64),
11855
+ /** Actuator groups only. Readings leave this unset. */
11856
+ active: ActiveCriterionSchema.optional()
11857
+ }),
11858
+ object({
11859
+ kind: literal("appliance"),
11860
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
11861
+ appliance: _enum(APPLIANCE_KINDS),
11862
+ thresholdWatts: number().positive(),
11863
+ startDwellMs: number().int().nonnegative(),
11864
+ stopDwellMs: number().int().nonnegative(),
11865
+ member: HelperMemberSchema
11866
+ }),
11867
+ object({
11868
+ kind: literal("statistic"),
11869
+ stat: _enum(STATISTIC_STATS),
11870
+ window: StatisticWindowSchema,
11871
+ source: HelperMemberSchema
11872
+ }),
11873
+ object({
11874
+ kind: literal("integral"),
11875
+ reset: _enum(RESET_PERIODS),
11876
+ output: _enum(["energy", "meter"]),
11877
+ source: HelperMemberSchema
11878
+ }),
11879
+ object({
11880
+ kind: literal("threshold"),
11881
+ source: HelperMemberSchema,
11882
+ high: number(),
11883
+ low: number(),
11884
+ onForMs: number().int().nonnegative(),
11885
+ offForMs: number().int().nonnegative()
11886
+ }),
11887
+ object({
11888
+ kind: literal("arithmetic"),
11889
+ op: _enum([
11890
+ "sum",
11891
+ "difference",
11892
+ "ratio"
11893
+ ]),
11894
+ inputs: array(HelperMemberSchema).min(2).max(64)
11895
+ }),
11896
+ object({
11897
+ kind: literal("expression"),
11898
+ expr: string().min(1),
11899
+ inputs: array(object({
11900
+ name: string().min(1),
11901
+ member: HelperMemberSchema
11902
+ })).min(1).max(64),
11903
+ output: _enum(["number", "boolean"])
11904
+ }),
11905
+ object({
11906
+ kind: literal("derivative"),
11907
+ window: RollingWindowSchema,
11908
+ per: _enum([
11909
+ "second",
11910
+ "minute",
11911
+ "hour"
11912
+ ]),
11913
+ source: HelperMemberSchema
11914
+ })
11915
+ ]);
11916
+ /** A composed device's stableId is this prefix plus the block id. */
11917
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11918
+ /** Longest snippet a `code` source may carry (slice 4). */
11919
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11659
11920
  /** One field of one capability of one source device. */
11660
11921
  var CompositionFieldReadSchema = object({
11661
11922
  source: CompositionSourceRefSchema,
@@ -11699,7 +11960,59 @@ var CompositionCodeSourceSchema = object({
11699
11960
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11700
11961
  CompositionFromSourceSchema,
11701
11962
  CompositionExpressionSourceSchema,
11702
- CompositionCodeSourceSchema
11963
+ CompositionCodeSourceSchema,
11964
+ object({
11965
+ kind: literal("aggregate"),
11966
+ reducer: _enum([
11967
+ "mean",
11968
+ "median",
11969
+ "min",
11970
+ "max",
11971
+ "sum",
11972
+ "any",
11973
+ "all"
11974
+ ]),
11975
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11976
+ /**
11977
+ * Set when the reads are an enum state, not a yes/no: the listed states
11978
+ * count as active (a cover is `open`). A state outside the list is inactive;
11979
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
11980
+ */
11981
+ activeWhen: array(string().min(1)).min(1).optional(),
11982
+ /** `value` is the reading; the others are the member counts. */
11983
+ publish: _enum([
11984
+ "value",
11985
+ "included",
11986
+ "excluded",
11987
+ "total"
11988
+ ]),
11989
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11990
+ empty: _enum([
11991
+ "null",
11992
+ "omit",
11993
+ "offline"
11994
+ ])
11995
+ }),
11996
+ object({
11997
+ kind: literal("series"),
11998
+ read: CompositionFieldReadSchema,
11999
+ /** `value` is the statistic or the integral. The others are the gap counts. */
12000
+ publish: _enum([
12001
+ "value",
12002
+ "included",
12003
+ "excluded",
12004
+ "total"
12005
+ ]),
12006
+ /**
12007
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
12008
+ * device unseeded until history answers, so a restart cannot publish 0.
12009
+ */
12010
+ empty: _enum([
12011
+ "null",
12012
+ "omit",
12013
+ "offline"
12014
+ ])
12015
+ })
11703
12016
  ]);
11704
12017
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11705
12018
  kind: literal("forward"),
@@ -11745,7 +12058,13 @@ var CompositionSchema = object({
11745
12058
  kind: literal("existing"),
11746
12059
  device: CompositionSourceRefSchema
11747
12060
  })]),
11748
- features: array(CompositionFeatureSchema).min(1).max(16)
12061
+ features: array(CompositionFeatureSchema).min(1).max(16),
12062
+ /**
12063
+ * Present when the block is a helper template. Absent on a free
12064
+ * composition (the room example, device 4603). The editor reads this,
12065
+ * never the compiled features.
12066
+ */
12067
+ helper: HelperSpecSchema.optional()
11749
12068
  }).superRefine((composition, ctx) => {
11750
12069
  const seen = /* @__PURE__ */ new Set();
11751
12070
  composition.features.forEach((feature, index) => {
@@ -17422,7 +17741,22 @@ var BrokerProviderInfoSchema = object({
17422
17741
  */
17423
17742
  interactiveConnectPath: string().min(1).optional(),
17424
17743
  /** Relative hub path to disconnect the linked account (same popup pattern). */
17425
- interactiveDisconnectPath: string().min(1).optional()
17744
+ interactiveDisconnectPath: string().min(1).optional(),
17745
+ /**
17746
+ * The kind signs in through a server-held interactive session instead of
17747
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17748
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17749
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17750
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17751
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17752
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17753
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17754
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17755
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17756
+ * offers it as the default path; the fields the session fills are
17757
+ * marked `filledBySignIn` in the kind's settings schema.
17758
+ */
17759
+ signIn: object({ basePath: string().min(1) }).optional()
17426
17760
  }))
17427
17761
  });
17428
17762
  var ListInputSchema = object({
@@ -18523,7 +18857,12 @@ var CompositionProblemSchema = object({
18523
18857
  "item-field-reserved",
18524
18858
  "target-missing",
18525
18859
  "target-unreadable",
18526
- "not-replaceable"
18860
+ "not-replaceable",
18861
+ "native-capability-replace",
18862
+ "mixed-quantity",
18863
+ "helper-mismatch",
18864
+ "member-constraint",
18865
+ "retention"
18527
18866
  ]),
18528
18867
  path: string(),
18529
18868
  message: string()
@@ -18544,7 +18883,8 @@ var CompositionUnavailableModeSchema = _enum([
18544
18883
  "null",
18545
18884
  "offline",
18546
18885
  "unknown",
18547
- "release"
18886
+ "release",
18887
+ "omit"
18548
18888
  ]);
18549
18889
  /**
18550
18890
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18581,9 +18921,11 @@ var CompositionFieldPlanSchema = object({
18581
18921
  reads: array(CompositionFieldReadSchema)
18582
18922
  });
18583
18923
  /**
18584
- * What a feature does to its target (D663): `new` builds a capability on a new
18585
- * device; on an existing device it `add`s a capability the device lacks, or
18586
- * `replace`s named fields of one it has natively.
18924
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18925
+ * new device; on an existing device it `add`s a capability the device lacks.
18926
+ * Replacing a capability the device already serves is refused
18927
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18928
+ * wire for a claim the hub already holds.
18587
18929
  */
18588
18930
  var CompositionFeatureModeSchema = _enum([
18589
18931
  "new",
@@ -18754,8 +19096,392 @@ Object.freeze({
18754
19096
  latchedAt: {
18755
19097
  kind: "fixed",
18756
19098
  result: ["number", "null"]
19099
+ },
19100
+ schmitt: {
19101
+ kind: "fixed",
19102
+ result: ["boolean", "null"]
18757
19103
  }
18758
19104
  });
19105
+ /**
19106
+ * One catalog row per base reading a group helper can build (D944 §3).
19107
+ * Data, not a form default written into the block.
19108
+ */
19109
+ var NOW$2 = {
19110
+ fieldPath: "lastFetchedAt",
19111
+ expr: "now"
19112
+ };
19113
+ var CHANGED = {
19114
+ fieldPath: "lastChangedAt",
19115
+ expr: "now"
19116
+ };
19117
+ function numeric(input) {
19118
+ return {
19119
+ ...input,
19120
+ family: "numeric",
19121
+ deviceType: DeviceType.Sensor,
19122
+ memberDeviceType: DeviceType.Sensor,
19123
+ memberDeviceTypes: [DeviceType.Sensor],
19124
+ activeWhen: void 0,
19125
+ commandCaps: [],
19126
+ baseUnit: baseUnitOf(input.quantity),
19127
+ aggregates: NUMERIC_AGGREGATES,
19128
+ accept: [{
19129
+ cap: input.outputCap,
19130
+ fieldPath: input.outputField
19131
+ }]
19132
+ };
19133
+ }
19134
+ function binary(input) {
19135
+ return {
19136
+ ...input,
19137
+ family: "binary",
19138
+ deviceType: DeviceType.Sensor,
19139
+ memberDeviceType: DeviceType.Sensor,
19140
+ memberDeviceTypes: [DeviceType.Sensor],
19141
+ activeWhen: void 0,
19142
+ commandCaps: [],
19143
+ quantity: "none",
19144
+ baseUnit: void 0,
19145
+ aggregates: BINARY_AGGREGATES,
19146
+ empty: "offline"
19147
+ };
19148
+ }
19149
+ var GROUP_TEMPLATES = [
19150
+ numeric({
19151
+ id: "temperature",
19152
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
19153
+ role: DeviceRole.TemperatureSensor,
19154
+ outputCap: "temperature-sensor",
19155
+ outputField: "celsius",
19156
+ quantity: "temperature",
19157
+ defaultAggregate: "mean",
19158
+ empty: "null",
19159
+ companions: [NOW$2]
19160
+ }),
19161
+ numeric({
19162
+ id: "humidity",
19163
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
19164
+ role: DeviceRole.HumiditySensor,
19165
+ outputCap: "humidity-sensor",
19166
+ outputField: "percent",
19167
+ quantity: "humidity",
19168
+ defaultAggregate: "mean",
19169
+ empty: "null",
19170
+ companions: [NOW$2]
19171
+ }),
19172
+ numeric({
19173
+ id: "pressure",
19174
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
19175
+ role: DeviceRole.PressureSensor,
19176
+ outputCap: "pressure-sensor",
19177
+ outputField: "hpa",
19178
+ quantity: "pressure",
19179
+ defaultAggregate: "mean",
19180
+ empty: "null",
19181
+ companions: [NOW$2]
19182
+ }),
19183
+ numeric({
19184
+ id: "illuminance",
19185
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
19186
+ role: DeviceRole.AmbientLightSensor,
19187
+ outputCap: "ambient-light-sensor",
19188
+ outputField: "lux",
19189
+ quantity: "illuminance",
19190
+ defaultAggregate: "mean",
19191
+ empty: "null",
19192
+ companions: [NOW$2]
19193
+ }),
19194
+ numeric({
19195
+ id: "power",
19196
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
19197
+ role: DeviceRole.PowerSensor,
19198
+ outputCap: "power-meter",
19199
+ outputField: "watts",
19200
+ quantity: "power",
19201
+ defaultAggregate: "sum",
19202
+ empty: "omit",
19203
+ companions: [NOW$2]
19204
+ }),
19205
+ numeric({
19206
+ id: "energy",
19207
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
19208
+ role: DeviceRole.EnergySensor,
19209
+ outputCap: "power-meter",
19210
+ outputField: "kwhTotal",
19211
+ quantity: "energy",
19212
+ defaultAggregate: "sum",
19213
+ empty: "omit",
19214
+ companions: [NOW$2]
19215
+ }),
19216
+ numeric({
19217
+ id: "battery",
19218
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
19219
+ role: DeviceRole.BatterySensor,
19220
+ outputCap: "battery",
19221
+ outputField: "percentage",
19222
+ quantity: "percentage",
19223
+ defaultAggregate: "mean",
19224
+ empty: "null",
19225
+ companions: [
19226
+ {
19227
+ fieldPath: "charging",
19228
+ expr: "\"unknown\""
19229
+ },
19230
+ {
19231
+ fieldPath: "sleeping",
19232
+ expr: "false"
19233
+ },
19234
+ {
19235
+ fieldPath: "lastUpdated",
19236
+ expr: "now"
19237
+ }
19238
+ ]
19239
+ }),
19240
+ numeric({
19241
+ id: "numeric",
19242
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
19243
+ role: DeviceRole.NumericSensor,
19244
+ outputCap: "numeric-sensor",
19245
+ outputField: "value",
19246
+ quantity: "device",
19247
+ defaultAggregate: "mean",
19248
+ empty: "null",
19249
+ companions: [NOW$2]
19250
+ }),
19251
+ binary({
19252
+ id: "motion",
19253
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
19254
+ role: DeviceRole.MotionSensor,
19255
+ outputCap: "motion",
19256
+ outputField: "detected",
19257
+ defaultAggregate: "any",
19258
+ accept: [{
19259
+ cap: "motion",
19260
+ fieldPath: "detected"
19261
+ }],
19262
+ companions: [{
19263
+ fieldPath: "lastDetectedAt",
19264
+ expr: "null"
19265
+ }, {
19266
+ fieldPath: "autoClearAfterMs",
19267
+ expr: "null"
19268
+ }]
19269
+ }),
19270
+ binary({
19271
+ id: "contact",
19272
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
19273
+ role: DeviceRole.ContactSensor,
19274
+ outputCap: "contact",
19275
+ outputField: "entryOpen",
19276
+ defaultAggregate: "any",
19277
+ accept: [{
19278
+ cap: "contact",
19279
+ fieldPath: "entryOpen"
19280
+ }],
19281
+ companions: [CHANGED]
19282
+ }),
19283
+ binary({
19284
+ id: "leak",
19285
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
19286
+ role: DeviceRole.LeakSensor,
19287
+ outputCap: "flood",
19288
+ outputField: "flooded",
19289
+ defaultAggregate: "any",
19290
+ accept: [{
19291
+ cap: "flood",
19292
+ fieldPath: "flooded"
19293
+ }],
19294
+ companions: [CHANGED]
19295
+ }),
19296
+ binary({
19297
+ id: "smoke",
19298
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
19299
+ role: DeviceRole.SmokeSensor,
19300
+ outputCap: "smoke",
19301
+ outputField: "detected",
19302
+ defaultAggregate: "any",
19303
+ accept: [{
19304
+ cap: "smoke",
19305
+ fieldPath: "detected"
19306
+ }],
19307
+ companions: [CHANGED]
19308
+ }),
19309
+ binary({
19310
+ id: "occupancy",
19311
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
19312
+ role: DeviceRole.BinarySensor,
19313
+ outputCap: "binary",
19314
+ outputField: "on",
19315
+ defaultAggregate: "any",
19316
+ accept: [{
19317
+ cap: "motion",
19318
+ fieldPath: "detected"
19319
+ }, {
19320
+ cap: "binary",
19321
+ fieldPath: "on"
19322
+ }],
19323
+ companions: [CHANGED]
19324
+ }),
19325
+ binary({
19326
+ id: "binary",
19327
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
19328
+ role: DeviceRole.BinarySensor,
19329
+ outputCap: "binary",
19330
+ outputField: "on",
19331
+ defaultAggregate: "any",
19332
+ accept: [{
19333
+ cap: "binary",
19334
+ fieldPath: "on"
19335
+ }],
19336
+ companions: [CHANGED]
19337
+ }),
19338
+ actuator({
19339
+ id: "switch",
19340
+ 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.",
19341
+ deviceType: DeviceType.Switch,
19342
+ outputCap: "switch",
19343
+ outputField: "on",
19344
+ member: {
19345
+ cap: "switch",
19346
+ fieldPath: "on"
19347
+ },
19348
+ commandCaps: ["switch"]
19349
+ }),
19350
+ actuator({
19351
+ id: "light",
19352
+ 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.",
19353
+ deviceType: DeviceType.Light,
19354
+ outputCap: "switch",
19355
+ outputField: "on",
19356
+ member: {
19357
+ cap: "switch",
19358
+ fieldPath: "on"
19359
+ },
19360
+ commandCaps: [
19361
+ "switch",
19362
+ "brightness",
19363
+ "color"
19364
+ ]
19365
+ }),
19366
+ actuator({
19367
+ id: "fan",
19368
+ 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.",
19369
+ deviceType: DeviceType.Fan,
19370
+ outputCap: "switch",
19371
+ outputField: "on",
19372
+ member: {
19373
+ cap: "switch",
19374
+ fieldPath: "on"
19375
+ },
19376
+ commandCaps: ["switch", "fan-control"]
19377
+ }),
19378
+ actuator({
19379
+ id: "cover",
19380
+ 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.",
19381
+ deviceType: DeviceType.Cover,
19382
+ outputCap: "group-active",
19383
+ outputField: "active",
19384
+ member: {
19385
+ cap: "cover",
19386
+ fieldPath: "state"
19387
+ },
19388
+ activeWhen: [
19389
+ "open",
19390
+ "opening",
19391
+ "stopped"
19392
+ ],
19393
+ commandCaps: ["cover"]
19394
+ }),
19395
+ actuator({
19396
+ id: "valve",
19397
+ 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.",
19398
+ deviceType: DeviceType.Valve,
19399
+ outputCap: "group-active",
19400
+ outputField: "active",
19401
+ member: {
19402
+ cap: "valve",
19403
+ fieldPath: "state"
19404
+ },
19405
+ activeWhen: [
19406
+ "open",
19407
+ "opening",
19408
+ "stopped"
19409
+ ],
19410
+ commandCaps: ["valve"]
19411
+ }),
19412
+ actuator({
19413
+ id: "lock",
19414
+ 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.",
19415
+ deviceType: DeviceType.Lock,
19416
+ outputCap: "group-active",
19417
+ outputField: "active",
19418
+ member: {
19419
+ cap: "lock-control",
19420
+ fieldPath: "state"
19421
+ },
19422
+ activeWhen: ["locked"],
19423
+ commandCaps: ["lock-control"]
19424
+ }),
19425
+ actuator({
19426
+ id: "media-player",
19427
+ 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.",
19428
+ deviceType: DeviceType.MediaPlayer,
19429
+ outputCap: "group-active",
19430
+ outputField: "active",
19431
+ member: {
19432
+ cap: "media-player",
19433
+ fieldPath: "state"
19434
+ },
19435
+ activeWhen: ["playing", "buffering"],
19436
+ commandCaps: ["media-player"]
19437
+ }),
19438
+ actuator({
19439
+ id: "climate",
19440
+ 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.",
19441
+ deviceType: DeviceType.Thermostat,
19442
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
19443
+ outputCap: "group-active",
19444
+ outputField: "active",
19445
+ member: {
19446
+ cap: "climate-control",
19447
+ fieldPath: "mode"
19448
+ },
19449
+ activeWhen: [
19450
+ "heat",
19451
+ "cool",
19452
+ "auto",
19453
+ "heat_cool",
19454
+ "fan_only",
19455
+ "dry"
19456
+ ],
19457
+ commandCaps: ["climate-control"]
19458
+ })
19459
+ ];
19460
+ function actuator(input) {
19461
+ return {
19462
+ id: input.id,
19463
+ summary: input.summary,
19464
+ deviceType: input.deviceType,
19465
+ outputCap: input.outputCap,
19466
+ outputField: input.outputField,
19467
+ commandCaps: input.commandCaps,
19468
+ family: "actuator",
19469
+ role: void 0,
19470
+ memberDeviceType: input.deviceType,
19471
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
19472
+ activeWhen: input.activeWhen,
19473
+ quantity: "none",
19474
+ baseUnit: void 0,
19475
+ aggregates: BINARY_AGGREGATES,
19476
+ defaultAggregate: "any",
19477
+ empty: "offline",
19478
+ accept: [input.member],
19479
+ companions: [CHANGED]
19480
+ };
19481
+ }
19482
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19483
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19484
+ DeviceType.Camera;
18759
19485
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18760
19486
  /**
18761
19487
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20582,12 +21308,12 @@ function buildDeviceAdminUrl(parts) {
20582
21308
  * providers detect renames by matching `uniqueId` and patch `id`
20583
21309
  * in-place so dispatch keeps working.
20584
21310
  *
20585
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20586
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20587
- * state push and are the single source of truth. The `device_class` tag is
20588
- * an HA-specific input that is consumed at the discovery boundary
20589
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20590
- * stored after that point.
21311
+ * **Display unit and precision are NOT stored here.** They are a read-time
21312
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
21313
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
21314
+ * neither is repeated on each sample. The `device_class` tag is an
21315
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
21316
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20591
21317
  *
20592
21318
  * The `raw` escape hatch carries provider-specific attributes that don't
20593
21319
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20601,6 +21327,14 @@ var SourceInfoSchema = object({
20601
21327
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20602
21328
  * Used to detect rename when `id` changes. */
20603
21329
  uniqueId: string().optional(),
21330
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
21331
+ quantity: ReadingQuantitySchema.optional(),
21332
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
21333
+ dateTimeShape: _enum([
21334
+ "date",
21335
+ "time",
21336
+ "datetime"
21337
+ ]).optional(),
20604
21338
  /** Provider-specific extras that don't fit the structured slots.
20605
21339
  * Whitelist what goes here per provider — do NOT dump entire HA
20606
21340
  * attribute blobs (would explode the meta JSON column). */
@@ -41705,7 +42439,8 @@ method(object({
41705
42439
  "enum"
41706
42440
  ]),
41707
42441
  enumValues: array(string()).optional(),
41708
- item: boolean().optional()
42442
+ item: boolean().optional(),
42443
+ quantity: ReadingQuantitySchema.optional()
41709
42444
  })).readonly(),
41710
42445
  itemArray: object({
41711
42446
  path: string(),
@@ -44106,7 +44841,7 @@ var NetworkAccessStatusSchema = object({
44106
44841
  });
44107
44842
  /**
44108
44843
  * Optional, richer endpoint shape returned by providers that expose
44109
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44844
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
44110
44845
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
44111
44846
  * the originating provider config (mode + sourcePort) so the
44112
44847
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -45589,8 +46324,8 @@ var MaskPolygonShapeSchema = object({
45589
46324
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45590
46325
  var MaskGridShapeSchema = object({
45591
46326
  kind: literal("grid"),
45592
- gridWidth: number(),
45593
- gridHeight: number(),
46327
+ gridWidth: number().meta(readingMeta("none")),
46328
+ gridHeight: number().meta(readingMeta("none")),
45594
46329
  cells: array(boolean())
45595
46330
  });
45596
46331
  discriminatedUnion("kind", [
@@ -45702,6 +46437,8 @@ var NcSystemEventKindSchema = _enum([
45702
46437
  "detection-blind",
45703
46438
  "motion-noisy",
45704
46439
  "motion-calm",
46440
+ "package-llm-silent",
46441
+ "package-llm-answered",
45705
46442
  "recording-stalled",
45706
46443
  "recording-healthy",
45707
46444
  "recording-degraded",
@@ -46791,7 +47528,20 @@ var NcRuleInputSchema = object({
46791
47528
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46792
47529
  * path does not run Zod defaults.
46793
47530
  */
46794
- groupingSec: number().int().min(5).max(60).optional()
47531
+ groupingSec: number().int().min(5).max(60).optional(),
47532
+ /**
47533
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
47534
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
47535
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
47536
+ * same mark the Library writes, and records whose notification it was and
47537
+ * when. A channel with no buttons gets the same two choices as links.
47538
+ *
47539
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
47540
+ * cap path): absent and `false` both mean off, which is what every rule
47541
+ * authored before this field meant. An editor turns it off by writing
47542
+ * `false`, so it needs no `clear` key.
47543
+ */
47544
+ askVerdict: boolean().optional()
46795
47545
  });
46796
47546
  /**
46797
47547
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -57315,6 +58065,28 @@ var PackageVersionInfoSchema = object({
57315
58065
  deprecated: string().optional(),
57316
58066
  distTags: array(string()).readonly()
57317
58067
  });
58068
+ /**
58069
+ * One published document. `ok` is markdown from the registry. `missing` means
58070
+ * the registry answered and the file was not there. `error` is a registry or
58071
+ * tarball failure — never an empty string standing in for either.
58072
+ */
58073
+ var PackageDocBodySchema = discriminatedUnion("status", [
58074
+ object({
58075
+ status: literal("ok"),
58076
+ markdown: string()
58077
+ }),
58078
+ object({ status: literal("missing") }),
58079
+ object({
58080
+ status: literal("error"),
58081
+ message: string()
58082
+ })
58083
+ ]);
58084
+ var PackageDocsSchema = object({
58085
+ name: string(),
58086
+ version: string(),
58087
+ readme: PackageDocBodySchema,
58088
+ changelog: PackageDocBodySchema
58089
+ });
57318
58090
  var SearchResultSchema = object({
57319
58091
  name: string(),
57320
58092
  version: string(),
@@ -57533,7 +58305,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57533
58305
  }), object({ success: literal(true) }), {
57534
58306
  kind: "mutation",
57535
58307
  auth: "admin"
57536
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
58308
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
58309
+ name: string().min(1),
58310
+ version: string().min(1).optional()
58311
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
57537
58312
  kind: "mutation",
57538
58313
  auth: "admin"
57539
58314
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -57592,27 +58367,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57592
58367
  */
57593
58368
  var AirQualitySensorStatusSchema = object({
57594
58369
  /** Carbon dioxide concentration in ppm. */
57595
- co2Ppm: number().min(0).optional(),
58370
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
57596
58371
  /** Total volatile organic compounds in ppb. */
57597
- vocPpb: number().min(0).optional(),
58372
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
57598
58373
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
57599
- pm25: number().min(0).optional(),
58374
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
57600
58375
  /** Particulate matter ≤ 10 μm in µg/m³. */
57601
- pm10: number().min(0).optional(),
58376
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
57602
58377
  /** Composite AQI value (typically 0..500). */
57603
- aqi: number().optional(),
58378
+ aqi: number().optional().meta(readingMeta("index")),
57604
58379
  /** Ms epoch when the slice was last updated. */
57605
- lastFetchedAt: number(),
57606
- /** Live display unit of the single metric this slice carries (e.g. HA
57607
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
57608
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
57609
- * per slice is unambiguous. */
57610
- unit: string().optional(),
57611
- /** Suggested decimal places for numeric display.
57612
- * Populated live from the upstream source when provided (e.g. HA
57613
- * `attributes.suggested_display_precision`). Falls back to
57614
- * auto-formatting when absent. */
57615
- precision: number().int().min(0).max(10).optional()
58380
+ lastFetchedAt: number()
57616
58381
  });
57617
58382
  var airQualitySensorCapability = {
57618
58383
  name: "air-quality-sensor",
@@ -57649,12 +58414,12 @@ var airQualitySensorCapability = {
57649
58414
  },
57650
58415
  pm25: {
57651
58416
  kind: "gauge",
57652
- unit: "ug/m3",
58417
+ unit: "µg/m³",
57653
58418
  deadband: 1
57654
58419
  },
57655
58420
  pm10: {
57656
58421
  kind: "gauge",
57657
- unit: "ug/m3",
58422
+ unit: "µg/m³",
57658
58423
  deadband: 1
57659
58424
  },
57660
58425
  aqi: {
@@ -57672,22 +58437,13 @@ var airQualitySensorCapability = {
57672
58437
  * entries with `device_class: illuminance`.
57673
58438
  */
57674
58439
  var AmbientLightSensorStatusSchema = object({
57675
- /** Current illuminance in lux (lx). */
57676
- lux: number().min(0).meta({
57677
- unit: "lx",
58440
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
58441
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
58442
+ precision: 0,
57678
58443
  label: "Illuminance"
57679
- }),
58444
+ })),
57680
58445
  /** Ms epoch when the slice was last updated. */
57681
- lastFetchedAt: number(),
57682
- /** Live display unit from the upstream source (e.g. HA
57683
- * `attributes.unit_of_measurement`). The UI prefers this over the
57684
- * role's canonical unit. Absent → fall back to the canonical unit. */
57685
- unit: string().optional(),
57686
- /** Suggested decimal places for numeric display.
57687
- * Populated live from the upstream source when provided (e.g. HA
57688
- * `attributes.suggested_display_precision`). Falls back to
57689
- * auto-formatting when absent. */
57690
- precision: number().int().min(0).max(10).optional()
58446
+ lastFetchedAt: number()
57691
58447
  });
57692
58448
  var ambientLightSensorCapability = {
57693
58449
  name: "ambient-light-sensor",
@@ -57719,12 +58475,7 @@ var ambientLightSensorCapability = {
57719
58475
  } } },
57720
58476
  /** Clock fields: written, but excluded from the compare that decides
57721
58477
  * whether persisting is worth a SQLite commit. */
57722
- volatileStateFields: ["lastFetchedAt"],
57723
- customize: {
57724
- version: 1,
57725
- advanced: "derive",
57726
- targets: []
57727
- }
58478
+ volatileStateFields: ["lastFetchedAt"]
57728
58479
  };
57729
58480
  /**
57730
58481
  * Per-class audio metrics aggregated over a sliding window.
@@ -57951,10 +58702,7 @@ var BatteryStatusSchema = object({
57951
58702
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57952
58703
  * declines to band a non-finite reading, which is the correct shape.
57953
58704
  */
57954
- percentage: number().min(0).max(100).nullable().meta({
57955
- unit: "%",
57956
- label: "Charge"
57957
- }),
58705
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57958
58706
  /**
57959
58707
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57960
58708
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57981,7 +58729,14 @@ var BatteryStatusSchema = object({
57981
58729
  */
57982
58730
  sleeping: boolean().meta({ label: "Sleeping" }),
57983
58731
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57984
- lastUpdated: number(),
58732
+ lastUpdated: number().meta(readingMeta("none")),
58733
+ /**
58734
+ * Battery voltage in volts, for a cell the firmware reports as voltage
58735
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
58736
+ * that only report charge. A second reading on this cap, not a second
58737
+ * `numeric-sensor` on the same device.
58738
+ */
58739
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57985
58740
  /**
57986
58741
  * Ms epoch of the last time the device PROVED it was reachable — a
57987
58742
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -58028,12 +58783,12 @@ var BATTERY_DEVICE_TYPES = [
58028
58783
  DeviceType.Switch,
58029
58784
  DeviceType.PetFeeder,
58030
58785
  DeviceType.Container,
58031
- DeviceType.Thermostat
58786
+ DeviceType.Thermostat,
58787
+ DeviceType.EventEmitter
58032
58788
  ];
58033
58789
  /** Generic Customize target for the percentage reading (D936). */
58034
58790
  var batteryCustomize = {
58035
58791
  version: 1,
58036
- advanced: "derive",
58037
58792
  targets: [{
58038
58793
  id: "percentage",
58039
58794
  cardinality: "scalar",
@@ -58042,7 +58797,7 @@ var batteryCustomize = {
58042
58797
  stateField: "percentage"
58043
58798
  },
58044
58799
  deviceTypes: BATTERY_DEVICE_TYPES,
58045
- apply: "add-or-replace",
58800
+ apply: "add",
58046
58801
  path: "percentage",
58047
58802
  sources: [{
58048
58803
  kind: "field",
@@ -58163,10 +58918,16 @@ onStatusChanged: { data: object({
58163
58918
  /**
58164
58919
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
58165
58920
  */
58166
- history: { fields: { percentage: {
58167
- kind: "gauge",
58168
- unit: "%"
58169
- } } },
58921
+ history: { fields: {
58922
+ percentage: {
58923
+ kind: "gauge",
58924
+ unit: "%"
58925
+ },
58926
+ volts: {
58927
+ kind: "gauge",
58928
+ unit: "V"
58929
+ }
58930
+ } },
58170
58931
  customize: batteryCustomize,
58171
58932
  /** Clock fields: written, but excluded from the compare that decides
58172
58933
  * whether persisting is worth a SQLite commit. */
@@ -58213,12 +58974,7 @@ var binaryCapability = {
58213
58974
  history: { fields: { on: { kind: "boolean" } } },
58214
58975
  /** Clock fields: written, but excluded from the compare that decides
58215
58976
  * whether persisting is worth a SQLite commit. */
58216
- volatileStateFields: ["lastChangedAt"],
58217
- customize: {
58218
- version: 1,
58219
- advanced: "derive",
58220
- targets: []
58221
- }
58977
+ volatileStateFields: ["lastChangedAt"]
58222
58978
  };
58223
58979
  /**
58224
58980
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -58234,7 +58990,7 @@ var binaryCapability = {
58234
58990
  */
58235
58991
  var BrightnessStatusSchema = object({
58236
58992
  /** Current level as 0..100 inclusive. Firmware-reported. */
58237
- percentage: number().min(0).max(100),
58993
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
58238
58994
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
58239
58995
  lastChangedAt: number()
58240
58996
  });
@@ -58688,21 +59444,21 @@ var ClimateControlStatusSchema = object({
58688
59444
  /** Single setpoint in Celsius. Used by single-target modes
58689
59445
  * (heat / cool / dry). Null when the device is in a range mode
58690
59446
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58691
- target: number().nullable(),
59447
+ target: number().nullable().meta(readingMeta("temperature")),
58692
59448
  /** Upper bound of the dual setpoint in Celsius. Populated only
58693
59449
  * in `heat_cool` mode. */
58694
- targetHigh: number().nullable(),
59450
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58695
59451
  /** Lower bound of the dual setpoint in Celsius. Populated only
58696
59452
  * in `heat_cool` mode. */
58697
- targetLow: number().nullable(),
59453
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58698
59454
  /** Target relative humidity (0..100). Null when the device has no
58699
59455
  * humidity control. */
58700
- targetHumidity: number().min(0).max(100).nullable(),
59456
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58701
59457
  /** Current measured temperature in Celsius. Read-only — pushed by
58702
59458
  * the device's internal sensor. */
58703
- currentTemp: number().nullable(),
59459
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58704
59460
  /** Current measured relative humidity (0..100). Read-only. */
58705
- currentHumidity: number().min(0).max(100).nullable(),
59461
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58706
59462
  /** Vertical louver swing on/off. Null when the device has no vertical
58707
59463
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58708
59464
  swingVertical: boolean().nullable(),
@@ -58710,11 +59466,11 @@ var ClimateControlStatusSchema = object({
58710
59466
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58711
59467
  swingHorizontal: boolean().nullable(),
58712
59468
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58713
- minTemp: number().nullable().optional(),
59469
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58714
59470
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58715
- maxTemp: number().nullable().optional(),
59471
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58716
59472
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58717
- tempStep: number().positive().nullable().optional(),
59473
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58718
59474
  /** Ms epoch when the slice was last updated (push or command). */
58719
59475
  lastFetchedAt: number()
58720
59476
  });
@@ -58856,17 +59612,17 @@ var climateControlCapability = {
58856
59612
  * modes to expose in the picker without a slice fetch.
58857
59613
  */
58858
59614
  var RgbTripletSchema = object({
58859
- r: number().int().min(0).max(255),
58860
- g: number().int().min(0).max(255),
58861
- b: number().int().min(0).max(255)
59615
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
59616
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
59617
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58862
59618
  });
58863
59619
  var HsvTripletSchema = object({
58864
59620
  /** Hue in degrees, 0..360. */
58865
- h: number().min(0).max(360),
59621
+ h: number().min(0).max(360).meta(readingMeta("none")),
58866
59622
  /** Saturation as 0..100 inclusive. */
58867
- s: number().min(0).max(100),
59623
+ s: number().min(0).max(100).meta(readingMeta("none")),
58868
59624
  /** Value/brightness as 0..100 inclusive. */
58869
- v: number().min(0).max(100)
59625
+ v: number().min(0).max(100).meta(readingMeta("none"))
58870
59626
  });
58871
59627
  var ColorInputSchema = discriminatedUnion("mode", [
58872
59628
  object({
@@ -58895,7 +59651,7 @@ var ColorStatusSchema = object({
58895
59651
  /** Populated when `mode === 'hsv'`. */
58896
59652
  hsv: HsvTripletSchema.optional(),
58897
59653
  /** Populated when `mode === 'mired'`. */
58898
- mireds: number().int().optional(),
59654
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58899
59655
  /** Ms epoch of the last operator-driven change. */
58900
59656
  lastChangedAt: number()
58901
59657
  });
@@ -59090,18 +59846,12 @@ _enum([
59090
59846
  "empty",
59091
59847
  "full"
59092
59848
  ]);
59093
- /** Units a remaining-life counter is reported in. Closed: the panel renders
59094
- * each. */
59095
- var ConsumableRemainingUnitSchema = _enum([
59096
- "days",
59097
- "hours",
59098
- "cycles"
59099
- ]);
59100
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
59101
- * days). `value` null = unknown (D393). */
59849
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
59850
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
59851
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
59102
59852
  var ConsumableRemainingSchema = object({
59103
- value: number().min(0).nullable(),
59104
- unit: ConsumableRemainingUnitSchema
59853
+ value: number().min(0).nullable().meta(readingMeta("device")),
59854
+ kind: ConsumableRemainingKindSchema
59105
59855
  });
59106
59856
  /** A single consumable item. Either a continuous `level` (remaining
59107
59857
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -59114,7 +59864,7 @@ var ConsumableItemSchema = object({
59114
59864
  /** Display name. */
59115
59865
  label: string().min(1),
59116
59866
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
59117
- level: number().min(0).max(100).nullable(),
59867
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59118
59868
  /** The provider's discrete verdict when known (binary mode). */
59119
59869
  status: ConsumableStatusSchema.nullable(),
59120
59870
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -59196,8 +59946,8 @@ var consumablesCustomize = {
59196
59946
  transforms: [{ kind: "identity" }]
59197
59947
  }],
59198
59948
  literals: [{
59199
- path: "remaining.unit",
59200
- values: [...ConsumableRemainingUnitSchema.options]
59949
+ path: "remaining.kind",
59950
+ values: [...ConsumableRemainingKindSchema.options]
59201
59951
  }]
59202
59952
  },
59203
59953
  {
@@ -59326,7 +60076,6 @@ reset: method(object({
59326
60076
  },
59327
60077
  "remaining.value": {
59328
60078
  kind: "gauge",
59329
- unitFrom: "remaining.unit",
59330
60079
  deadband: 1,
59331
60080
  displayName: "Remaining"
59332
60081
  }
@@ -59396,10 +60145,9 @@ var contactCapability = {
59396
60145
  * (empty array for the other kinds). It lives in the slice because
59397
60146
  * HA `select.*` entities may update their options at runtime.
59398
60147
  *
59399
- * The `unit`/`min`/`max`/`step` fields are meaningful for
59400
- * `kind === 'numeric'` and are absent (undefined) for all other
59401
- * kinds. They are populated live from HA attributes on each state
59402
- * push — consistent with `options` and the sensor-unit approach.
60148
+ * Numeric `min` / `max` / `step` live in the slice and share the device
60149
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
60150
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
59403
60151
  */
59404
60152
  var ControlStatusSchema = object({
59405
60153
  kind: _enum([
@@ -59411,39 +60159,17 @@ var ControlStatusSchema = object({
59411
60159
  /** Current value. Numeric kind → number; select/text/datetime → string.
59412
60160
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
59413
60161
  * or time-only — HA accepts all three forms). */
59414
- value: union([number(), string()]),
60162
+ value: union([number().meta(readingMeta("device")), string()]),
59415
60163
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
59416
60164
  options: array(string()),
59417
60165
  /** Ms epoch when the slice was last updated. */
59418
60166
  lastChangedAt: number(),
59419
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
59420
- * Populated live from HA `attributes.unit_of_measurement` on each push.
59421
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59422
- unit: string().optional(),
59423
- /** Minimum allowed value. Populated live from HA `attributes.min`.
59424
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59425
- min: number().optional(),
59426
- /** Maximum allowed value. Populated live from HA `attributes.max`.
59427
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59428
- max: number().optional(),
59429
- /** Step increment. Populated live from HA `attributes.step`.
59430
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59431
- step: number().optional(),
59432
- /** Suggested decimal places for numeric display. Populated live from HA
59433
- * `attributes.suggested_display_precision`, or derived from `step` when the
59434
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
59435
- * other kinds. Falls back to auto-formatting when absent. */
59436
- precision: number().int().min(0).max(10).optional(),
59437
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
59438
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
59439
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
59440
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
59441
- * `has_date`/`has_time` attributes on each push. */
59442
- format: _enum([
59443
- "date",
59444
- "time",
59445
- "datetime"
59446
- ]).optional()
60167
+ /** Minimum allowed value, in the device quantity's base unit. */
60168
+ min: number().optional().meta(readingMeta("device")),
60169
+ /** Maximum allowed value, in the device quantity's base unit. */
60170
+ max: number().optional().meta(readingMeta("device")),
60171
+ /** Step increment, in the device quantity's base unit. */
60172
+ step: number().optional().meta(readingMeta("device"))
59447
60173
  });
59448
60174
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
59449
60175
  object({
@@ -59509,9 +60235,9 @@ var CoverStatusSchema = object({
59509
60235
  ]),
59510
60236
  /** 0 = fully closed, 100 = fully open. Null when the device has
59511
60237
  * no intermediate position surface. */
59512
- position: number().min(0).max(100).nullable(),
60238
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59513
60239
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
59514
- tiltPosition: number().min(0).max(100).nullable(),
60240
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59515
60241
  /** Ms epoch when the slice was last updated. */
59516
60242
  lastChangedAt: number()
59517
60243
  });
@@ -59614,9 +60340,9 @@ var NormalizedRangeSchema$1 = object({
59614
60340
  var DayNightStatusSchema = object({
59615
60341
  mode: DayNightModeSchema,
59616
60342
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
59617
- sensitivity: number().optional(),
60343
+ sensitivity: number().optional().meta(readingMeta("none")),
59618
60344
  /** Delay before the IR-cut filter flips, in seconds. */
59619
- switchDelaySec: number().optional(),
60345
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
59620
60346
  lastFetchedAt: number()
59621
60347
  });
59622
60348
  /**
@@ -59834,11 +60560,11 @@ var DishwasherStatusSchema = object({
59834
60560
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59835
60561
  availablePrograms: array(string()),
59836
60562
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59837
- remainingSeconds: number().int().nonnegative().nullable(),
60563
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59838
60564
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59839
- delayedStartSeconds: number().int().nonnegative().nullable(),
60565
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59840
60566
  /** Cycle completion 0..100, or null when not applicable. */
59841
- progressPercent: number().min(0).max(100).nullable(),
60567
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59842
60568
  /** True when the door is reported open; null when the door state is not known. */
59843
60569
  doorOpen: boolean().nullable(),
59844
60570
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59862,7 +60588,7 @@ var DishwasherStatusSchema = object({
59862
60588
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59863
60589
  machineCareDue: boolean().nullable(),
59864
60590
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59865
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
60591
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59866
60592
  /** Option states: null when the appliance does not expose the option or has not said. */
59867
60593
  halfLoad: boolean().nullable(),
59868
60594
  hygienePlus: boolean().nullable(),
@@ -59877,7 +60603,7 @@ var DishwasherStatusSchema = object({
59877
60603
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59878
60604
  activeEvents: array(string()),
59879
60605
  /** Numeric error code when the appliance reports one; null otherwise. */
59880
- errorCode: number().nullable(),
60606
+ errorCode: number().nullable().meta(readingMeta("none")),
59881
60607
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59882
60608
  errorLabel: string().nullable(),
59883
60609
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -60108,7 +60834,7 @@ var DoorbellStatusSchema = object({
60108
60834
  /** Ms epoch of the last press. null = never observed since this provider started. */
60109
60835
  lastPressedAt: number().nullable(),
60110
60836
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
60111
- pressCountSinceStart: number()
60837
+ pressCountSinceStart: number().meta(readingMeta("count"))
60112
60838
  });
60113
60839
  var DoorbellPressEventSchema = object({
60114
60840
  deviceId: number(),
@@ -60173,17 +60899,7 @@ onPressed: {
60173
60899
  history: "none",
60174
60900
  customize: doorbellCustomize
60175
60901
  };
60176
- /**
60177
- * Enum-state sensor — a string value picked from a finite option set.
60178
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
60179
- * states, weather conditions, contact-source channels, mode strings).
60180
- * The option set is stable across observations; it is persisted in the
60181
- * device config blob (via the control slice at adoption) and is NOT
60182
- * stored in `sourceInfo`.
60183
- */
60184
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
60185
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
60186
- var EnumSensorDateTimeFormatSchema = _enum([
60902
+ _enum([
60187
60903
  "date",
60188
60904
  "time",
60189
60905
  "datetime"
@@ -60191,12 +60907,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
60191
60907
  var EnumSensorStatusSchema = object({
60192
60908
  value: string(),
60193
60909
  /**
60194
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
60195
- * as a locale date/time/datetime; absent for plain enum sensors. HA
60196
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
60197
- * a time-only sensor → `'time'`.
60910
+ * Set when `value` is a timestamp rather than an enum state, so history
60911
+ * does not learn ISO strings as states. The display shape (date / time /
60912
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
60198
60913
  */
60199
- format: EnumSensorDateTimeFormatSchema.optional(),
60914
+ timestamp: boolean().optional(),
60200
60915
  /** Ms epoch when the slice was last updated. */
60201
60916
  lastFetchedAt: number()
60202
60917
  });
@@ -60223,11 +60938,11 @@ var enumSensorCapability = {
60223
60938
  * History (D850): the value as an `enum` series (an appliance's program or
60224
60939
  * operation state, an HVAC action). The option set is per device, so the
60225
60940
  * value list is LEARNED as values are first seen (bounded). A sensor with a
60226
- * `format` carries a timestamp, not a state, and is not recorded.
60941
+ * `timestamp` carries a clock, not a state, and is not recorded.
60227
60942
  */
60228
60943
  history: { fields: { value: {
60229
60944
  kind: "enum",
60230
- skipWhenSet: "format"
60945
+ skipWhenSet: "timestamp"
60231
60946
  } } },
60232
60947
  /** Clock fields: written, but excluded from the compare that decides
60233
60948
  * whether persisting is worth a SQLite commit. */
@@ -60247,13 +60962,13 @@ var EventFireSchema = object({
60247
60962
  deviceId: number(),
60248
60963
  eventType: string(),
60249
60964
  data: record(string(), unknown()).nullable(),
60250
- timestamp: number(),
60251
- seq: number()
60965
+ timestamp: number().meta(readingMeta("none")),
60966
+ seq: number().meta(readingMeta("count"))
60252
60967
  });
60253
60968
  var EventEmitterStatusSchema = object({
60254
60969
  eventTypes: array(string()),
60255
60970
  lastEvent: EventFireSchema.nullable(),
60256
- eventCountSinceStart: number()
60971
+ eventCountSinceStart: number().meta(readingMeta("count"))
60257
60972
  });
60258
60973
  var eventEmitterCapability = {
60259
60974
  name: "event-emitter",
@@ -60666,10 +61381,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60666
61381
  var FanControlStatusSchema = object({
60667
61382
  /** Active speed as 0..100 inclusive. Null when the device has no
60668
61383
  * speed surface (single-speed fan). */
60669
- percentage: number().min(0).max(100).nullable(),
61384
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60670
61385
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60671
61386
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60672
- percentageStep: number().positive().optional(),
61387
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60673
61388
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60674
61389
  * — empty string when no preset is active or supported. */
60675
61390
  preset: string(),
@@ -60790,7 +61505,7 @@ var FeatureProbeStatusSchema = object({
60790
61505
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60791
61506
  model: string().nullable(),
60792
61507
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60793
- channelCount: number().nullable(),
61508
+ channelCount: number().nullable().meta(readingMeta("count")),
60794
61509
  /**
60795
61510
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60796
61511
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60913,9 +61628,151 @@ var gasCapability = {
60913
61628
  * whether persisting is worth a SQLite commit. */
60914
61629
  volatileStateFields: ["lastChangedAt"]
60915
61630
  };
61631
+ var GroupCommandsSchema = object({
61632
+ /** One row per member that has been commanded. A later command replaces that member's row. */
61633
+ results: array(object({
61634
+ deviceId: number().int().nonnegative(),
61635
+ cap: string().min(1),
61636
+ method: string().min(1),
61637
+ ok: boolean(),
61638
+ error: string().optional(),
61639
+ /** Ms epoch of the attempt. */
61640
+ at: number()
61641
+ })) });
61642
+ var groupCommandsCapability = {
61643
+ name: "group-commands",
61644
+ scope: "device",
61645
+ deviceNative: true,
61646
+ mode: "singleton",
61647
+ deviceTypes: [
61648
+ DeviceType.Switch,
61649
+ DeviceType.Light,
61650
+ DeviceType.Fan,
61651
+ DeviceType.Cover,
61652
+ DeviceType.Valve,
61653
+ DeviceType.Lock,
61654
+ DeviceType.MediaPlayer,
61655
+ DeviceType.Thermostat,
61656
+ DeviceType.Climate
61657
+ ],
61658
+ methods: {},
61659
+ status: {
61660
+ schema: GroupCommandsSchema,
61661
+ kind: "push"
61662
+ },
61663
+ runtimeState: GroupCommandsSchema,
61664
+ /**
61665
+ * Runtime-state durability: **session** — the last attempt. A restored
61666
+ * failure would outlive the command that caused it.
61667
+ *
61668
+ * See `RuntimeStateDurability`. Enforced by
61669
+ * `scripts/check-runtime-state-durability.ts`.
61670
+ */
61671
+ durability: "session",
61672
+ history: "none"
61673
+ };
61674
+ /**
61675
+ * Whether a helper actuator group counts as active (D944): at least one
61676
+ * included member is, or every included member is. What "active" means is the
61677
+ * type's: a cover is open, a lock is locked, a media player is playing, a
61678
+ * climate device is not off. A group of lights, switches and fans reports
61679
+ * `switch.on` instead; this cap is for the types whose own state is not a
61680
+ * yes/no. Unknown members are left out (`group-status` counts them) and an
61681
+ * empty included set takes the device offline, never `active: false`.
61682
+ */
61683
+ var GroupActiveSchema = object({
61684
+ active: boolean().meta({ label: "Active" }),
61685
+ /** Ms epoch when the slice was last updated. */
61686
+ lastChangedAt: number()
61687
+ });
61688
+ var groupActiveCapability = {
61689
+ name: "group-active",
61690
+ scope: "device",
61691
+ deviceNative: true,
61692
+ mode: "singleton",
61693
+ deviceTypes: [
61694
+ DeviceType.Cover,
61695
+ DeviceType.Valve,
61696
+ DeviceType.Lock,
61697
+ DeviceType.MediaPlayer,
61698
+ DeviceType.Climate,
61699
+ DeviceType.Thermostat
61700
+ ],
61701
+ methods: {},
61702
+ status: {
61703
+ schema: GroupActiveSchema,
61704
+ kind: "push"
61705
+ },
61706
+ runtimeState: GroupActiveSchema,
61707
+ /**
61708
+ * Runtime-state durability: **session** — recomputed from the members on the
61709
+ * next source event; a restored value would outlive the members it named.
61710
+ *
61711
+ * See `RuntimeStateDurability`. Enforced by
61712
+ * `scripts/check-runtime-state-durability.ts`.
61713
+ */
61714
+ durability: "session",
61715
+ history: "none"
61716
+ };
61717
+ /**
61718
+ * How many members of a helper group answered (D944). Counts are integers,
61719
+ * not measurements: zero included is a real count, never a stand-in reading.
61720
+ * The reading itself stays on the output capability. History is none — the
61721
+ * series that later statistics read is the output cap's own history block.
61722
+ */
61723
+ var GroupStatusSchema = object({
61724
+ /** Members whose current value entered the aggregate. */
61725
+ included: number().int().nonnegative().meta(readingMeta("count", {
61726
+ precision: 0,
61727
+ label: "Reporting"
61728
+ })),
61729
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
61730
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
61731
+ precision: 0,
61732
+ label: "Excluded"
61733
+ })),
61734
+ /** `included` + `excluded`. */
61735
+ total: number().int().nonnegative().meta(readingMeta("count", {
61736
+ precision: 0,
61737
+ label: "Members"
61738
+ }))
61739
+ });
61740
+ var groupStatusCapability = {
61741
+ name: "group-status",
61742
+ scope: "device",
61743
+ deviceNative: true,
61744
+ mode: "singleton",
61745
+ deviceTypes: [
61746
+ DeviceType.Sensor,
61747
+ DeviceType.Switch,
61748
+ DeviceType.Light,
61749
+ DeviceType.Fan,
61750
+ DeviceType.Cover,
61751
+ DeviceType.Valve,
61752
+ DeviceType.Lock,
61753
+ DeviceType.MediaPlayer,
61754
+ DeviceType.Thermostat,
61755
+ DeviceType.Climate
61756
+ ],
61757
+ methods: {},
61758
+ status: {
61759
+ schema: GroupStatusSchema,
61760
+ kind: "push"
61761
+ },
61762
+ runtimeState: GroupStatusSchema,
61763
+ /**
61764
+ * Runtime-state durability: **session** — recomputed from members on the
61765
+ * next source event; a restored count would outlive the readings it named.
61766
+ *
61767
+ * See `RuntimeStateDurability`. Enforced by
61768
+ * `scripts/check-runtime-state-durability.ts`.
61769
+ */
61770
+ durability: "session",
61771
+ history: "none"
61772
+ };
60916
61773
  var HeatingValveStatusSchema = object({
60917
61774
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60918
- position: number().min(0).max(100).nullable(),
61775
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60919
61776
  /** Ms epoch when the slice was last updated (push or poll). */
60920
61777
  lastFetchedAt: number()
60921
61778
  });
@@ -60958,9 +61815,9 @@ var HumidifierStatusSchema = object({
60958
61815
  /** Whether the humidifier is currently on. */
60959
61816
  on: boolean(),
60960
61817
  /** Current measured relative humidity (0..100). Null when not reported. */
60961
- currentHumidity: number().min(0).max(100).nullable(),
61818
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60962
61819
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60963
- targetHumidity: number().min(0).max(100).nullable(),
61820
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60964
61821
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60965
61822
  * device has no mode surface. */
60966
61823
  mode: string().nullable(),
@@ -60970,9 +61827,9 @@ var HumidifierStatusSchema = object({
60970
61827
  * `idle` / `off`). Null when the device doesn't report it. */
60971
61828
  action: string().nullable(),
60972
61829
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60973
- minHumidity: number().nullable(),
61830
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60974
61831
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60975
- maxHumidity: number().nullable(),
61832
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60976
61833
  /** Ms epoch when the slice was last updated. */
60977
61834
  lastChangedAt: number()
60978
61835
  });
@@ -61042,22 +61899,13 @@ var humidifierCapability = {
61042
61899
  * `sourceInfo`.
61043
61900
  */
61044
61901
  var HumiditySensorStatusSchema = object({
61045
- /** Current relative humidity, 0..100. */
61046
- percent: number().min(0).max(100).meta({
61047
- unit: "%",
61902
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
61903
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
61904
+ precision: 0,
61048
61905
  label: "Humidity"
61049
- }),
61906
+ })),
61050
61907
  /** Ms epoch when the slice was last updated. */
61051
- lastFetchedAt: number(),
61052
- /** Live display unit from the upstream source (e.g. HA
61053
- * `attributes.unit_of_measurement`). The UI prefers this over the
61054
- * role's canonical unit. Absent → fall back to the canonical unit. */
61055
- unit: string().optional(),
61056
- /** Suggested decimal places for numeric display.
61057
- * Populated live from the upstream source when provided (e.g. HA
61058
- * `attributes.suggested_display_precision`). Falls back to
61059
- * auto-formatting when absent. */
61060
- precision: number().int().min(0).max(10).optional()
61908
+ lastFetchedAt: number()
61061
61909
  });
61062
61910
  var humiditySensorCapability = {
61063
61911
  name: "humidity-sensor",
@@ -61088,12 +61936,7 @@ var humiditySensorCapability = {
61088
61936
  } } },
61089
61937
  /** Clock fields: written, but excluded from the compare that decides
61090
61938
  * whether persisting is worth a SQLite commit. */
61091
- volatileStateFields: ["lastFetchedAt"],
61092
- customize: {
61093
- version: 1,
61094
- advanced: "derive",
61095
- targets: []
61096
- }
61939
+ volatileStateFields: ["lastFetchedAt"]
61097
61940
  };
61098
61941
  /**
61099
61942
  * Image display cap. Models a single still image exposed by an integration —
@@ -61114,7 +61957,7 @@ var ImageStatusSchema = object({
61114
61957
  * entity exposes no `entity_picture` (yet). */
61115
61958
  url: string().nullable(),
61116
61959
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
61117
- lastUpdated: number().nullable()
61960
+ lastUpdated: number().nullable().meta(readingMeta("none"))
61118
61961
  });
61119
61962
  var imageCapability = {
61120
61963
  name: "image",
@@ -61202,19 +62045,19 @@ var NormalizedRangeSchema = object({
61202
62045
  */
61203
62046
  var ImageSettingsStatusSchema = object({
61204
62047
  /** Normalized 0–100. */
61205
- brightness: number().optional(),
62048
+ brightness: number().optional().meta(readingMeta("none")),
61206
62049
  /** Normalized 0–100. */
61207
- contrast: number().optional(),
62050
+ contrast: number().optional().meta(readingMeta("none")),
61208
62051
  /** Normalized 0–100. */
61209
- saturation: number().optional(),
62052
+ saturation: number().optional().meta(readingMeta("none")),
61210
62053
  /** Normalized 0–100. */
61211
- sharpness: number().optional(),
62054
+ sharpness: number().optional().meta(readingMeta("none")),
61212
62055
  mirror: boolean().optional(),
61213
62056
  flip: boolean().optional(),
61214
62057
  rotate: ImageRotateSchema.optional(),
61215
62058
  whiteBalance: WhiteBalanceModeSchema.optional(),
61216
62059
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
61217
- warmth: number().optional(),
62060
+ warmth: number().optional().meta(readingMeta("none")),
61218
62061
  exposureMode: ExposureModeSchema.optional(),
61219
62062
  backlightMode: BacklightModeSchema.optional(),
61220
62063
  lastFetchedAt: number()
@@ -61493,10 +62336,10 @@ var IntercomAbilitySchema = object({
61493
62336
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
61494
62337
  codecs: array(string()),
61495
62338
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
61496
- sampleRate: number(),
62339
+ sampleRate: number().meta(readingMeta("none")),
61497
62340
  duplex: _enum(["half", "full"]),
61498
62341
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
61499
- maxBacklogMs: number()
62342
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
61500
62343
  });
61501
62344
  var IntercomStatusSchema = object({
61502
62345
  /** True while a talk session is open. */
@@ -61723,13 +62566,13 @@ var LawnMowerControlStatusSchema = object({
61723
62566
  activity: LawnMowerActivitySchema,
61724
62567
  /** 0..100 battery percentage. Null when the device has no battery
61725
62568
  * reading. */
61726
- batteryLevel: number().min(0).max(100).nullable(),
62569
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61727
62570
  /** 0..100 mowing-completion percentage of the current task, or null when no
61728
62571
  * task is active / progress is unavailable. */
61729
- progressPercent: number().min(0).max(100).nullable(),
62572
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61730
62573
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61731
62574
  * or null when unknown. */
61732
- currentCode: number().nullable(),
62575
+ currentCode: number().nullable().meta(readingMeta("none")),
61733
62576
  /** Human label for {@link currentCode}, or null when undecodable. */
61734
62577
  currentCodeLabel: string().nullable(),
61735
62578
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -62182,7 +63025,14 @@ var lockControlCapability = {
62182
63025
  }),
62183
63026
  unlock: method(object({
62184
63027
  deviceId: number().int().nonnegative(),
62185
- code: string().min(1).optional()
63028
+ code: string().min(1).optional(),
63029
+ /**
63030
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
63031
+ * with one call is refused unless the caller says it was confirmed. A
63032
+ * single lock ignores it. The refusal lives in the callee, so no
63033
+ * automation or API caller reaches the doors by skipping a dialog.
63034
+ */
63035
+ confirm: boolean().optional()
62186
63036
  }), _void(), {
62187
63037
  kind: "mutation",
62188
63038
  auth: "protected"
@@ -62219,6 +63069,44 @@ var lockControlCapability = {
62219
63069
  volatileStateFields: ["lastChangedAt"]
62220
63070
  };
62221
63071
  /**
63072
+ * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
63073
+ * so dashboards keep one series; every other instantaneous reading lands here.
63074
+ * `value` is the time integral in the source quantity's accumulated base
63075
+ * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
63076
+ */
63077
+ var MeterStatusSchema = object({
63078
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
63079
+ lastFetchedAt: number()
63080
+ });
63081
+ var meterCapability = {
63082
+ name: "meter",
63083
+ scope: "device",
63084
+ deviceNative: true,
63085
+ mode: "singleton",
63086
+ deviceTypes: [DeviceType.Sensor],
63087
+ methods: {},
63088
+ status: {
63089
+ schema: MeterStatusSchema,
63090
+ kind: "push"
63091
+ },
63092
+ runtimeState: MeterStatusSchema,
63093
+ /**
63094
+ * Runtime-state durability: **restored** — the total is a cache of the
63095
+ * history integral. A restart shows it only until the history pass
63096
+ * recomputes the open period; it is not an in-memory buffer.
63097
+ *
63098
+ * See `RuntimeStateDurability`. Enforced by
63099
+ * `scripts/check-runtime-state-durability.ts`.
63100
+ */
63101
+ durability: "restored",
63102
+ history: { fields: { value: {
63103
+ kind: "counter",
63104
+ deadband: .001,
63105
+ minIntervalMs: 6e4
63106
+ } } },
63107
+ volatileStateFields: ["lastFetchedAt"]
63108
+ };
63109
+ /**
62222
63110
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
62223
63111
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
62224
63112
  * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
@@ -62262,7 +63150,7 @@ var MediaPlayerStatusSchema = object({
62262
63150
  state: MediaPlayerStateSchema,
62263
63151
  /** Volume as 0..100 inclusive. Null when the device has no volume
62264
63152
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
62265
- volumeLevel: number().min(0).max(100).nullable(),
63153
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62266
63154
  /** Mute toggle distinct from volume=0. Null when no mute surface.
62267
63155
  * Pair with `DeviceFeature.MediaPlayerMute`. */
62268
63156
  isMuted: boolean().nullable(),
@@ -62275,10 +63163,10 @@ var MediaPlayerStatusSchema = object({
62275
63163
  currentMedia: MediaInfoSchema.nullable(),
62276
63164
  /** Current playback position in ms. Null when not seekable or
62277
63165
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
62278
- positionMs: number().int().nonnegative().nullable(),
63166
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62279
63167
  /** Total duration of the current media in ms. Null when unknown
62280
63168
  * (live stream) or nothing is playing. */
62281
- durationMs: number().int().nonnegative().nullable(),
63169
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62282
63170
  /** Shuffle toggle. Null when no shuffle surface.
62283
63171
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
62284
63172
  shuffle: boolean().nullable(),
@@ -62587,7 +63475,7 @@ var motionCustomize = {
62587
63475
  eventName: "onMotionChanged"
62588
63476
  },
62589
63477
  deviceTypes: [DeviceType.Camera],
62590
- apply: "replace",
63478
+ apply: "add",
62591
63479
  applyAuthority: {
62592
63480
  kind: "camera-sensors",
62593
63481
  timeoutField: "motionCooldownMs"
@@ -62659,7 +63547,7 @@ var MotionStatusSchema = object({
62659
63547
  * it, every provider seeded it at boot, and every session left it
62660
63548
  * `detected: true` after its detach.
62661
63549
  */
62662
- autoClearAfterMs: number().nullable()
63550
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62663
63551
  });
62664
63552
  /**
62665
63553
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62696,12 +63584,18 @@ var motionCapability = {
62696
63584
  methods: {
62697
63585
  /**
62698
63586
  * Pull the current motion state synchronously. Convenience shortcut
62699
- * for consumers that don't need the full status object; equivalent
62700
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62701
- * `getStatus` once the auto-injected status surface lands in every
62702
- * consumer.
63587
+ * for consumers that don't need the full status object. A composed
63588
+ * group answers from its slice (D944): unknown throws, it is not off.
62703
63589
  */
62704
63590
  isDetected: method(object({ deviceId: number() }), boolean()) },
63591
+ /**
63592
+ * A composed motion group serves `isDetected` from `detected` (D944).
63593
+ * Without this the cap stays uncomposable (`has-commands`).
63594
+ */
63595
+ composedMethods: { isDetected: {
63596
+ kind: "slice",
63597
+ field: "detected"
63598
+ } },
62705
63599
  events: {
62706
63600
  /**
62707
63601
  * Fires every time the runner transitions a camera between
@@ -62841,7 +63735,7 @@ var MotionZoneRegionSchema = object({
62841
63735
  * the grid region(s). */
62842
63736
  var MotionZoneStatusSchema = object({
62843
63737
  enabled: boolean(),
62844
- sensitivity: number(),
63738
+ sensitivity: number().meta(readingMeta("none")),
62845
63739
  /** Grid region(s). Today exactly one `grid` shape. */
62846
63740
  regions: array(MotionZoneRegionSchema),
62847
63741
  lastFetchedAt: number()
@@ -63211,7 +64105,7 @@ var NavigationStatusSchema = object({
63211
64105
  following: boolean(),
63212
64106
  flash: boolean(),
63213
64107
  lightMode: NavigationLightModeSchema,
63214
- lightLevel: number().min(40).max(100),
64108
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
63215
64109
  /** Ms epoch when the slice was last updated. */
63216
64110
  lastChangedAt: number()
63217
64111
  });
@@ -63345,13 +64239,13 @@ var NetworkLinkStatusSchema = object({
63345
64239
  * one whose reading has not landed must not be drawn at 0 %. Consumers
63346
64240
  * SKIP a null rather than coerce it.
63347
64241
  */
63348
- signalPercent: number().min(0).max(100).nullable(),
64242
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63349
64243
  /** Raw received signal strength in dBm, when the firmware reports one. */
63350
- rssiDbm: number().optional(),
64244
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
63351
64245
  /** Network name of a wireless link, when the firmware reports it. */
63352
64246
  ssid: string().optional(),
63353
64247
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
63354
- lastUpdated: number()
64248
+ lastUpdated: number().meta(readingMeta("none"))
63355
64249
  });
63356
64250
  /** The slice a provider seeds before its first read: nothing is known yet. */
63357
64251
  var NETWORK_LINK_UNKNOWN = {
@@ -63756,7 +64650,7 @@ var NotifierStatusSchema = object({
63756
64650
  lastError: string().nullable(),
63757
64651
  /** Number of deliveries currently buffered server-side (rate-limited
63758
64652
  * notifiers may queue). 0 when the provider sends synchronously. */
63759
- queueDepth: number().int().nonnegative(),
64653
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63760
64654
  /** Per-feature capability matrix — authoritative source for the
63761
64655
  * compose-form field gating. */
63762
64656
  supports: NotifierSupportsSchema
@@ -63848,11 +64742,9 @@ onSent: { data: object({
63848
64742
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63849
64743
  * for: water flow, distance, weight, frequency, signal strength, …).
63850
64744
  *
63851
- * The `unit` and `precision` fields carry the live display metadata that
63852
- * the upstream source provides with each state push (HA
63853
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63854
- * The UI reads them directly from the slice — they are the single source
63855
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
64745
+ * The quantity is not on this slice. It is chosen when the device is
64746
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
64747
+ * that quantity's base unit. `null` is a refused sample (D393).
63856
64748
  *
63857
64749
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63858
64750
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63861,15 +64753,8 @@ onSent: { data: object({
63861
64753
  * numeric values land here.
63862
64754
  */
63863
64755
  var NumericSensorStatusSchema = object({
63864
- value: number(),
63865
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63866
- * Populated live from the upstream source on each state push.
63867
- * Absent for unitless sensors. */
63868
- unit: string().optional(),
63869
- /** Suggested decimal places for numeric display.
63870
- * Populated live from the upstream source when provided.
63871
- * Falls back to auto-formatting when absent. */
63872
- precision: number().int().min(0).max(10).optional(),
64756
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
64757
+ value: number().nullable().meta(readingMeta("device")),
63873
64758
  /** Ms epoch when the slice was last updated. */
63874
64759
  lastFetchedAt: number()
63875
64760
  });
@@ -63893,11 +64778,11 @@ var numericSensorCapability = {
63893
64778
  */
63894
64779
  durability: "restored",
63895
64780
  /**
63896
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
64781
+ * History (D850): recorded series. The unit is the device quantity's base,
64782
+ * resolved when the series is read — the cap has no single unit.
63897
64783
  */
63898
64784
  history: { fields: { value: {
63899
64785
  kind: "gauge",
63900
- unitFrom: "unit",
63901
64786
  minIntervalMs: 3e4
63902
64787
  } } },
63903
64788
  /** Clock fields: written, but excluded from the compare that decides
@@ -64235,23 +65120,26 @@ var PetFeederStatusSchema = object({
64235
65120
  /** Food currently in the bowl (grams). Null when the device has not
64236
65121
  * reported a reading yet. On dual-hopper models this is the combined
64237
65122
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
64238
- foodLevel: number().nullable(),
65123
+ foodLevel: number().nullable().meta(readingMeta("none")),
64239
65124
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
64240
65125
  * single-hopper models. */
64241
- food1: number().nullable(),
65126
+ food1: number().nullable().meta(readingMeta("none")),
64242
65127
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
64243
65128
  * single-hopper models. */
64244
- food2: number().nullable(),
65129
+ food2: number().nullable().meta(readingMeta("none")),
64245
65130
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
64246
65131
  * (`device_class: problem`, on = low). True when the bowl is empty /
64247
65132
  * below the feeder's low threshold. */
64248
65133
  lowFood: boolean(),
64249
65134
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
64250
65135
  * device has no battery reading. */
64251
- batteryPower: number().min(0).max(100).nullable(),
64252
- /** Days of desiccant life remaining. Null when the model has no
64253
- * desiccant sensor. */
64254
- desiccantLeftDays: number().nullable(),
65136
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65137
+ /**
65138
+ * Desiccant life remaining, stored in seconds (duration base, D944).
65139
+ * Providers convert from the days the feeder reports. Null when the
65140
+ * model has no desiccant sensor.
65141
+ */
65142
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
64255
65143
  /** True while a feed is in progress. */
64256
65144
  feeding: boolean(),
64257
65145
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -64262,7 +65150,7 @@ var PetFeederStatusSchema = object({
64262
65150
  * with `errorCode` for consumers that want the raw integer. */
64263
65151
  error: string().nullable(),
64264
65152
  /** Raw device error code (0 / null = no error). */
64265
- errorCode: number().nullable(),
65153
+ errorCode: number().nullable().meta(readingMeta("none")),
64266
65154
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
64267
65155
  isDualHopper: boolean(),
64268
65156
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -64272,7 +65160,7 @@ var PetFeederStatusSchema = object({
64272
65160
  /** Play a chime when dispensing. */
64273
65161
  feedSound: boolean(),
64274
65162
  /** Speaker / prompt volume level (device-scaled integer). */
64275
- volume: number(),
65163
+ volume: number().meta(readingMeta("none")),
64276
65164
  /** Ms epoch when the slice was last refreshed from the cloud. */
64277
65165
  lastFetchedAt: number()
64278
65166
  });
@@ -64391,7 +65279,7 @@ var petFeederCapability = {
64391
65279
  },
64392
65280
  desiccantLeftDays: {
64393
65281
  kind: "gauge",
64394
- unit: "days"
65282
+ unit: "s"
64395
65283
  }
64396
65284
  } }
64397
65285
  };
@@ -64792,43 +65680,88 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64792
65680
  */
64793
65681
  var PowerMeterStatusSchema = object({
64794
65682
  /** Instantaneous power draw in watts. */
64795
- watts: number().optional().meta({
64796
- unit: "W",
64797
- label: "Power"
64798
- }),
65683
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64799
65684
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64800
- kwhTotal: number().optional().meta({
64801
- unit: "kWh",
64802
- label: "Energy"
64803
- }),
65685
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64804
65686
  /** Voltage in volts. */
64805
- volts: number().optional(),
65687
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
64806
65688
  /** Current in amperes. */
64807
- amps: number().optional(),
64808
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64809
- va: number().optional(),
65689
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
65690
+ /** Apparent power in volt-amperes. */
65691
+ va: number().optional().meta(readingMeta("apparent-power")),
64810
65692
  /** Power factor (0..1) if reported. */
64811
- powerFactor: number().min(0).max(1).optional(),
65693
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64812
65694
  /** Ms epoch when the slice was last updated. */
64813
- lastFetchedAt: number(),
64814
- /** Live display unit of the single metric this slice carries (e.g. HA
64815
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64816
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64817
- * unit per slice is unambiguous. The UI prefers this over the role's
64818
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64819
- unit: string().optional(),
64820
- /** Suggested decimal places for numeric display.
64821
- * Populated live from the upstream source when provided (e.g. HA
64822
- * `attributes.suggested_display_precision`). Falls back to
64823
- * auto-formatting when absent. */
64824
- precision: number().int().min(0).max(10).optional()
65695
+ lastFetchedAt: number()
64825
65696
  });
65697
+ /**
65698
+ * Loads that draw metered mains power (D951). The reading is a feature of the
65699
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
65700
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
65701
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
65702
+ * camera and the like are left out: their draw is not what anyone meters.
65703
+ */
65704
+ var POWER_METER_DEVICE_TYPES = [
65705
+ DeviceType.Sensor,
65706
+ DeviceType.Switch,
65707
+ DeviceType.Light,
65708
+ DeviceType.Fan,
65709
+ DeviceType.Climate,
65710
+ DeviceType.Thermostat,
65711
+ DeviceType.Humidifier,
65712
+ DeviceType.WaterHeater,
65713
+ DeviceType.MediaPlayer,
65714
+ DeviceType.Washer,
65715
+ DeviceType.Dryer,
65716
+ DeviceType.Dishwasher,
65717
+ DeviceType.Vacuum,
65718
+ DeviceType.LawnMower,
65719
+ DeviceType.PetFeeder
65720
+ ];
65721
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
65722
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
65723
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
65724
+ function powerTarget(id, unit) {
65725
+ return {
65726
+ id,
65727
+ cardinality: "scalar",
65728
+ vocabulary: {
65729
+ cap: "power-meter",
65730
+ stateField: id
65731
+ },
65732
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
65733
+ apply: "add",
65734
+ path: id,
65735
+ sources: [{
65736
+ kind: "numeric",
65737
+ requiredUnit: unit,
65738
+ transforms: [{ kind: "identity" }]
65739
+ }],
65740
+ companions: [{
65741
+ kind: "clock",
65742
+ path: "lastFetchedAt",
65743
+ when: "always"
65744
+ }],
65745
+ sourceReport: { status: "assignment-status" },
65746
+ advancedExpression: true
65747
+ };
65748
+ }
65749
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
65750
+ var powerMeterCustomize = {
65751
+ version: 1,
65752
+ targets: [
65753
+ powerTarget("watts", "W"),
65754
+ powerTarget("kwhTotal", "kWh"),
65755
+ powerTarget("volts", "V"),
65756
+ powerTarget("amps", "A")
65757
+ ]
65758
+ };
64826
65759
  var powerMeterCapability = {
64827
65760
  name: "power-meter",
64828
65761
  scope: "device",
64829
65762
  deviceNative: true,
64830
65763
  mode: "singleton",
64831
- deviceTypes: [DeviceType.Sensor],
65764
+ deviceTypes: POWER_METER_DEVICE_TYPES,
64832
65765
  methods: {},
64833
65766
  status: {
64834
65767
  schema: PowerMeterStatusSchema,
@@ -64886,11 +65819,7 @@ var powerMeterCapability = {
64886
65819
  /** Clock fields: written, but excluded from the compare that decides
64887
65820
  * whether persisting is worth a SQLite commit. */
64888
65821
  volatileStateFields: ["lastFetchedAt"],
64889
- customize: {
64890
- version: 1,
64891
- advanced: "derive",
64892
- targets: []
64893
- }
65822
+ customize: powerMeterCustomize
64894
65823
  };
64895
65824
  /**
64896
65825
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64910,11 +65839,11 @@ var powerMeterCapability = {
64910
65839
  */
64911
65840
  var GpsLocationSchema = object({
64912
65841
  /** Latitude in decimal degrees, -90..90. */
64913
- latitude: number().min(-90).max(90),
65842
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64914
65843
  /** Longitude in decimal degrees, -180..180. */
64915
- longitude: number().min(-180).max(180),
65844
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64916
65845
  /** Reported accuracy in meters (lower = better). */
64917
- accuracyMeters: number().nonnegative()
65846
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64918
65847
  });
64919
65848
  var PresenceStatusSchema = object({
64920
65849
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64926,7 +65855,7 @@ var PresenceStatusSchema = object({
64926
65855
  gps: GpsLocationSchema.nullable(),
64927
65856
  /** Optional battery level of the tracking device (0..100). Null
64928
65857
  * when not reported. */
64929
- batteryPercent: number().min(0).max(100).nullable(),
65858
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64930
65859
  /** Ms epoch when the slice was last updated. */
64931
65860
  lastChangedAt: number()
64932
65861
  });
@@ -64976,19 +65905,10 @@ var presenceCapability = {
64976
65905
  * `sourceInfo`.
64977
65906
  */
64978
65907
  var PressureSensorStatusSchema = object({
64979
- /** Current pressure in hPa. */
64980
- hpa: number(),
65908
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
65909
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64981
65910
  /** Ms epoch when the slice was last updated. */
64982
- lastFetchedAt: number(),
64983
- /** Live display unit from the upstream source (e.g. HA
64984
- * `attributes.unit_of_measurement`). The UI prefers this over the
64985
- * role's canonical unit. Absent → fall back to the canonical unit. */
64986
- unit: string().optional(),
64987
- /** Suggested decimal places for numeric display.
64988
- * Populated live from the upstream source when provided (e.g. HA
64989
- * `attributes.suggested_display_precision`). Falls back to
64990
- * auto-formatting when absent. */
64991
- precision: number().int().min(0).max(10).optional()
65911
+ lastFetchedAt: number()
64992
65912
  });
64993
65913
  var pressureSensorCapability = {
64994
65914
  name: "pressure-sensor",
@@ -65514,8 +66434,8 @@ var ptzCapability = {
65514
66434
  };
65515
66435
  var PtzAutotrackSettingsSchema = object({
65516
66436
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
65517
- stopDelaySeconds: number().int().min(0).max(300),
65518
- disappearDelaySeconds: number().int().min(0).max(300)
66437
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
66438
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
65519
66439
  });
65520
66440
  /**
65521
66441
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65669,7 +66589,7 @@ var RunningStateStatusSchema = object({
65669
66589
  /** null = unknown — never coerced to false (D393). */
65670
66590
  running: boolean().nullable(),
65671
66591
  /** Epoch ms the current boolean began; null while `running` is null. */
65672
- since: number().nullable(),
66592
+ since: number().nullable().meta(readingMeta("none")),
65673
66593
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65674
66594
  phase: string().nullable(),
65675
66595
  /** null until a reading has been published (D393). */
@@ -65684,7 +66604,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65684
66604
  /** Generic Customize target for the running boolean (D936). */
65685
66605
  var runningCustomize = {
65686
66606
  version: 1,
65687
- advanced: "derive",
65688
66607
  targets: [{
65689
66608
  id: "running",
65690
66609
  cardinality: "scalar",
@@ -65693,6 +66612,8 @@ var runningCustomize = {
65693
66612
  stateField: "running"
65694
66613
  },
65695
66614
  deviceTypes: [
66615
+ DeviceType.Washer,
66616
+ DeviceType.Dryer,
65696
66617
  DeviceType.Dishwasher,
65697
66618
  DeviceType.Vacuum,
65698
66619
  DeviceType.LawnMower,
@@ -65704,7 +66625,7 @@ var runningCustomize = {
65704
66625
  DeviceType.Thermostat,
65705
66626
  DeviceType.PetFeeder
65706
66627
  ],
65707
- apply: "add-or-replace",
66628
+ apply: "add",
65708
66629
  path: "running",
65709
66630
  sources: [
65710
66631
  {
@@ -66997,9 +67918,9 @@ var RecordTriggerSchema = _enum([
66997
67918
  */
66998
67919
  var RecordWindowSchema = object({
66999
67920
  trigger: RecordTriggerSchema,
67000
- day: number().int().min(0).max(6),
67001
- startMinute: number().int().min(0).max(1439),
67002
- endMinute: number().int().min(1).max(1440)
67921
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
67922
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
67923
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
67003
67924
  });
67004
67925
  /** Status of one physical volume, as the camera itself describes it. */
67005
67926
  var OnboardStorageVolumeSchema = object({
@@ -67075,10 +67996,10 @@ var RecordingOnboardStatusSchema = object({
67075
67996
  /** Master "record to the card at all" switch. */
67076
67997
  enabled: boolean().nullable(),
67077
67998
  overwriteWhenFull: boolean().nullable(),
67078
- preRecordSec: number().nullable(),
67079
- postRecordSec: number().nullable(),
67999
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
68000
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
67080
68001
  /** Length of one recorded file, in minutes. */
67081
- segmentMinutes: number().nullable(),
68002
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
67082
68003
  /** The primary track's weekly windows, flattened. */
67083
68004
  windows: array(RecordWindowSchema),
67084
68005
  /**
@@ -67094,7 +68015,7 @@ var RecordingOnboardStatusSchema = object({
67094
68015
  * provider reporting a non-zero count also reports the schedule as not
67095
68016
  * writable.
67096
68017
  */
67097
- unreadableWindows: number(),
68018
+ unreadableWindows: number().meta(readingMeta("count")),
67098
68019
  /**
67099
68020
  * The camera is scheduled to record and has NO usable storage.
67100
68021
  *
@@ -68104,7 +69025,7 @@ var CamStreamDescriptorSchema = object({
68104
69025
  url: string().optional(),
68105
69026
  codec: string().optional(),
68106
69027
  resolution: CamStreamResolutionSchema.optional(),
68107
- fps: number().positive().optional(),
69028
+ fps: number().positive().optional().meta(readingMeta("rate")),
68108
69029
  label: string().optional(),
68109
69030
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
68110
69031
  deviceFeatures: array(string()).optional(),
@@ -68191,18 +69112,18 @@ var StreamProfileSchema = _enum([
68191
69112
  ]);
68192
69113
  /** The current encoding config of one stream profile. */
68193
69114
  var StreamProfileConfigSchema = object({
68194
- width: number(),
68195
- height: number(),
69115
+ width: number().meta(readingMeta("none")),
69116
+ height: number().meta(readingMeta("none")),
68196
69117
  codec: _enum(["h264", "h265"]),
68197
- framerate: number(),
68198
- bitrate: number(),
69118
+ framerate: number().meta(readingMeta("rate")),
69119
+ bitrate: number().meta(readingMeta("bitrate")),
68199
69120
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
68200
69121
  encoderProfile: _enum([
68201
69122
  "high",
68202
69123
  "main",
68203
69124
  "baseline"
68204
69125
  ]).optional(),
68205
- gop: number().optional(),
69126
+ gop: number().optional().meta(readingMeta("none")),
68206
69127
  /**
68207
69128
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
68208
69129
  *
@@ -68574,7 +69495,8 @@ var switchCapability = {
68574
69495
  deviceTypes: [
68575
69496
  DeviceType.Switch,
68576
69497
  DeviceType.Siren,
68577
- DeviceType.Light
69498
+ DeviceType.Light,
69499
+ DeviceType.Fan
68578
69500
  ],
68579
69501
  methods: { setState: method(object({
68580
69502
  deviceId: number(),
@@ -68621,12 +69543,7 @@ var switchCapability = {
68621
69543
  label: "On",
68622
69544
  kind: "boolean"
68623
69545
  }
68624
- }] },
68625
- customize: {
68626
- version: 1,
68627
- advanced: "derive",
68628
- targets: []
68629
- }
69546
+ }] }
68630
69547
  };
68631
69548
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
68632
69549
  function hasControlCharacter(value) {
@@ -69209,35 +70126,21 @@ var tamperCapability = {
69209
70126
  * entries with `device_class: temperature` and any future native
69210
70127
  * thermometer.
69211
70128
  *
69212
- * Unit normalisation: providers convert to Celsius before storing.
69213
- * The slice value is always Celsius so cross-cap aggregators
69214
- * (climate-control's `currentTemp`, energy analytics) can compose
69215
- * without per-source unit fixups. The canonical display unit (`°C`) is
69216
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
69217
- * `sourceInfo`.
70129
+ * The number is Celsius (D944). Providers convert on ingest. A failed
70130
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
69218
70131
  *
69219
70132
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
69220
70133
  * frozen feed (provider hung) distinct from a "temperature hasn't
69221
70134
  * changed" steady state.
69222
70135
  */
69223
70136
  var TemperatureSensorStatusSchema = object({
69224
- /** Current temperature in Celsius. */
69225
- celsius: number().meta({
69226
- unit: "°C",
70137
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
70138
+ celsius: number().nullable().meta(readingMeta("temperature", {
70139
+ precision: 1,
69227
70140
  label: "Temperature"
69228
- }),
70141
+ })),
69229
70142
  /** Ms epoch when the slice was last updated (push or poll). */
69230
- lastFetchedAt: number(),
69231
- /** Live display unit from the upstream source (e.g. HA
69232
- * `attributes.unit_of_measurement`). The UI prefers this over the
69233
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
69234
- * Absent → the UI falls back to the role's canonical unit. */
69235
- unit: string().optional(),
69236
- /** Suggested decimal places for numeric display.
69237
- * Populated live from the upstream source when provided (e.g. HA
69238
- * `attributes.suggested_display_precision`). Falls back to
69239
- * auto-formatting when absent. */
69240
- precision: number().int().min(0).max(10).optional()
70143
+ lastFetchedAt: number()
69241
70144
  });
69242
70145
  var temperatureSensorCapability = {
69243
70146
  name: "temperature-sensor",
@@ -69268,12 +70171,7 @@ var temperatureSensorCapability = {
69268
70171
  } } },
69269
70172
  /** Clock fields: written, but excluded from the compare that decides
69270
70173
  * whether persisting is worth a SQLite commit. */
69271
- volatileStateFields: ["lastFetchedAt"],
69272
- customize: {
69273
- version: 1,
69274
- advanced: "derive",
69275
- targets: []
69276
- }
70174
+ volatileStateFields: ["lastFetchedAt"]
69277
70175
  };
69278
70176
  /**
69279
70177
  * toast — system-scoped singleton capability that streams toast
@@ -69556,16 +70454,16 @@ var VacuumControlStatusSchema = object({
69556
70454
  ]),
69557
70455
  /** 0..100 battery percentage. Null when the device has no battery
69558
70456
  * reading. */
69559
- batteryLevel: number().min(0).max(100).nullable(),
70457
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69560
70458
  /** Current fan-speed token (provider-verbatim). Null when unknown or
69561
70459
  * the vacuum has no speed control. */
69562
70460
  fanSpeed: string().nullable(),
69563
70461
  /** Speed tokens the hardware accepts — drives the UI selector. */
69564
70462
  availableFanSpeeds: array(string()),
69565
70463
  /** 0..100 cleaning-completion percentage of the current task, or null. */
69566
- progressPercent: number().min(0).max(100).nullable(),
70464
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69567
70465
  /** Current error code (0 / null = no error). */
69568
- errorCode: number().nullable(),
70466
+ errorCode: number().nullable().meta(readingMeta("none")),
69569
70467
  /** Human label for {@link errorCode}, or null when none / undecodable. */
69570
70468
  errorLabel: string().nullable(),
69571
70469
  /** Ms epoch when the slice was last updated. */
@@ -69643,7 +70541,7 @@ var ValveStatusSchema = object({
69643
70541
  ]),
69644
70542
  /** 0 = fully closed, 100 = fully open. Null when the device has no
69645
70543
  * intermediate position surface. */
69646
- position: number().min(0).max(100).nullable(),
70544
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69647
70545
  /** Ms epoch when the slice was last updated. */
69648
70546
  lastChangedAt: number()
69649
70547
  });
@@ -69751,9 +70649,9 @@ var vibrationCapability = {
69751
70649
  */
69752
70650
  var WaterHeaterStatusSchema = object({
69753
70651
  /** Current measured temperature. Null when not reported. */
69754
- currentTemp: number().nullable(),
70652
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
69755
70653
  /** Target temperature setpoint. Null when no setpoint surface. */
69756
- targetTemp: number().nullable(),
70654
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
69757
70655
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
69758
70656
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
69759
70657
  * device reports an unknown state. */
@@ -69764,9 +70662,9 @@ var WaterHeaterStatusSchema = object({
69764
70662
  * has no away surface. */
69765
70663
  away: boolean().nullable(),
69766
70664
  /** HA `min_temp` attribute. Null when not reported. */
69767
- minTemp: number().nullable(),
70665
+ minTemp: number().nullable().meta(readingMeta("temperature")),
69768
70666
  /** HA `max_temp` attribute. Null when not reported. */
69769
- maxTemp: number().nullable(),
70667
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
69770
70668
  /** Ms epoch when the slice was last updated. */
69771
70669
  lastChangedAt: number()
69772
70670
  });
@@ -69851,25 +70749,18 @@ var waterHeaterCapability = {
69851
70749
  * Forecast deferred — current conditions only for v1.
69852
70750
  */
69853
70751
  var WeatherStatusSchema = object({
69854
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69855
- * when no condition has been reported yet. */
70752
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69856
70753
  condition: string().nullable(),
69857
- /** Current temperature in the reported unit. Null when not provided. */
69858
- temperature: number().nullable(),
69859
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69860
- temperatureUnit: string().nullable(),
70754
+ /** Current temperature in Celsius. Null when not provided or refused. */
70755
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69861
70756
  /** Relative humidity (0..100). Null when not provided. */
69862
- humidity: number().min(0).max(100).nullable(),
69863
- /** Barometric pressure in the reported unit. Null when not provided. */
69864
- pressure: number().nullable(),
69865
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69866
- pressureUnit: string().nullable(),
69867
- /** Wind speed in the reported unit. Null when not provided. */
69868
- windSpeed: number().nullable(),
69869
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69870
- windSpeedUnit: string().nullable(),
70757
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
70758
+ /** Barometric pressure in hPa. Null when not provided or refused. */
70759
+ pressure: number().nullable().meta(readingMeta("pressure")),
70760
+ /** Wind speed in metres per second. Null when not provided or refused. */
70761
+ windSpeed: number().nullable().meta(readingMeta("speed")),
69871
70762
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69872
- windBearing: number().nullable(),
70763
+ windBearing: number().nullable().meta(readingMeta("angle")),
69873
70764
  /** Ms epoch when the slice was last updated. */
69874
70765
  lastFetchedAt: number()
69875
70766
  });
@@ -69897,12 +70788,12 @@ var weatherCapability = {
69897
70788
  */
69898
70789
  durability: "session",
69899
70790
  /**
69900
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
70791
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69901
70792
  */
69902
70793
  history: { fields: {
69903
70794
  temperature: {
69904
70795
  kind: "gauge",
69905
- unitFrom: "temperatureUnit",
70796
+ unit: "°C",
69906
70797
  deadband: .2
69907
70798
  },
69908
70799
  humidity: {
@@ -69912,12 +70803,12 @@ var weatherCapability = {
69912
70803
  },
69913
70804
  pressure: {
69914
70805
  kind: "gauge",
69915
- unitFrom: "pressureUnit",
70806
+ unit: "hPa",
69916
70807
  deadband: .5
69917
70808
  },
69918
70809
  windSpeed: {
69919
70810
  kind: "gauge",
69920
- unitFrom: "windSpeedUnit",
70811
+ unit: "m/s",
69921
70812
  deadband: .5
69922
70813
  }
69923
70814
  } }
@@ -70951,8 +71842,9 @@ function withClipEventLabels(provider, deps) {
70951
71842
  listClips
70952
71843
  };
70953
71844
  }
71845
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
70954
71846
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
70955
- 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;
71847
+ 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;
70956
71848
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
70957
71849
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
70958
71850
  new Set(CAP_NAMES);
@@ -71296,6 +72188,9 @@ var DEVICE_LOCAL_STATE_CAPS = {
71296
72188
  featureProbe: featureProbeCapability,
71297
72189
  flood: floodCapability,
71298
72190
  gas: gasCapability,
72191
+ groupActive: groupActiveCapability,
72192
+ groupCommands: groupCommandsCapability,
72193
+ groupStatus: groupStatusCapability,
71299
72194
  heatingValve: heatingValveCapability,
71300
72195
  humidifier: humidifierCapability,
71301
72196
  humiditySensor: humiditySensorCapability,
@@ -71305,6 +72200,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
71305
72200
  lawnMowerControl: lawnMowerControlCapability,
71306
72201
  lockControl: lockControlCapability,
71307
72202
  mediaPlayer: mediaPlayerCapability,
72203
+ meter: meterCapability,
71308
72204
  motion: motionCapability,
71309
72205
  motionTrigger: motionTriggerCapability,
71310
72206
  motionZones: motionZonesCapability,
@@ -72564,7 +73460,7 @@ var BaseDeviceProvider = class extends BaseAddon {
72564
73460
  return toDeviceSummary(device, this.addonId);
72565
73461
  }
72566
73462
  };
72567
- 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;
73463
+ 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;
72568
73464
  new Set(Object.values(DeviceType));
72569
73465
  Object.freeze({
72570
73466
  "accessories.getStatus": {
@@ -72672,6 +73568,13 @@ Object.freeze({
72672
73568
  access: "view",
72673
73569
  role: "admin"
72674
73570
  },
73571
+ "addons.getPackageDocs": {
73572
+ capName: "addons",
73573
+ capScope: "system",
73574
+ addonId: null,
73575
+ access: "view",
73576
+ role: "admin"
73577
+ },
72675
73578
  "addons.getVersions": {
72676
73579
  capName: "addons",
72677
73580
  capScope: "system",
@@ -75780,6 +76683,27 @@ Object.freeze({
75780
76683
  access: "view",
75781
76684
  role: "viewer"
75782
76685
  },
76686
+ "groupActive.getStatus": {
76687
+ capName: "group-active",
76688
+ capScope: "device",
76689
+ addonId: null,
76690
+ access: "view",
76691
+ role: "viewer"
76692
+ },
76693
+ "groupCommands.getStatus": {
76694
+ capName: "group-commands",
76695
+ capScope: "device",
76696
+ addonId: null,
76697
+ access: "view",
76698
+ role: "viewer"
76699
+ },
76700
+ "groupStatus.getStatus": {
76701
+ capName: "group-status",
76702
+ capScope: "device",
76703
+ addonId: null,
76704
+ access: "view",
76705
+ role: "viewer"
76706
+ },
75783
76707
  "heatingValve.getStatus": {
75784
76708
  capName: "heating-valve",
75785
76709
  capScope: "device",
@@ -76732,6 +77656,13 @@ Object.freeze({
76732
77656
  access: "create",
76733
77657
  role: "admin"
76734
77658
  },
77659
+ "meter.getStatus": {
77660
+ capName: "meter",
77661
+ capScope: "device",
77662
+ addonId: null,
77663
+ access: "view",
77664
+ role: "viewer"
77665
+ },
76735
77666
  "metricsProvider.collectSnapshot": {
76736
77667
  capName: "metrics-provider",
76737
77668
  capScope: "system",
@@ -84830,7 +85761,39 @@ var APPLIANCE_CAP_VOCABULARY = {
84830
85761
  phrase: t("When {device} finishes", "Quando {device} finisce"),
84831
85762
  icon: "square"
84832
85763
  }
84833
- }
85764
+ },
85765
+ overrides: { byType: {
85766
+ [DeviceType.Washer]: {
85767
+ state: { running: {
85768
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
85769
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
85770
+ } },
85771
+ events: {
85772
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
85773
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
85774
+ }
85775
+ },
85776
+ [DeviceType.Dryer]: {
85777
+ state: { running: {
85778
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
85779
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
85780
+ } },
85781
+ events: {
85782
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
85783
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
85784
+ }
85785
+ },
85786
+ [DeviceType.Dishwasher]: {
85787
+ state: { running: {
85788
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
85789
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
85790
+ } },
85791
+ events: {
85792
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
85793
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
85794
+ }
85795
+ }
85796
+ } }
84834
85797
  },
84835
85798
  update: {
84836
85799
  scope: "device",
@@ -85602,6 +86565,41 @@ var SENSOR_CAP_VOCABULARY = {
85602
86565
  icon: "flame"
85603
86566
  }]
85604
86567
  },
86568
+ "group-active": {
86569
+ scope: "device",
86570
+ noun: t("Group state", "Stato del gruppo")
86571
+ },
86572
+ "group-commands": {
86573
+ scope: "device",
86574
+ noun: t("Group command", "Comando di gruppo")
86575
+ },
86576
+ "group-status": {
86577
+ scope: "device",
86578
+ noun: t("Group", "Gruppo"),
86579
+ state: [
86580
+ {
86581
+ kind: "numeric",
86582
+ field: "included",
86583
+ quantity: t("the members reporting", "i membri che rispondono"),
86584
+ label: t("Reporting", "Rispondono"),
86585
+ icon: "users"
86586
+ },
86587
+ {
86588
+ kind: "numeric",
86589
+ field: "excluded",
86590
+ quantity: t("the members left out", "i membri esclusi"),
86591
+ label: t("Excluded", "Esclusi"),
86592
+ icon: "users"
86593
+ },
86594
+ {
86595
+ kind: "numeric",
86596
+ field: "total",
86597
+ quantity: t("the member count", "il numero di membri"),
86598
+ label: t("Members", "Membri"),
86599
+ icon: "sigma"
86600
+ }
86601
+ ]
86602
+ },
85605
86603
  "humidity-sensor": {
85606
86604
  scope: "device",
85607
86605
  noun: t("Humidity", "Umidità"),
@@ -85627,6 +86625,17 @@ var SENSOR_CAP_VOCABULARY = {
85627
86625
  }],
85628
86626
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
85629
86627
  },
86628
+ meter: {
86629
+ scope: "device",
86630
+ noun: t("Meter", "Contatore"),
86631
+ state: [{
86632
+ kind: "numeric",
86633
+ field: "value",
86634
+ quantity: t("the total", "il totale"),
86635
+ label: t("Total", "Totale"),
86636
+ icon: "gauge"
86637
+ }]
86638
+ },
85630
86639
  "numeric-sensor": {
85631
86640
  scope: "device",
85632
86641
  noun: t("Sensor", "Sensore"),
@@ -85635,7 +86644,6 @@ var SENSOR_CAP_VOCABULARY = {
85635
86644
  field: "value",
85636
86645
  quantity: t("the reading", "il valore"),
85637
86646
  label: t("Reading", "Valore"),
85638
- unitField: "unit",
85639
86647
  icon: "gauge"
85640
86648
  }]
85641
86649
  },
@@ -85771,7 +86779,7 @@ var SENSOR_CAP_VOCABULARY = {
85771
86779
  field: "temperature",
85772
86780
  quantity: t("the outdoor temperature", "la temperatura esterna"),
85773
86781
  label: t("Temperature", "Temperatura"),
85774
- unitField: "temperatureUnit",
86782
+ unit: "°C",
85775
86783
  icon: "thermometer"
85776
86784
  },
85777
86785
  {
@@ -85787,7 +86795,7 @@ var SENSOR_CAP_VOCABULARY = {
85787
86795
  field: "windSpeed",
85788
86796
  quantity: t("the wind speed", "la velocità del vento"),
85789
86797
  label: t("Wind", "Vento"),
85790
- unitField: "windSpeedUnit",
86798
+ unit: "m/s",
85791
86799
  icon: "wind"
85792
86800
  },
85793
86801
  {
@@ -85795,7 +86803,7 @@ var SENSOR_CAP_VOCABULARY = {
85795
86803
  field: "pressure",
85796
86804
  quantity: t("the air pressure", "la pressione atmosferica"),
85797
86805
  label: t("Pressure", "Pressione"),
85798
- unitField: "pressureUnit",
86806
+ unit: "hPa",
85799
86807
  icon: "gauge"
85800
86808
  }
85801
86809
  ]
@@ -86100,8 +87108,47 @@ function projectAction(cap, action) {
86100
87108
  };
86101
87109
  }
86102
87110
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
86103
- 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;
87111
+ 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;
86104
87112
  DeviceFeature.BatteryOperated;
87113
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
87114
+ 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.";
87115
+ 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.";
87116
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
87117
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
87118
+ 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.";
87119
+ function withheld(reason) {
87120
+ return {
87121
+ [DeviceType.Washer]: {
87122
+ exported: false,
87123
+ reason
87124
+ },
87125
+ [DeviceType.Dryer]: {
87126
+ exported: false,
87127
+ reason
87128
+ },
87129
+ [DeviceType.Dishwasher]: {
87130
+ exported: false,
87131
+ reason
87132
+ }
87133
+ };
87134
+ }
87135
+ function offered(received) {
87136
+ return {
87137
+ [DeviceType.Washer]: {
87138
+ exported: true,
87139
+ as: received
87140
+ },
87141
+ [DeviceType.Dryer]: {
87142
+ exported: true,
87143
+ as: received
87144
+ },
87145
+ [DeviceType.Dishwasher]: {
87146
+ exported: true,
87147
+ as: received
87148
+ }
87149
+ };
87150
+ }
87151
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
86105
87152
  var LAST_FETCHED_FIELD = "lastFetchedAt";
86106
87153
  /**
86107
87154
  * How long a bridge stops re-attempting a refresh that did not land.
@@ -87220,6 +88267,21 @@ Object.freeze({
87220
88267
  form: "single",
87221
88268
  optional: false
87222
88269
  }],
88270
+ "groupActive.getStatus": [{
88271
+ name: "deviceId",
88272
+ form: "single",
88273
+ optional: false
88274
+ }],
88275
+ "groupCommands.getStatus": [{
88276
+ name: "deviceId",
88277
+ form: "single",
88278
+ optional: false
88279
+ }],
88280
+ "groupStatus.getStatus": [{
88281
+ name: "deviceId",
88282
+ form: "single",
88283
+ optional: false
88284
+ }],
87223
88285
  "heatingValve.getStatus": [{
87224
88286
  name: "deviceId",
87225
88287
  form: "single",
@@ -87420,6 +88482,11 @@ Object.freeze({
87420
88482
  form: "single",
87421
88483
  optional: false
87422
88484
  }],
88485
+ "meter.getStatus": [{
88486
+ name: "deviceId",
88487
+ form: "single",
88488
+ optional: false
88489
+ }],
87423
88490
  "motion.getStatus": [{
87424
88491
  name: "deviceId",
87425
88492
  form: "single",
@@ -89027,7 +90094,6 @@ Object.freeze({
89027
90094
  "network-access": "ingress",
89028
90095
  "smtp-provider": "email"
89029
90096
  });
89030
- DeviceType.Camera;
89031
90097
  /**
89032
90098
  * A saved Customize assignment (D936). The schema is the type: callers use
89033
90099
  * `z.infer`, they do not restate the shape.
@@ -89204,6 +90270,8 @@ new Map(Object.entries({
89204
90270
  "detection-blind": "device-events",
89205
90271
  "motion-noisy": "device-events",
89206
90272
  "motion-calm": "device-events",
90273
+ "package-llm-silent": "device-events",
90274
+ "package-llm-answered": "device-events",
89207
90275
  "recording-stalled": "device-events",
89208
90276
  "recording-healthy": "device-events",
89209
90277
  "recording-degraded": "device-events",
@@ -89287,6 +90355,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
89287
90355
  label: "Motion without a subject",
89288
90356
  kinds: ["motion-noisy", "motion-calm"]
89289
90357
  },
90358
+ {
90359
+ id: "package-llm",
90360
+ family: "device-events",
90361
+ label: "Package confirm",
90362
+ kinds: ["package-llm-silent", "package-llm-answered"]
90363
+ },
89290
90364
  {
89291
90365
  id: "recording",
89292
90366
  family: "device-events",
@@ -89468,6 +90542,8 @@ var DEVICE_EVENTS = [
89468
90542
  "detection-blind",
89469
90543
  "motion-noisy",
89470
90544
  "motion-calm",
90545
+ "package-llm-silent",
90546
+ "package-llm-answered",
89471
90547
  "recording-stalled",
89472
90548
  "recording-healthy",
89473
90549
  "recording-degraded",
@@ -89736,6 +90812,15 @@ new Map([
89736
90812
  snapshotLabel: "Cameras with constant motion now",
89737
90813
  snapshotSection: null
89738
90814
  },
90815
+ {
90816
+ id: "package-llm-confirm",
90817
+ family: "device-events",
90818
+ label: "Package confirm (not answering / answering again)",
90819
+ bad: ["package-llm-silent"],
90820
+ recovery: ["package-llm-answered"],
90821
+ snapshotLabel: "Cameras whose package confirm is not answering",
90822
+ snapshotSection: null
90823
+ },
89739
90824
  {
89740
90825
  id: "recording-health",
89741
90826
  family: "device-events",