@camstack/addon-auth 1.2.168 → 1.2.170

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.
@@ -1,4 +1,4 @@
1
- //#region ../types/dist/event-category-DHXxB8t9.mjs
1
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
2
2
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
3
3
  EventCategory["SystemBoot"] = "system.boot";
4
4
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -324,6 +324,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
324
324
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
325
325
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
326
326
  /**
327
+ * A camera's package LLM confirm has failed closed three times in a row
328
+ * (D931, 2026-10-26). One edge per camera until
329
+ * {@link PipelinePackageLlmAnswered}.
330
+ */
331
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
332
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
333
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
334
+ /**
327
335
  * Per-camera pipeline config was mutated by the orchestrator
328
336
  * (a settings change via `setAgentInferenceDevices` /
329
337
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5566,7 +5574,7 @@ var ZodIssueCode = {
5566
5574
  var ZodFirstPartyTypeKind;
5567
5575
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5568
5576
  //#endregion
5569
- //#region ../types/dist/sleep-BErAxhul.mjs
5577
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5570
5578
  /**
5571
5579
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5572
5580
  * window to float samples (D455).
@@ -8294,6 +8302,99 @@ function defineEntryKind(name, spec) {
8294
8302
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8295
8303
  return defined;
8296
8304
  }
8305
+ var READING_QUANTITIES = [
8306
+ "temperature",
8307
+ "pressure",
8308
+ "speed",
8309
+ "precipitation",
8310
+ "precipitation-rate",
8311
+ "length",
8312
+ "area",
8313
+ "illuminance",
8314
+ "irradiance",
8315
+ "power",
8316
+ "energy",
8317
+ "voltage",
8318
+ "current",
8319
+ "apparent-power",
8320
+ "humidity",
8321
+ "percentage",
8322
+ "concentration-volume",
8323
+ "concentration-mass",
8324
+ "concentration-ppb",
8325
+ "volume",
8326
+ "duration",
8327
+ "duration-ms",
8328
+ "information",
8329
+ "sound-level",
8330
+ "signal-level",
8331
+ "colour-temperature",
8332
+ "angle",
8333
+ "bitrate",
8334
+ "rate",
8335
+ "mass",
8336
+ "index",
8337
+ "ratio",
8338
+ "count",
8339
+ "none",
8340
+ "device"
8341
+ ];
8342
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8343
+ new Set(READING_QUANTITIES);
8344
+ /**
8345
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8346
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8347
+ */
8348
+ var QUANTITY_BASE = {
8349
+ temperature: "°C",
8350
+ pressure: "hPa",
8351
+ speed: "m/s",
8352
+ precipitation: "mm",
8353
+ "precipitation-rate": "mm/h",
8354
+ length: "m",
8355
+ area: "m²",
8356
+ illuminance: "lx",
8357
+ irradiance: "W/m²",
8358
+ power: "W",
8359
+ energy: "kWh",
8360
+ voltage: "V",
8361
+ current: "A",
8362
+ "apparent-power": "VA",
8363
+ humidity: "%",
8364
+ percentage: "%",
8365
+ "concentration-volume": "ppm",
8366
+ "concentration-mass": "µg/m³",
8367
+ "concentration-ppb": "ppb",
8368
+ volume: "L",
8369
+ duration: "s",
8370
+ "duration-ms": "ms",
8371
+ information: "B",
8372
+ "sound-level": "dBFS",
8373
+ "signal-level": "dBm",
8374
+ "colour-temperature": "mired",
8375
+ angle: "°",
8376
+ bitrate: "kbps",
8377
+ rate: "fps",
8378
+ mass: "kg",
8379
+ index: "none",
8380
+ ratio: "none",
8381
+ count: "none",
8382
+ none: "none",
8383
+ device: "device"
8384
+ };
8385
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8386
+ function baseUnitOf(quantity) {
8387
+ const symbol = QUANTITY_BASE[quantity];
8388
+ if (symbol === "none" || symbol === "device") return void 0;
8389
+ return symbol;
8390
+ }
8391
+ function readingMeta(quantity, extras) {
8392
+ return {
8393
+ quantity,
8394
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8395
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8396
+ };
8397
+ }
8297
8398
  /** Shared Zod schemas used across streaming capabilities. */
8298
8399
  var CamProfileSchema = _enum([
8299
8400
  "high",
@@ -8321,8 +8422,8 @@ var CamStreamKindSchema = _enum([
8321
8422
  "derived"
8322
8423
  ]);
8323
8424
  var CamStreamResolutionSchema = object({
8324
- width: number().int().positive(),
8325
- height: number().int().positive()
8425
+ width: number().int().positive().meta(readingMeta("none")),
8426
+ height: number().int().positive().meta(readingMeta("none"))
8326
8427
  });
8327
8428
  var CameraStreamSchema = object({
8328
8429
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8836,6 +8937,9 @@ var CAP_NAMES = Object.freeze([
8836
8937
  "filesystem-browse",
8837
8938
  "flood",
8838
8939
  "gas",
8940
+ "group-active",
8941
+ "group-commands",
8942
+ "group-status",
8839
8943
  "heating-valve",
8840
8944
  "humidifier",
8841
8945
  "humidity-sensor",
@@ -8856,6 +8960,7 @@ var CAP_NAMES = Object.freeze([
8856
8960
  "login-method",
8857
8961
  "media-player",
8858
8962
  "mesh-network",
8963
+ "meter",
8859
8964
  "metrics-provider",
8860
8965
  "model-convert",
8861
8966
  "model-distributor",
@@ -8965,14 +9070,6 @@ Object.freeze({
8965
9070
  {
8966
9071
  "path": "aqi",
8967
9072
  "valueType": "number"
8968
- },
8969
- {
8970
- "path": "unit",
8971
- "valueType": "string"
8972
- },
8973
- {
8974
- "path": "precision",
8975
- "valueType": "number"
8976
9073
  }
8977
9074
  ],
8978
9075
  "alarm-panel": [
@@ -9008,20 +9105,10 @@ Object.freeze({
9008
9105
  "valueType": "boolean"
9009
9106
  }
9010
9107
  ],
9011
- "ambient-light-sensor": [
9012
- {
9013
- "path": "lux",
9014
- "valueType": "number"
9015
- },
9016
- {
9017
- "path": "unit",
9018
- "valueType": "string"
9019
- },
9020
- {
9021
- "path": "precision",
9022
- "valueType": "number"
9023
- }
9024
- ],
9108
+ "ambient-light-sensor": [{
9109
+ "path": "lux",
9110
+ "valueType": "number"
9111
+ }],
9025
9112
  "audio-metrics": [
9026
9113
  {
9027
9114
  "path": "ts",
@@ -9101,6 +9188,10 @@ Object.freeze({
9101
9188
  "path": "lastUpdated",
9102
9189
  "valueType": "number"
9103
9190
  },
9191
+ {
9192
+ "path": "volts",
9193
+ "valueType": "number"
9194
+ },
9104
9195
  {
9105
9196
  "path": "lastContactAt",
9106
9197
  "valueType": "number"
@@ -9343,10 +9434,6 @@ Object.freeze({
9343
9434
  "datetime"
9344
9435
  ]
9345
9436
  },
9346
- {
9347
- "path": "unit",
9348
- "valueType": "string"
9349
- },
9350
9437
  {
9351
9438
  "path": "min",
9352
9439
  "valueType": "number"
@@ -9358,19 +9445,6 @@ Object.freeze({
9358
9445
  {
9359
9446
  "path": "step",
9360
9447
  "valueType": "number"
9361
- },
9362
- {
9363
- "path": "precision",
9364
- "valueType": "number"
9365
- },
9366
- {
9367
- "path": "format",
9368
- "valueType": "enum",
9369
- "enumValues": [
9370
- "date",
9371
- "time",
9372
- "datetime"
9373
- ]
9374
9448
  }
9375
9449
  ],
9376
9450
  "cover": [
@@ -9611,13 +9685,8 @@ Object.freeze({
9611
9685
  "path": "value",
9612
9686
  "valueType": "string"
9613
9687
  }, {
9614
- "path": "format",
9615
- "valueType": "enum",
9616
- "enumValues": [
9617
- "date",
9618
- "time",
9619
- "datetime"
9620
- ]
9688
+ "path": "timestamp",
9689
+ "valueType": "boolean"
9621
9690
  }],
9622
9691
  "event-emitter": [
9623
9692
  {
@@ -9690,6 +9759,24 @@ Object.freeze({
9690
9759
  "path": "detected",
9691
9760
  "valueType": "boolean"
9692
9761
  }],
9762
+ "group-active": [{
9763
+ "path": "active",
9764
+ "valueType": "boolean"
9765
+ }],
9766
+ "group-status": [
9767
+ {
9768
+ "path": "included",
9769
+ "valueType": "number"
9770
+ },
9771
+ {
9772
+ "path": "excluded",
9773
+ "valueType": "number"
9774
+ },
9775
+ {
9776
+ "path": "total",
9777
+ "valueType": "number"
9778
+ }
9779
+ ],
9693
9780
  "heating-valve": [{
9694
9781
  "path": "position",
9695
9782
  "valueType": "number"
@@ -9724,20 +9811,10 @@ Object.freeze({
9724
9811
  "valueType": "number"
9725
9812
  }
9726
9813
  ],
9727
- "humidity-sensor": [
9728
- {
9729
- "path": "percent",
9730
- "valueType": "number"
9731
- },
9732
- {
9733
- "path": "unit",
9734
- "valueType": "string"
9735
- },
9736
- {
9737
- "path": "precision",
9738
- "valueType": "number"
9739
- }
9740
- ],
9814
+ "humidity-sensor": [{
9815
+ "path": "percent",
9816
+ "valueType": "number"
9817
+ }],
9741
9818
  "image": [{
9742
9819
  "path": "url",
9743
9820
  "valueType": "string"
@@ -9945,6 +10022,10 @@ Object.freeze({
9945
10022
  ]
9946
10023
  }
9947
10024
  ],
10025
+ "meter": [{
10026
+ "path": "value",
10027
+ "valueType": "number"
10028
+ }],
9948
10029
  "motion": [
9949
10030
  {
9950
10031
  "path": "detected",
@@ -10076,20 +10157,10 @@ Object.freeze({
10076
10157
  "valueType": "boolean"
10077
10158
  }
10078
10159
  ],
10079
- "numeric-sensor": [
10080
- {
10081
- "path": "value",
10082
- "valueType": "number"
10083
- },
10084
- {
10085
- "path": "unit",
10086
- "valueType": "string"
10087
- },
10088
- {
10089
- "path": "precision",
10090
- "valueType": "number"
10091
- }
10092
- ],
10160
+ "numeric-sensor": [{
10161
+ "path": "value",
10162
+ "valueType": "number"
10163
+ }],
10093
10164
  "pet-feeder": [
10094
10165
  {
10095
10166
  "path": "foodLevel",
@@ -10181,14 +10252,6 @@ Object.freeze({
10181
10252
  {
10182
10253
  "path": "powerFactor",
10183
10254
  "valueType": "number"
10184
- },
10185
- {
10186
- "path": "unit",
10187
- "valueType": "string"
10188
- },
10189
- {
10190
- "path": "precision",
10191
- "valueType": "number"
10192
10255
  }
10193
10256
  ],
10194
10257
  "presence": [
@@ -10217,20 +10280,10 @@ Object.freeze({
10217
10280
  "valueType": "number"
10218
10281
  }
10219
10282
  ],
10220
- "pressure-sensor": [
10221
- {
10222
- "path": "hpa",
10223
- "valueType": "number"
10224
- },
10225
- {
10226
- "path": "unit",
10227
- "valueType": "string"
10228
- },
10229
- {
10230
- "path": "precision",
10231
- "valueType": "number"
10232
- }
10233
- ],
10283
+ "pressure-sensor": [{
10284
+ "path": "hpa",
10285
+ "valueType": "number"
10286
+ }],
10234
10287
  "privacy-mask": [{
10235
10288
  "path": "enabled",
10236
10289
  "valueType": "boolean"
@@ -10541,20 +10594,10 @@ Object.freeze({
10541
10594
  "path": "tampered",
10542
10595
  "valueType": "boolean"
10543
10596
  }],
10544
- "temperature-sensor": [
10545
- {
10546
- "path": "celsius",
10547
- "valueType": "number"
10548
- },
10549
- {
10550
- "path": "unit",
10551
- "valueType": "string"
10552
- },
10553
- {
10554
- "path": "precision",
10555
- "valueType": "number"
10556
- }
10557
- ],
10597
+ "temperature-sensor": [{
10598
+ "path": "celsius",
10599
+ "valueType": "number"
10600
+ }],
10558
10601
  "update": [
10559
10602
  {
10560
10603
  "path": "currentVersion",
@@ -10665,10 +10708,6 @@ Object.freeze({
10665
10708
  "path": "temperature",
10666
10709
  "valueType": "number"
10667
10710
  },
10668
- {
10669
- "path": "temperatureUnit",
10670
- "valueType": "string"
10671
- },
10672
10711
  {
10673
10712
  "path": "humidity",
10674
10713
  "valueType": "number"
@@ -10677,18 +10716,10 @@ Object.freeze({
10677
10716
  "path": "pressure",
10678
10717
  "valueType": "number"
10679
10718
  },
10680
- {
10681
- "path": "pressureUnit",
10682
- "valueType": "string"
10683
- },
10684
10719
  {
10685
10720
  "path": "windSpeed",
10686
10721
  "valueType": "number"
10687
10722
  },
10688
- {
10689
- "path": "windSpeedUnit",
10690
- "valueType": "string"
10691
- },
10692
10723
  {
10693
10724
  "path": "windBearing",
10694
10725
  "valueType": "number"
@@ -10738,7 +10769,7 @@ object({
10738
10769
  params: record(string(), union([string(), number()])).optional()
10739
10770
  });
10740
10771
  //#endregion
10741
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10772
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10742
10773
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10743
10774
  DeviceType["Camera"] = "camera";
10744
10775
  DeviceType["Hub"] = "hub";
@@ -10832,8 +10863,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10832
10863
  DeviceType["PetFeeder"] = "pet-feeder";
10833
10864
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10834
10865
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10835
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10866
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10867
+ * A plug-built dishwasher carries running-state and power instead. */
10836
10868
  DeviceType["Dishwasher"] = "dishwasher";
10869
+ /** Washing machine. A plug-built one is running while its power stays
10870
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10871
+ DeviceType["Washer"] = "washer";
10872
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10873
+ DeviceType["Dryer"] = "dryer";
10837
10874
  return DeviceType;
10838
10875
  }({});
10839
10876
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -11091,7 +11128,7 @@ function errMsg(err) {
11091
11128
  return String(err);
11092
11129
  }
11093
11130
  //#endregion
11094
- //#region ../types/dist/composition-kEl9mevu.mjs
11131
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
11095
11132
  /**
11096
11133
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11097
11134
  *
@@ -11406,6 +11443,13 @@ var table = {
11406
11443
  apply: () => {
11407
11444
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11408
11445
  }
11446
+ },
11447
+ schmitt: {
11448
+ minArgs: 3,
11449
+ maxArgs: 5,
11450
+ apply: () => {
11451
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11452
+ }
11409
11453
  }
11410
11454
  };
11411
11455
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11846,15 +11890,232 @@ function validateExpressionSource(src) {
11846
11890
  }
11847
11891
  return null;
11848
11892
  }
11849
- /** A composed device's stableId is this prefix plus the block id. */
11850
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11851
- /** Longest snippet a `code` source may carry (slice 4). */
11852
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11893
+ /**
11894
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11895
+ *
11896
+ * Standby of the control board is under 5 W; a motor or a heater is far
11897
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11898
+ * a cycle, so running starts only after 30 s above the line.
11899
+ *
11900
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11901
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11902
+ * report "finished" in the middle of the program, so the stop dwell is
11903
+ * 10 minutes on every type. The number is stored per type so one machine
11904
+ * can move later without moving the others.
11905
+ */
11906
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11907
+ var APPLIANCE_KINDS = [
11908
+ "washer",
11909
+ "dryer",
11910
+ "dishwasher"
11911
+ ];
11912
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11913
+ /**
11914
+ * Statistics and integrals over a history read (D944).
11915
+ *
11916
+ * Pure: the same points always give the same answer, so a restart recomputes
11917
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11918
+ * never the mean of the samples. A hole with no held value is not integrated
11919
+ * as zero. A truncated read is not published.
11920
+ */
11921
+ var STATISTIC_STATS = [
11922
+ "min",
11923
+ "max",
11924
+ "mean",
11925
+ "sum",
11926
+ "count",
11927
+ "change"
11928
+ ];
11929
+ /**
11930
+ * The range a statistics or integral helper asks history for (D944).
11931
+ *
11932
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11933
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11934
+ * is strictly longer than the longest that range can be. Forever qualifies.
11935
+ * Equal to a tier's retention is a refusal, not a short answer.
11936
+ */
11937
+ var MINUTE_MS = 6e4;
11938
+ var HOUR_MS = 60 * MINUTE_MS;
11939
+ var DAY_MS = 24 * HOUR_MS;
11940
+ var ROLLING_SPANS = [
11941
+ "1h",
11942
+ "24h",
11943
+ "2d",
11944
+ "7d",
11945
+ "30d",
11946
+ "custom"
11947
+ ];
11948
+ var CALENDAR_PERIODS = [
11949
+ "today",
11950
+ "yesterday",
11951
+ "this-week",
11952
+ "this-month"
11953
+ ];
11954
+ var RESET_PERIODS = [
11955
+ "never",
11956
+ "daily",
11957
+ "weekly",
11958
+ "monthly"
11959
+ ];
11960
+ 5 * MINUTE_MS;
11961
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11962
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11963
+ var rollingWindowObject = object({
11964
+ kind: literal("rolling"),
11965
+ span: _enum(ROLLING_SPANS),
11966
+ customMinutes: number().int().min(5).optional()
11967
+ }).check((payload) => {
11968
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11969
+ code: "custom",
11970
+ message: "a custom range needs a duration in minutes",
11971
+ path: ["customMinutes"],
11972
+ input: payload.value
11973
+ });
11974
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11975
+ code: "custom",
11976
+ message: "only a custom range carries a duration",
11977
+ path: ["customMinutes"],
11978
+ input: payload.value
11979
+ });
11980
+ });
11981
+ var RollingWindowSchema = rollingWindowObject;
11982
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
11983
+ kind: literal("calendar"),
11984
+ period: _enum(CALENDAR_PERIODS)
11985
+ })]);
11986
+ /**
11987
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11988
+ * id. Split out so helper schemas can name it without a cycle through
11989
+ * `composition.ts` (D659).
11990
+ */
11853
11991
  var CompositionSourceRefSchema = object({
11854
11992
  /** The owning addon; `stableId` is unique only within it. */
11855
11993
  addonId: string().min(1),
11856
11994
  stableId: string().min(1)
11857
11995
  });
11996
+ var GROUP_TEMPLATE_IDS = [
11997
+ "temperature",
11998
+ "humidity",
11999
+ "pressure",
12000
+ "illuminance",
12001
+ "power",
12002
+ "energy",
12003
+ "battery",
12004
+ "numeric",
12005
+ "motion",
12006
+ "contact",
12007
+ "leak",
12008
+ "smoke",
12009
+ "occupancy",
12010
+ "binary",
12011
+ "switch",
12012
+ "light",
12013
+ "fan",
12014
+ "cover",
12015
+ "valve",
12016
+ "lock",
12017
+ "media-player",
12018
+ "climate"
12019
+ ];
12020
+ var NUMERIC_AGGREGATES = [
12021
+ "mean",
12022
+ "median",
12023
+ "min",
12024
+ "max",
12025
+ "sum"
12026
+ ];
12027
+ var BINARY_AGGREGATES = ["any", "all"];
12028
+ /** How an actuator group decides it is on. Unknown members never count as off. */
12029
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
12030
+ var AGGREGATE_REDUCERS = [
12031
+ "mean",
12032
+ "median",
12033
+ "min",
12034
+ "max",
12035
+ "sum",
12036
+ "any",
12037
+ "all"
12038
+ ];
12039
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
12040
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
12041
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
12042
+ /** One member. The field is stored so occupancy can name motion or binary. */
12043
+ var HelperMemberSchema = object({
12044
+ source: CompositionSourceRefSchema,
12045
+ cap: string().min(1),
12046
+ fieldPath: string().min(1)
12047
+ });
12048
+ var HelperSpecSchema = discriminatedUnion("kind", [
12049
+ object({
12050
+ kind: literal("group"),
12051
+ templateId: GroupTemplateIdSchema,
12052
+ aggregate: AggregateReducerSchema,
12053
+ members: array(HelperMemberSchema).min(1).max(64),
12054
+ /** Actuator groups only. Readings leave this unset. */
12055
+ active: ActiveCriterionSchema.optional()
12056
+ }),
12057
+ object({
12058
+ kind: literal("appliance"),
12059
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
12060
+ appliance: _enum(APPLIANCE_KINDS),
12061
+ thresholdWatts: number().positive(),
12062
+ startDwellMs: number().int().nonnegative(),
12063
+ stopDwellMs: number().int().nonnegative(),
12064
+ member: HelperMemberSchema
12065
+ }),
12066
+ object({
12067
+ kind: literal("statistic"),
12068
+ stat: _enum(STATISTIC_STATS),
12069
+ window: StatisticWindowSchema,
12070
+ source: HelperMemberSchema
12071
+ }),
12072
+ object({
12073
+ kind: literal("integral"),
12074
+ reset: _enum(RESET_PERIODS),
12075
+ output: _enum(["energy", "meter"]),
12076
+ source: HelperMemberSchema
12077
+ }),
12078
+ object({
12079
+ kind: literal("threshold"),
12080
+ source: HelperMemberSchema,
12081
+ high: number(),
12082
+ low: number(),
12083
+ onForMs: number().int().nonnegative(),
12084
+ offForMs: number().int().nonnegative()
12085
+ }),
12086
+ object({
12087
+ kind: literal("arithmetic"),
12088
+ op: _enum([
12089
+ "sum",
12090
+ "difference",
12091
+ "ratio"
12092
+ ]),
12093
+ inputs: array(HelperMemberSchema).min(2).max(64)
12094
+ }),
12095
+ object({
12096
+ kind: literal("expression"),
12097
+ expr: string().min(1),
12098
+ inputs: array(object({
12099
+ name: string().min(1),
12100
+ member: HelperMemberSchema
12101
+ })).min(1).max(64),
12102
+ output: _enum(["number", "boolean"])
12103
+ }),
12104
+ object({
12105
+ kind: literal("derivative"),
12106
+ window: RollingWindowSchema,
12107
+ per: _enum([
12108
+ "second",
12109
+ "minute",
12110
+ "hour"
12111
+ ]),
12112
+ source: HelperMemberSchema
12113
+ })
12114
+ ]);
12115
+ /** A composed device's stableId is this prefix plus the block id. */
12116
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12117
+ /** Longest snippet a `code` source may carry (slice 4). */
12118
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11858
12119
  /** One field of one capability of one source device. */
11859
12120
  var CompositionFieldReadSchema = object({
11860
12121
  source: CompositionSourceRefSchema,
@@ -11898,7 +12159,59 @@ var CompositionCodeSourceSchema = object({
11898
12159
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11899
12160
  CompositionFromSourceSchema,
11900
12161
  CompositionExpressionSourceSchema,
11901
- CompositionCodeSourceSchema
12162
+ CompositionCodeSourceSchema,
12163
+ object({
12164
+ kind: literal("aggregate"),
12165
+ reducer: _enum([
12166
+ "mean",
12167
+ "median",
12168
+ "min",
12169
+ "max",
12170
+ "sum",
12171
+ "any",
12172
+ "all"
12173
+ ]),
12174
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12175
+ /**
12176
+ * Set when the reads are an enum state, not a yes/no: the listed states
12177
+ * count as active (a cover is `open`). A state outside the list is inactive;
12178
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
12179
+ */
12180
+ activeWhen: array(string().min(1)).min(1).optional(),
12181
+ /** `value` is the reading; the others are the member counts. */
12182
+ publish: _enum([
12183
+ "value",
12184
+ "included",
12185
+ "excluded",
12186
+ "total"
12187
+ ]),
12188
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12189
+ empty: _enum([
12190
+ "null",
12191
+ "omit",
12192
+ "offline"
12193
+ ])
12194
+ }),
12195
+ object({
12196
+ kind: literal("series"),
12197
+ read: CompositionFieldReadSchema,
12198
+ /** `value` is the statistic or the integral. The others are the gap counts. */
12199
+ publish: _enum([
12200
+ "value",
12201
+ "included",
12202
+ "excluded",
12203
+ "total"
12204
+ ]),
12205
+ /**
12206
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
12207
+ * device unseeded until history answers, so a restart cannot publish 0.
12208
+ */
12209
+ empty: _enum([
12210
+ "null",
12211
+ "omit",
12212
+ "offline"
12213
+ ])
12214
+ })
11902
12215
  ]);
11903
12216
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11904
12217
  kind: literal("forward"),
@@ -11944,7 +12257,13 @@ var CompositionSchema = object({
11944
12257
  kind: literal("existing"),
11945
12258
  device: CompositionSourceRefSchema
11946
12259
  })]),
11947
- features: array(CompositionFeatureSchema).min(1).max(16)
12260
+ features: array(CompositionFeatureSchema).min(1).max(16),
12261
+ /**
12262
+ * Present when the block is a helper template. Absent on a free
12263
+ * composition (the room example, device 4603). The editor reads this,
12264
+ * never the compiled features.
12265
+ */
12266
+ helper: HelperSpecSchema.optional()
11948
12267
  }).superRefine((composition, ctx) => {
11949
12268
  const seen = /* @__PURE__ */ new Set();
11950
12269
  composition.features.forEach((feature, index) => {
@@ -17427,7 +17746,22 @@ var BrokerProviderInfoSchema = object({
17427
17746
  */
17428
17747
  interactiveConnectPath: string().min(1).optional(),
17429
17748
  /** Relative hub path to disconnect the linked account (same popup pattern). */
17430
- interactiveDisconnectPath: string().min(1).optional()
17749
+ interactiveDisconnectPath: string().min(1).optional(),
17750
+ /**
17751
+ * The kind signs in through a server-held interactive session instead of
17752
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17753
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17754
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17755
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17756
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17757
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17758
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17759
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17760
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17761
+ * offers it as the default path; the fields the session fills are
17762
+ * marked `filledBySignIn` in the kind's settings schema.
17763
+ */
17764
+ signIn: object({ basePath: string().min(1) }).optional()
17431
17765
  }))
17432
17766
  });
17433
17767
  var ListInputSchema = object({
@@ -18422,7 +18756,12 @@ var CompositionProblemSchema = object({
18422
18756
  "item-field-reserved",
18423
18757
  "target-missing",
18424
18758
  "target-unreadable",
18425
- "not-replaceable"
18759
+ "not-replaceable",
18760
+ "native-capability-replace",
18761
+ "mixed-quantity",
18762
+ "helper-mismatch",
18763
+ "member-constraint",
18764
+ "retention"
18426
18765
  ]),
18427
18766
  path: string(),
18428
18767
  message: string()
@@ -18443,7 +18782,8 @@ var CompositionUnavailableModeSchema = _enum([
18443
18782
  "null",
18444
18783
  "offline",
18445
18784
  "unknown",
18446
- "release"
18785
+ "release",
18786
+ "omit"
18447
18787
  ]);
18448
18788
  /**
18449
18789
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18480,9 +18820,11 @@ var CompositionFieldPlanSchema = object({
18480
18820
  reads: array(CompositionFieldReadSchema)
18481
18821
  });
18482
18822
  /**
18483
- * What a feature does to its target (D663): `new` builds a capability on a new
18484
- * device; on an existing device it `add`s a capability the device lacks, or
18485
- * `replace`s named fields of one it has natively.
18823
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18824
+ * new device; on an existing device it `add`s a capability the device lacks.
18825
+ * Replacing a capability the device already serves is refused
18826
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18827
+ * wire for a claim the hub already holds.
18486
18828
  */
18487
18829
  var CompositionFeatureModeSchema = _enum([
18488
18830
  "new",
@@ -18653,8 +18995,392 @@ Object.freeze({
18653
18995
  latchedAt: {
18654
18996
  kind: "fixed",
18655
18997
  result: ["number", "null"]
18998
+ },
18999
+ schmitt: {
19000
+ kind: "fixed",
19001
+ result: ["boolean", "null"]
18656
19002
  }
18657
19003
  });
19004
+ /**
19005
+ * One catalog row per base reading a group helper can build (D944 §3).
19006
+ * Data, not a form default written into the block.
19007
+ */
19008
+ var NOW$2 = {
19009
+ fieldPath: "lastFetchedAt",
19010
+ expr: "now"
19011
+ };
19012
+ var CHANGED = {
19013
+ fieldPath: "lastChangedAt",
19014
+ expr: "now"
19015
+ };
19016
+ function numeric(input) {
19017
+ return {
19018
+ ...input,
19019
+ family: "numeric",
19020
+ deviceType: DeviceType.Sensor,
19021
+ memberDeviceType: DeviceType.Sensor,
19022
+ memberDeviceTypes: [DeviceType.Sensor],
19023
+ activeWhen: void 0,
19024
+ commandCaps: [],
19025
+ baseUnit: baseUnitOf(input.quantity),
19026
+ aggregates: NUMERIC_AGGREGATES,
19027
+ accept: [{
19028
+ cap: input.outputCap,
19029
+ fieldPath: input.outputField
19030
+ }]
19031
+ };
19032
+ }
19033
+ function binary(input) {
19034
+ return {
19035
+ ...input,
19036
+ family: "binary",
19037
+ deviceType: DeviceType.Sensor,
19038
+ memberDeviceType: DeviceType.Sensor,
19039
+ memberDeviceTypes: [DeviceType.Sensor],
19040
+ activeWhen: void 0,
19041
+ commandCaps: [],
19042
+ quantity: "none",
19043
+ baseUnit: void 0,
19044
+ aggregates: BINARY_AGGREGATES,
19045
+ empty: "offline"
19046
+ };
19047
+ }
19048
+ var GROUP_TEMPLATES = [
19049
+ numeric({
19050
+ id: "temperature",
19051
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
19052
+ role: DeviceRole.TemperatureSensor,
19053
+ outputCap: "temperature-sensor",
19054
+ outputField: "celsius",
19055
+ quantity: "temperature",
19056
+ defaultAggregate: "mean",
19057
+ empty: "null",
19058
+ companions: [NOW$2]
19059
+ }),
19060
+ numeric({
19061
+ id: "humidity",
19062
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
19063
+ role: DeviceRole.HumiditySensor,
19064
+ outputCap: "humidity-sensor",
19065
+ outputField: "percent",
19066
+ quantity: "humidity",
19067
+ defaultAggregate: "mean",
19068
+ empty: "null",
19069
+ companions: [NOW$2]
19070
+ }),
19071
+ numeric({
19072
+ id: "pressure",
19073
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
19074
+ role: DeviceRole.PressureSensor,
19075
+ outputCap: "pressure-sensor",
19076
+ outputField: "hpa",
19077
+ quantity: "pressure",
19078
+ defaultAggregate: "mean",
19079
+ empty: "null",
19080
+ companions: [NOW$2]
19081
+ }),
19082
+ numeric({
19083
+ id: "illuminance",
19084
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
19085
+ role: DeviceRole.AmbientLightSensor,
19086
+ outputCap: "ambient-light-sensor",
19087
+ outputField: "lux",
19088
+ quantity: "illuminance",
19089
+ defaultAggregate: "mean",
19090
+ empty: "null",
19091
+ companions: [NOW$2]
19092
+ }),
19093
+ numeric({
19094
+ id: "power",
19095
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
19096
+ role: DeviceRole.PowerSensor,
19097
+ outputCap: "power-meter",
19098
+ outputField: "watts",
19099
+ quantity: "power",
19100
+ defaultAggregate: "sum",
19101
+ empty: "omit",
19102
+ companions: [NOW$2]
19103
+ }),
19104
+ numeric({
19105
+ id: "energy",
19106
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
19107
+ role: DeviceRole.EnergySensor,
19108
+ outputCap: "power-meter",
19109
+ outputField: "kwhTotal",
19110
+ quantity: "energy",
19111
+ defaultAggregate: "sum",
19112
+ empty: "omit",
19113
+ companions: [NOW$2]
19114
+ }),
19115
+ numeric({
19116
+ id: "battery",
19117
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
19118
+ role: DeviceRole.BatterySensor,
19119
+ outputCap: "battery",
19120
+ outputField: "percentage",
19121
+ quantity: "percentage",
19122
+ defaultAggregate: "mean",
19123
+ empty: "null",
19124
+ companions: [
19125
+ {
19126
+ fieldPath: "charging",
19127
+ expr: "\"unknown\""
19128
+ },
19129
+ {
19130
+ fieldPath: "sleeping",
19131
+ expr: "false"
19132
+ },
19133
+ {
19134
+ fieldPath: "lastUpdated",
19135
+ expr: "now"
19136
+ }
19137
+ ]
19138
+ }),
19139
+ numeric({
19140
+ id: "numeric",
19141
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
19142
+ role: DeviceRole.NumericSensor,
19143
+ outputCap: "numeric-sensor",
19144
+ outputField: "value",
19145
+ quantity: "device",
19146
+ defaultAggregate: "mean",
19147
+ empty: "null",
19148
+ companions: [NOW$2]
19149
+ }),
19150
+ binary({
19151
+ id: "motion",
19152
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
19153
+ role: DeviceRole.MotionSensor,
19154
+ outputCap: "motion",
19155
+ outputField: "detected",
19156
+ defaultAggregate: "any",
19157
+ accept: [{
19158
+ cap: "motion",
19159
+ fieldPath: "detected"
19160
+ }],
19161
+ companions: [{
19162
+ fieldPath: "lastDetectedAt",
19163
+ expr: "null"
19164
+ }, {
19165
+ fieldPath: "autoClearAfterMs",
19166
+ expr: "null"
19167
+ }]
19168
+ }),
19169
+ binary({
19170
+ id: "contact",
19171
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
19172
+ role: DeviceRole.ContactSensor,
19173
+ outputCap: "contact",
19174
+ outputField: "entryOpen",
19175
+ defaultAggregate: "any",
19176
+ accept: [{
19177
+ cap: "contact",
19178
+ fieldPath: "entryOpen"
19179
+ }],
19180
+ companions: [CHANGED]
19181
+ }),
19182
+ binary({
19183
+ id: "leak",
19184
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
19185
+ role: DeviceRole.LeakSensor,
19186
+ outputCap: "flood",
19187
+ outputField: "flooded",
19188
+ defaultAggregate: "any",
19189
+ accept: [{
19190
+ cap: "flood",
19191
+ fieldPath: "flooded"
19192
+ }],
19193
+ companions: [CHANGED]
19194
+ }),
19195
+ binary({
19196
+ id: "smoke",
19197
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
19198
+ role: DeviceRole.SmokeSensor,
19199
+ outputCap: "smoke",
19200
+ outputField: "detected",
19201
+ defaultAggregate: "any",
19202
+ accept: [{
19203
+ cap: "smoke",
19204
+ fieldPath: "detected"
19205
+ }],
19206
+ companions: [CHANGED]
19207
+ }),
19208
+ binary({
19209
+ id: "occupancy",
19210
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
19211
+ role: DeviceRole.BinarySensor,
19212
+ outputCap: "binary",
19213
+ outputField: "on",
19214
+ defaultAggregate: "any",
19215
+ accept: [{
19216
+ cap: "motion",
19217
+ fieldPath: "detected"
19218
+ }, {
19219
+ cap: "binary",
19220
+ fieldPath: "on"
19221
+ }],
19222
+ companions: [CHANGED]
19223
+ }),
19224
+ binary({
19225
+ id: "binary",
19226
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
19227
+ role: DeviceRole.BinarySensor,
19228
+ outputCap: "binary",
19229
+ outputField: "on",
19230
+ defaultAggregate: "any",
19231
+ accept: [{
19232
+ cap: "binary",
19233
+ fieldPath: "on"
19234
+ }],
19235
+ companions: [CHANGED]
19236
+ }),
19237
+ actuator({
19238
+ id: "switch",
19239
+ 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.",
19240
+ deviceType: DeviceType.Switch,
19241
+ outputCap: "switch",
19242
+ outputField: "on",
19243
+ member: {
19244
+ cap: "switch",
19245
+ fieldPath: "on"
19246
+ },
19247
+ commandCaps: ["switch"]
19248
+ }),
19249
+ actuator({
19250
+ id: "light",
19251
+ 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.",
19252
+ deviceType: DeviceType.Light,
19253
+ outputCap: "switch",
19254
+ outputField: "on",
19255
+ member: {
19256
+ cap: "switch",
19257
+ fieldPath: "on"
19258
+ },
19259
+ commandCaps: [
19260
+ "switch",
19261
+ "brightness",
19262
+ "color"
19263
+ ]
19264
+ }),
19265
+ actuator({
19266
+ id: "fan",
19267
+ 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.",
19268
+ deviceType: DeviceType.Fan,
19269
+ outputCap: "switch",
19270
+ outputField: "on",
19271
+ member: {
19272
+ cap: "switch",
19273
+ fieldPath: "on"
19274
+ },
19275
+ commandCaps: ["switch", "fan-control"]
19276
+ }),
19277
+ actuator({
19278
+ id: "cover",
19279
+ 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.",
19280
+ deviceType: DeviceType.Cover,
19281
+ outputCap: "group-active",
19282
+ outputField: "active",
19283
+ member: {
19284
+ cap: "cover",
19285
+ fieldPath: "state"
19286
+ },
19287
+ activeWhen: [
19288
+ "open",
19289
+ "opening",
19290
+ "stopped"
19291
+ ],
19292
+ commandCaps: ["cover"]
19293
+ }),
19294
+ actuator({
19295
+ id: "valve",
19296
+ 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.",
19297
+ deviceType: DeviceType.Valve,
19298
+ outputCap: "group-active",
19299
+ outputField: "active",
19300
+ member: {
19301
+ cap: "valve",
19302
+ fieldPath: "state"
19303
+ },
19304
+ activeWhen: [
19305
+ "open",
19306
+ "opening",
19307
+ "stopped"
19308
+ ],
19309
+ commandCaps: ["valve"]
19310
+ }),
19311
+ actuator({
19312
+ id: "lock",
19313
+ 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.",
19314
+ deviceType: DeviceType.Lock,
19315
+ outputCap: "group-active",
19316
+ outputField: "active",
19317
+ member: {
19318
+ cap: "lock-control",
19319
+ fieldPath: "state"
19320
+ },
19321
+ activeWhen: ["locked"],
19322
+ commandCaps: ["lock-control"]
19323
+ }),
19324
+ actuator({
19325
+ id: "media-player",
19326
+ 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.",
19327
+ deviceType: DeviceType.MediaPlayer,
19328
+ outputCap: "group-active",
19329
+ outputField: "active",
19330
+ member: {
19331
+ cap: "media-player",
19332
+ fieldPath: "state"
19333
+ },
19334
+ activeWhen: ["playing", "buffering"],
19335
+ commandCaps: ["media-player"]
19336
+ }),
19337
+ actuator({
19338
+ id: "climate",
19339
+ 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.",
19340
+ deviceType: DeviceType.Thermostat,
19341
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
19342
+ outputCap: "group-active",
19343
+ outputField: "active",
19344
+ member: {
19345
+ cap: "climate-control",
19346
+ fieldPath: "mode"
19347
+ },
19348
+ activeWhen: [
19349
+ "heat",
19350
+ "cool",
19351
+ "auto",
19352
+ "heat_cool",
19353
+ "fan_only",
19354
+ "dry"
19355
+ ],
19356
+ commandCaps: ["climate-control"]
19357
+ })
19358
+ ];
19359
+ function actuator(input) {
19360
+ return {
19361
+ id: input.id,
19362
+ summary: input.summary,
19363
+ deviceType: input.deviceType,
19364
+ outputCap: input.outputCap,
19365
+ outputField: input.outputField,
19366
+ commandCaps: input.commandCaps,
19367
+ family: "actuator",
19368
+ role: void 0,
19369
+ memberDeviceType: input.deviceType,
19370
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
19371
+ activeWhen: input.activeWhen,
19372
+ quantity: "none",
19373
+ baseUnit: void 0,
19374
+ aggregates: BINARY_AGGREGATES,
19375
+ defaultAggregate: "any",
19376
+ empty: "offline",
19377
+ accept: [input.member],
19378
+ companions: [CHANGED]
19379
+ };
19380
+ }
19381
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19382
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19383
+ DeviceType.Camera;
18658
19384
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18659
19385
  /**
18660
19386
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20368,12 +21094,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20368
21094
  * providers detect renames by matching `uniqueId` and patch `id`
20369
21095
  * in-place so dispatch keeps working.
20370
21096
  *
20371
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20372
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20373
- * state push and are the single source of truth. The `device_class` tag is
20374
- * an HA-specific input that is consumed at the discovery boundary
20375
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20376
- * stored after that point.
21097
+ * **Display unit and precision are NOT stored here.** They are a read-time
21098
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
21099
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
21100
+ * neither is repeated on each sample. The `device_class` tag is an
21101
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
21102
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20377
21103
  *
20378
21104
  * The `raw` escape hatch carries provider-specific attributes that don't
20379
21105
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20387,6 +21113,14 @@ var SourceInfoSchema = object({
20387
21113
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20388
21114
  * Used to detect rename when `id` changes. */
20389
21115
  uniqueId: string().optional(),
21116
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
21117
+ quantity: ReadingQuantitySchema.optional(),
21118
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
21119
+ dateTimeShape: _enum([
21120
+ "date",
21121
+ "time",
21122
+ "datetime"
21123
+ ]).optional(),
20390
21124
  /** Provider-specific extras that don't fit the structured slots.
20391
21125
  * Whitelist what goes here per provider — do NOT dump entire HA
20392
21126
  * attribute blobs (would explode the meta JSON column). */
@@ -41317,7 +42051,8 @@ method(object({
41317
42051
  "enum"
41318
42052
  ]),
41319
42053
  enumValues: array(string()).optional(),
41320
- item: boolean().optional()
42054
+ item: boolean().optional(),
42055
+ quantity: ReadingQuantitySchema.optional()
41321
42056
  })).readonly(),
41322
42057
  itemArray: object({
41323
42058
  path: string(),
@@ -43686,7 +44421,7 @@ var NetworkAccessStatusSchema = object({
43686
44421
  });
43687
44422
  /**
43688
44423
  * Optional, richer endpoint shape returned by providers that expose
43689
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44424
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43690
44425
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43691
44426
  * the originating provider config (mode + sourcePort) so the
43692
44427
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -45125,8 +45860,8 @@ var MaskPolygonShapeSchema = object({
45125
45860
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45126
45861
  var MaskGridShapeSchema = object({
45127
45862
  kind: literal("grid"),
45128
- gridWidth: number(),
45129
- gridHeight: number(),
45863
+ gridWidth: number().meta(readingMeta("none")),
45864
+ gridHeight: number().meta(readingMeta("none")),
45130
45865
  cells: array(boolean())
45131
45866
  });
45132
45867
  discriminatedUnion("kind", [
@@ -45238,6 +45973,8 @@ var NcSystemEventKindSchema = _enum([
45238
45973
  "detection-blind",
45239
45974
  "motion-noisy",
45240
45975
  "motion-calm",
45976
+ "package-llm-silent",
45977
+ "package-llm-answered",
45241
45978
  "recording-stalled",
45242
45979
  "recording-healthy",
45243
45980
  "recording-degraded",
@@ -46327,7 +47064,20 @@ var NcRuleInputSchema = object({
46327
47064
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46328
47065
  * path does not run Zod defaults.
46329
47066
  */
46330
- groupingSec: number().int().min(5).max(60).optional()
47067
+ groupingSec: number().int().min(5).max(60).optional(),
47068
+ /**
47069
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
47070
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
47071
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
47072
+ * same mark the Library writes, and records whose notification it was and
47073
+ * when. A channel with no buttons gets the same two choices as links.
47074
+ *
47075
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
47076
+ * cap path): absent and `false` both mean off, which is what every rule
47077
+ * authored before this field meant. An editor turns it off by writing
47078
+ * `false`, so it needs no `clear` key.
47079
+ */
47080
+ askVerdict: boolean().optional()
46331
47081
  });
46332
47082
  /**
46333
47083
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -56303,6 +57053,28 @@ var PackageVersionInfoSchema = object({
56303
57053
  deprecated: string().optional(),
56304
57054
  distTags: array(string()).readonly()
56305
57055
  });
57056
+ /**
57057
+ * One published document. `ok` is markdown from the registry. `missing` means
57058
+ * the registry answered and the file was not there. `error` is a registry or
57059
+ * tarball failure — never an empty string standing in for either.
57060
+ */
57061
+ var PackageDocBodySchema = discriminatedUnion("status", [
57062
+ object({
57063
+ status: literal("ok"),
57064
+ markdown: string()
57065
+ }),
57066
+ object({ status: literal("missing") }),
57067
+ object({
57068
+ status: literal("error"),
57069
+ message: string()
57070
+ })
57071
+ ]);
57072
+ var PackageDocsSchema = object({
57073
+ name: string(),
57074
+ version: string(),
57075
+ readme: PackageDocBodySchema,
57076
+ changelog: PackageDocBodySchema
57077
+ });
56306
57078
  var SearchResultSchema = object({
56307
57079
  name: string(),
56308
57080
  version: string(),
@@ -56521,7 +57293,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56521
57293
  }), object({ success: literal(true) }), {
56522
57294
  kind: "mutation",
56523
57295
  auth: "admin"
56524
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57296
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57297
+ name: string().min(1),
57298
+ version: string().min(1).optional()
57299
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56525
57300
  kind: "mutation",
56526
57301
  auth: "admin"
56527
57302
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56571,46 +57346,27 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56571
57346
  }), LogStreamEntrySchema, { kind: "subscription" });
56572
57347
  object({
56573
57348
  /** Carbon dioxide concentration in ppm. */
56574
- co2Ppm: number().min(0).optional(),
57349
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56575
57350
  /** Total volatile organic compounds in ppb. */
56576
- vocPpb: number().min(0).optional(),
57351
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56577
57352
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56578
- pm25: number().min(0).optional(),
57353
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56579
57354
  /** Particulate matter ≤ 10 μm in µg/m³. */
56580
- pm10: number().min(0).optional(),
57355
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56581
57356
  /** Composite AQI value (typically 0..500). */
56582
- aqi: number().optional(),
57357
+ aqi: number().optional().meta(readingMeta("index")),
56583
57358
  /** Ms epoch when the slice was last updated. */
56584
- lastFetchedAt: number(),
56585
- /** Live display unit of the single metric this slice carries (e.g. HA
56586
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56587
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56588
- * per slice is unambiguous. */
56589
- unit: string().optional(),
56590
- /** Suggested decimal places for numeric display.
56591
- * Populated live from the upstream source when provided (e.g. HA
56592
- * `attributes.suggested_display_precision`). Falls back to
56593
- * auto-formatting when absent. */
56594
- precision: number().int().min(0).max(10).optional()
57359
+ lastFetchedAt: number()
56595
57360
  });
56596
57361
  DeviceType.Sensor;
56597
57362
  object({
56598
- /** Current illuminance in lux (lx). */
56599
- lux: number().min(0).meta({
56600
- unit: "lx",
57363
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57364
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57365
+ precision: 0,
56601
57366
  label: "Illuminance"
56602
- }),
57367
+ })),
56603
57368
  /** Ms epoch when the slice was last updated. */
56604
- lastFetchedAt: number(),
56605
- /** Live display unit from the upstream source (e.g. HA
56606
- * `attributes.unit_of_measurement`). The UI prefers this over the
56607
- * role's canonical unit. Absent → fall back to the canonical unit. */
56608
- unit: string().optional(),
56609
- /** Suggested decimal places for numeric display.
56610
- * Populated live from the upstream source when provided (e.g. HA
56611
- * `attributes.suggested_display_precision`). Falls back to
56612
- * auto-formatting when absent. */
56613
- precision: number().int().min(0).max(10).optional()
57369
+ lastFetchedAt: number()
56614
57370
  });
56615
57371
  DeviceType.Sensor;
56616
57372
  /**
@@ -56755,10 +57511,7 @@ var BatteryStatusSchema = object({
56755
57511
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56756
57512
  * declines to band a non-finite reading, which is the correct shape.
56757
57513
  */
56758
- percentage: number().min(0).max(100).nullable().meta({
56759
- unit: "%",
56760
- label: "Charge"
56761
- }),
57514
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56762
57515
  /**
56763
57516
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56764
57517
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56785,7 +57538,14 @@ var BatteryStatusSchema = object({
56785
57538
  */
56786
57539
  sleeping: boolean().meta({ label: "Sleeping" }),
56787
57540
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56788
- lastUpdated: number(),
57541
+ lastUpdated: number().meta(readingMeta("none")),
57542
+ /**
57543
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57544
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57545
+ * that only report charge. A second reading on this cap, not a second
57546
+ * `numeric-sensor` on the same device.
57547
+ */
57548
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56789
57549
  /**
56790
57550
  * Ms epoch of the last time the device PROVED it was reachable — a
56791
57551
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56825,7 +57585,7 @@ var BatteryStatusSchema = object({
56825
57585
  */
56826
57586
  binary: boolean().optional().meta({ label: "Binary indicator" })
56827
57587
  });
56828
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat;
57588
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter;
56829
57589
  method(object({
56830
57590
  deviceId: number(),
56831
57591
  /** Bound on the wait. Sensible range 3000–10000ms. */
@@ -56845,7 +57605,7 @@ object({
56845
57605
  DeviceType.Sensor;
56846
57606
  object({
56847
57607
  /** Current level as 0..100 inclusive. Firmware-reported. */
56848
- percentage: number().min(0).max(100),
57608
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56849
57609
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56850
57610
  lastChangedAt: number()
56851
57611
  });
@@ -57230,21 +57990,21 @@ object({
57230
57990
  /** Single setpoint in Celsius. Used by single-target modes
57231
57991
  * (heat / cool / dry). Null when the device is in a range mode
57232
57992
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57233
- target: number().nullable(),
57993
+ target: number().nullable().meta(readingMeta("temperature")),
57234
57994
  /** Upper bound of the dual setpoint in Celsius. Populated only
57235
57995
  * in `heat_cool` mode. */
57236
- targetHigh: number().nullable(),
57996
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57237
57997
  /** Lower bound of the dual setpoint in Celsius. Populated only
57238
57998
  * in `heat_cool` mode. */
57239
- targetLow: number().nullable(),
57999
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57240
58000
  /** Target relative humidity (0..100). Null when the device has no
57241
58001
  * humidity control. */
57242
- targetHumidity: number().min(0).max(100).nullable(),
58002
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57243
58003
  /** Current measured temperature in Celsius. Read-only — pushed by
57244
58004
  * the device's internal sensor. */
57245
- currentTemp: number().nullable(),
58005
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57246
58006
  /** Current measured relative humidity (0..100). Read-only. */
57247
- currentHumidity: number().min(0).max(100).nullable(),
58007
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57248
58008
  /** Vertical louver swing on/off. Null when the device has no vertical
57249
58009
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57250
58010
  swingVertical: boolean().nullable(),
@@ -57252,11 +58012,11 @@ object({
57252
58012
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57253
58013
  swingHorizontal: boolean().nullable(),
57254
58014
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57255
- minTemp: number().nullable().optional(),
58015
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57256
58016
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57257
- maxTemp: number().nullable().optional(),
58017
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57258
58018
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57259
- tempStep: number().positive().nullable().optional(),
58019
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57260
58020
  /** Ms epoch when the slice was last updated (push or command). */
57261
58021
  lastFetchedAt: number()
57262
58022
  });
@@ -57335,17 +58095,17 @@ DeviceType.Thermostat, DeviceType.Climate, method(object({
57335
58095
  * modes to expose in the picker without a slice fetch.
57336
58096
  */
57337
58097
  var RgbTripletSchema = object({
57338
- r: number().int().min(0).max(255),
57339
- g: number().int().min(0).max(255),
57340
- b: number().int().min(0).max(255)
58098
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58099
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58100
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57341
58101
  });
57342
58102
  var HsvTripletSchema = object({
57343
58103
  /** Hue in degrees, 0..360. */
57344
- h: number().min(0).max(360),
58104
+ h: number().min(0).max(360).meta(readingMeta("none")),
57345
58105
  /** Saturation as 0..100 inclusive. */
57346
- s: number().min(0).max(100),
58106
+ s: number().min(0).max(100).meta(readingMeta("none")),
57347
58107
  /** Value/brightness as 0..100 inclusive. */
57348
- v: number().min(0).max(100)
58108
+ v: number().min(0).max(100).meta(readingMeta("none"))
57349
58109
  });
57350
58110
  var ColorInputSchema = discriminatedUnion("mode", [
57351
58111
  object({
@@ -57374,7 +58134,7 @@ object({
57374
58134
  /** Populated when `mode === 'hsv'`. */
57375
58135
  hsv: HsvTripletSchema.optional(),
57376
58136
  /** Populated when `mode === 'mired'`. */
57377
- mireds: number().int().optional(),
58137
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57378
58138
  /** Ms epoch of the last operator-driven change. */
57379
58139
  lastChangedAt: number()
57380
58140
  });
@@ -57498,18 +58258,12 @@ _enum([
57498
58258
  "empty",
57499
58259
  "full"
57500
58260
  ]);
57501
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57502
- * each. */
57503
- var ConsumableRemainingUnitSchema = _enum([
57504
- "days",
57505
- "hours",
57506
- "cycles"
57507
- ]);
57508
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57509
- * days). `value` null = unknown (D393). */
58261
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58262
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58263
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57510
58264
  var ConsumableRemainingSchema = object({
57511
- value: number().min(0).nullable(),
57512
- unit: ConsumableRemainingUnitSchema
58265
+ value: number().min(0).nullable().meta(readingMeta("device")),
58266
+ kind: ConsumableRemainingKindSchema
57513
58267
  });
57514
58268
  var ConsumablesStatusSchema = object({
57515
58269
  items: array(object({
@@ -57518,7 +58272,7 @@ var ConsumablesStatusSchema = object({
57518
58272
  /** Display name. */
57519
58273
  label: string().min(1),
57520
58274
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57521
- level: number().min(0).max(100).nullable(),
58275
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57522
58276
  /** The provider's discrete verdict when known (binary mode). */
57523
58277
  status: ConsumableStatusSchema.nullable(),
57524
58278
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57535,7 +58289,7 @@ var ConsumablesStatusSchema = object({
57535
58289
  })),
57536
58290
  lastChangedAt: number()
57537
58291
  });
57538
- Object.values(DeviceType), [...ConsumableRemainingUnitSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
58292
+ Object.values(DeviceType), [...ConsumableRemainingKindSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57539
58293
  Object.values(DeviceType), method(object({
57540
58294
  deviceId: number().int().nonnegative(),
57541
58295
  key: string().min(1)
@@ -57560,39 +58314,17 @@ object({
57560
58314
  /** Current value. Numeric kind → number; select/text/datetime → string.
57561
58315
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
57562
58316
  * or time-only — HA accepts all three forms). */
57563
- value: union([number(), string()]),
58317
+ value: union([number().meta(readingMeta("device")), string()]),
57564
58318
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
57565
58319
  options: array(string()),
57566
58320
  /** Ms epoch when the slice was last updated. */
57567
58321
  lastChangedAt: number(),
57568
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
57569
- * Populated live from HA `attributes.unit_of_measurement` on each push.
57570
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57571
- unit: string().optional(),
57572
- /** Minimum allowed value. Populated live from HA `attributes.min`.
57573
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57574
- min: number().optional(),
57575
- /** Maximum allowed value. Populated live from HA `attributes.max`.
57576
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57577
- max: number().optional(),
57578
- /** Step increment. Populated live from HA `attributes.step`.
57579
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57580
- step: number().optional(),
57581
- /** Suggested decimal places for numeric display. Populated live from HA
57582
- * `attributes.suggested_display_precision`, or derived from `step` when the
57583
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
57584
- * other kinds. Falls back to auto-formatting when absent. */
57585
- precision: number().int().min(0).max(10).optional(),
57586
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
57587
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
57588
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
57589
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
57590
- * `has_date`/`has_time` attributes on each push. */
57591
- format: _enum([
57592
- "date",
57593
- "time",
57594
- "datetime"
57595
- ]).optional()
58322
+ /** Minimum allowed value, in the device quantity's base unit. */
58323
+ min: number().optional().meta(readingMeta("device")),
58324
+ /** Maximum allowed value, in the device quantity's base unit. */
58325
+ max: number().optional().meta(readingMeta("device")),
58326
+ /** Step increment, in the device quantity's base unit. */
58327
+ step: number().optional().meta(readingMeta("device"))
57596
58328
  });
57597
58329
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
57598
58330
  object({
@@ -57630,9 +58362,9 @@ object({
57630
58362
  ]),
57631
58363
  /** 0 = fully closed, 100 = fully open. Null when the device has
57632
58364
  * no intermediate position surface. */
57633
- position: number().min(0).max(100).nullable(),
58365
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57634
58366
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
57635
- tiltPosition: number().min(0).max(100).nullable(),
58367
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57636
58368
  /** Ms epoch when the slice was last updated. */
57637
58369
  lastChangedAt: number()
57638
58370
  });
@@ -57690,9 +58422,9 @@ var NormalizedRangeSchema$1 = object({
57690
58422
  object({
57691
58423
  mode: DayNightModeSchema,
57692
58424
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
57693
- sensitivity: number().optional(),
58425
+ sensitivity: number().optional().meta(readingMeta("none")),
57694
58426
  /** Delay before the IR-cut filter flips, in seconds. */
57695
- switchDelaySec: number().optional(),
58427
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
57696
58428
  lastFetchedAt: number()
57697
58429
  });
57698
58430
  /**
@@ -57847,11 +58579,11 @@ object({
57847
58579
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
57848
58580
  availablePrograms: array(string()),
57849
58581
  /** Seconds remaining on the active cycle, or null when not running / not known. */
57850
- remainingSeconds: number().int().nonnegative().nullable(),
58582
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57851
58583
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
57852
- delayedStartSeconds: number().int().nonnegative().nullable(),
58584
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57853
58585
  /** Cycle completion 0..100, or null when not applicable. */
57854
- progressPercent: number().min(0).max(100).nullable(),
58586
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57855
58587
  /** True when the door is reported open; null when the door state is not known. */
57856
58588
  doorOpen: boolean().nullable(),
57857
58589
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -57875,7 +58607,7 @@ object({
57875
58607
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57876
58608
  machineCareDue: boolean().nullable(),
57877
58609
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57878
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
58610
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
57879
58611
  /** Option states: null when the appliance does not expose the option or has not said. */
57880
58612
  halfLoad: boolean().nullable(),
57881
58613
  hygienePlus: boolean().nullable(),
@@ -57890,7 +58622,7 @@ object({
57890
58622
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
57891
58623
  activeEvents: array(string()),
57892
58624
  /** Numeric error code when the appliance reports one; null otherwise. */
57893
- errorCode: number().nullable(),
58625
+ errorCode: number().nullable().meta(readingMeta("none")),
57894
58626
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
57895
58627
  errorLabel: string().nullable(),
57896
58628
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58036,24 +58768,14 @@ object({
58036
58768
  /** Ms epoch of the last press. null = never observed since this provider started. */
58037
58769
  lastPressedAt: number().nullable(),
58038
58770
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58039
- pressCountSinceStart: number()
58771
+ pressCountSinceStart: number().meta(readingMeta("count"))
58040
58772
  });
58041
58773
  object({
58042
58774
  deviceId: number(),
58043
58775
  timestamp: number()
58044
58776
  });
58045
58777
  DeviceType.Button, DeviceType.Camera;
58046
- /**
58047
- * Enum-state sensor — a string value picked from a finite option set.
58048
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
58049
- * states, weather conditions, contact-source channels, mode strings).
58050
- * The option set is stable across observations; it is persisted in the
58051
- * device config blob (via the control slice at adoption) and is NOT
58052
- * stored in `sourceInfo`.
58053
- */
58054
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
58055
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
58056
- var EnumSensorDateTimeFormatSchema = _enum([
58778
+ _enum([
58057
58779
  "date",
58058
58780
  "time",
58059
58781
  "datetime"
@@ -58061,12 +58783,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
58061
58783
  object({
58062
58784
  value: string(),
58063
58785
  /**
58064
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
58065
- * as a locale date/time/datetime; absent for plain enum sensors. HA
58066
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
58067
- * a time-only sensor → `'time'`.
58786
+ * Set when `value` is a timestamp rather than an enum state, so history
58787
+ * does not learn ISO strings as states. The display shape (date / time /
58788
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
58068
58789
  */
58069
- format: EnumSensorDateTimeFormatSchema.optional(),
58790
+ timestamp: boolean().optional(),
58070
58791
  /** Ms epoch when the slice was last updated. */
58071
58792
  lastFetchedAt: number()
58072
58793
  });
@@ -58085,13 +58806,13 @@ var EventFireSchema = object({
58085
58806
  deviceId: number(),
58086
58807
  eventType: string(),
58087
58808
  data: record(string(), unknown()).nullable(),
58088
- timestamp: number(),
58089
- seq: number()
58809
+ timestamp: number().meta(readingMeta("none")),
58810
+ seq: number().meta(readingMeta("count"))
58090
58811
  });
58091
58812
  object({
58092
58813
  eventTypes: array(string()),
58093
58814
  lastEvent: EventFireSchema.nullable(),
58094
- eventCountSinceStart: number()
58815
+ eventCountSinceStart: number().meta(readingMeta("count"))
58095
58816
  });
58096
58817
  DeviceType.EventEmitter;
58097
58818
  var IdentitySchema = object({
@@ -58480,10 +59201,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
58480
59201
  object({
58481
59202
  /** Active speed as 0..100 inclusive. Null when the device has no
58482
59203
  * speed surface (single-speed fan). */
58483
- percentage: number().min(0).max(100).nullable(),
59204
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58484
59205
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
58485
59206
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
58486
- percentageStep: number().positive().optional(),
59207
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
58487
59208
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
58488
59209
  * — empty string when no preset is active or supported. */
58489
59210
  preset: string(),
@@ -58565,7 +59286,7 @@ var FeatureProbeStatusSchema = object({
58565
59286
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
58566
59287
  model: string().nullable(),
58567
59288
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
58568
- channelCount: number().nullable(),
59289
+ channelCount: number().nullable().meta(readingMeta("count")),
58569
59290
  /**
58570
59291
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
58571
59292
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -58597,9 +59318,45 @@ object({
58597
59318
  lastChangedAt: number()
58598
59319
  });
58599
59320
  DeviceType.Sensor;
59321
+ object({
59322
+ /** One row per member that has been commanded. A later command replaces that member's row. */
59323
+ results: array(object({
59324
+ deviceId: number().int().nonnegative(),
59325
+ cap: string().min(1),
59326
+ method: string().min(1),
59327
+ ok: boolean(),
59328
+ error: string().optional(),
59329
+ /** Ms epoch of the attempt. */
59330
+ at: number()
59331
+ })) });
59332
+ DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
59333
+ object({
59334
+ active: boolean().meta({ label: "Active" }),
59335
+ /** Ms epoch when the slice was last updated. */
59336
+ lastChangedAt: number()
59337
+ });
59338
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Climate, DeviceType.Thermostat;
59339
+ object({
59340
+ /** Members whose current value entered the aggregate. */
59341
+ included: number().int().nonnegative().meta(readingMeta("count", {
59342
+ precision: 0,
59343
+ label: "Reporting"
59344
+ })),
59345
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
59346
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
59347
+ precision: 0,
59348
+ label: "Excluded"
59349
+ })),
59350
+ /** `included` + `excluded`. */
59351
+ total: number().int().nonnegative().meta(readingMeta("count", {
59352
+ precision: 0,
59353
+ label: "Members"
59354
+ }))
59355
+ });
59356
+ DeviceType.Sensor, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
58600
59357
  object({
58601
59358
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
58602
- position: number().min(0).max(100).nullable(),
59359
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58603
59360
  /** Ms epoch when the slice was last updated (push or poll). */
58604
59361
  lastFetchedAt: number()
58605
59362
  });
@@ -58608,9 +59365,9 @@ object({
58608
59365
  /** Whether the humidifier is currently on. */
58609
59366
  on: boolean(),
58610
59367
  /** Current measured relative humidity (0..100). Null when not reported. */
58611
- currentHumidity: number().min(0).max(100).nullable(),
59368
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58612
59369
  /** Target relative humidity (0..100). Null when no setpoint surface. */
58613
- targetHumidity: number().min(0).max(100).nullable(),
59370
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58614
59371
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
58615
59372
  * device has no mode surface. */
58616
59373
  mode: string().nullable(),
@@ -58620,9 +59377,9 @@ object({
58620
59377
  * `idle` / `off`). Null when the device doesn't report it. */
58621
59378
  action: string().nullable(),
58622
59379
  /** HA `min_humidity` attribute. Null → UI uses 0. */
58623
- minHumidity: number().nullable(),
59380
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
58624
59381
  /** HA `max_humidity` attribute. Null → UI uses 100. */
58625
- maxHumidity: number().nullable(),
59382
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
58626
59383
  /** Ms epoch when the slice was last updated. */
58627
59384
  lastChangedAt: number()
58628
59385
  });
@@ -58646,22 +59403,13 @@ DeviceType.Humidifier, method(object({
58646
59403
  auth: "protected"
58647
59404
  });
58648
59405
  object({
58649
- /** Current relative humidity, 0..100. */
58650
- percent: number().min(0).max(100).meta({
58651
- unit: "%",
59406
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
59407
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
59408
+ precision: 0,
58652
59409
  label: "Humidity"
58653
- }),
59410
+ })),
58654
59411
  /** Ms epoch when the slice was last updated. */
58655
- lastFetchedAt: number(),
58656
- /** Live display unit from the upstream source (e.g. HA
58657
- * `attributes.unit_of_measurement`). The UI prefers this over the
58658
- * role's canonical unit. Absent → fall back to the canonical unit. */
58659
- unit: string().optional(),
58660
- /** Suggested decimal places for numeric display.
58661
- * Populated live from the upstream source when provided (e.g. HA
58662
- * `attributes.suggested_display_precision`). Falls back to
58663
- * auto-formatting when absent. */
58664
- precision: number().int().min(0).max(10).optional()
59412
+ lastFetchedAt: number()
58665
59413
  });
58666
59414
  DeviceType.Sensor, DeviceType.Thermostat;
58667
59415
  object({
@@ -58669,7 +59417,7 @@ object({
58669
59417
  * entity exposes no `entity_picture` (yet). */
58670
59418
  url: string().nullable(),
58671
59419
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
58672
- lastUpdated: number().nullable()
59420
+ lastUpdated: number().nullable().meta(readingMeta("none"))
58673
59421
  });
58674
59422
  DeviceType.Image;
58675
59423
  /**
@@ -58725,19 +59473,19 @@ var NormalizedRangeSchema = object({
58725
59473
  });
58726
59474
  object({
58727
59475
  /** Normalized 0–100. */
58728
- brightness: number().optional(),
59476
+ brightness: number().optional().meta(readingMeta("none")),
58729
59477
  /** Normalized 0–100. */
58730
- contrast: number().optional(),
59478
+ contrast: number().optional().meta(readingMeta("none")),
58731
59479
  /** Normalized 0–100. */
58732
- saturation: number().optional(),
59480
+ saturation: number().optional().meta(readingMeta("none")),
58733
59481
  /** Normalized 0–100. */
58734
- sharpness: number().optional(),
59482
+ sharpness: number().optional().meta(readingMeta("none")),
58735
59483
  mirror: boolean().optional(),
58736
59484
  flip: boolean().optional(),
58737
59485
  rotate: ImageRotateSchema.optional(),
58738
59486
  whiteBalance: WhiteBalanceModeSchema.optional(),
58739
59487
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
58740
- warmth: number().optional(),
59488
+ warmth: number().optional().meta(readingMeta("none")),
58741
59489
  exposureMode: ExposureModeSchema.optional(),
58742
59490
  backlightMode: BacklightModeSchema.optional(),
58743
59491
  lastFetchedAt: number()
@@ -58982,10 +59730,10 @@ var IntercomAbilitySchema = object({
58982
59730
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
58983
59731
  codecs: array(string()),
58984
59732
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
58985
- sampleRate: number(),
59733
+ sampleRate: number().meta(readingMeta("none")),
58986
59734
  duplex: _enum(["half", "full"]),
58987
59735
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
58988
- maxBacklogMs: number()
59736
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
58989
59737
  });
58990
59738
  var IntercomStatusSchema = object({
58991
59739
  /** True while a talk session is open. */
@@ -59096,13 +59844,13 @@ object({
59096
59844
  activity: LawnMowerActivitySchema,
59097
59845
  /** 0..100 battery percentage. Null when the device has no battery
59098
59846
  * reading. */
59099
- batteryLevel: number().min(0).max(100).nullable(),
59847
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59100
59848
  /** 0..100 mowing-completion percentage of the current task, or null when no
59101
59849
  * task is active / progress is unavailable. */
59102
- progressPercent: number().min(0).max(100).nullable(),
59850
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59103
59851
  /** Current device/event code (dynamic — mostly status, sometimes an error),
59104
59852
  * or null when unknown. */
59105
- currentCode: number().nullable(),
59853
+ currentCode: number().nullable().meta(readingMeta("none")),
59106
59854
  /** Human label for {@link currentCode}, or null when undecodable. */
59107
59855
  currentCodeLabel: string().nullable(),
59108
59856
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -59512,7 +60260,14 @@ DeviceType.Lock, method(object({
59512
60260
  auth: "protected"
59513
60261
  }), method(object({
59514
60262
  deviceId: number().int().nonnegative(),
59515
- code: string().min(1).optional()
60263
+ code: string().min(1).optional(),
60264
+ /**
60265
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
60266
+ * with one call is refused unless the caller says it was confirmed. A
60267
+ * single lock ignores it. The refusal lives in the callee, so no
60268
+ * automation or API caller reaches the doors by skipping a dialog.
60269
+ */
60270
+ confirm: boolean().optional()
59516
60271
  }), _void(), {
59517
60272
  kind: "mutation",
59518
60273
  auth: "protected"
@@ -59520,6 +60275,11 @@ DeviceType.Lock, method(object({
59520
60275
  kind: "mutation",
59521
60276
  auth: "protected"
59522
60277
  });
60278
+ object({
60279
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
60280
+ lastFetchedAt: number()
60281
+ });
60282
+ DeviceType.Sensor;
59523
60283
  /**
59524
60284
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
59525
60285
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
@@ -59564,7 +60324,7 @@ object({
59564
60324
  state: MediaPlayerStateSchema,
59565
60325
  /** Volume as 0..100 inclusive. Null when the device has no volume
59566
60326
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
59567
- volumeLevel: number().min(0).max(100).nullable(),
60327
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59568
60328
  /** Mute toggle distinct from volume=0. Null when no mute surface.
59569
60329
  * Pair with `DeviceFeature.MediaPlayerMute`. */
59570
60330
  isMuted: boolean().nullable(),
@@ -59577,10 +60337,10 @@ object({
59577
60337
  currentMedia: MediaInfoSchema.nullable(),
59578
60338
  /** Current playback position in ms. Null when not seekable or
59579
60339
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
59580
- positionMs: number().int().nonnegative().nullable(),
60340
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59581
60341
  /** Total duration of the current media in ms. Null when unknown
59582
60342
  * (live stream) or nothing is playing. */
59583
- durationMs: number().int().nonnegative().nullable(),
60343
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59584
60344
  /** Shuffle toggle. Null when no shuffle surface.
59585
60345
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
59586
60346
  shuffle: boolean().nullable(),
@@ -59880,7 +60640,7 @@ object({
59880
60640
  * it, every provider seeded it at boot, and every session left it
59881
60641
  * `detected: true` after its detach.
59882
60642
  */
59883
- autoClearAfterMs: number().nullable()
60643
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
59884
60644
  });
59885
60645
  /**
59886
60646
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -59930,7 +60690,7 @@ var MotionZoneRegionSchema = object({
59930
60690
  });
59931
60691
  object({
59932
60692
  enabled: boolean(),
59933
- sensitivity: number(),
60693
+ sensitivity: number().meta(readingMeta("none")),
59934
60694
  /** Grid region(s). Today exactly one `grid` shape. */
59935
60695
  regions: array(MotionZoneRegionSchema),
59936
60696
  lastFetchedAt: number()
@@ -60234,7 +60994,7 @@ var NavigationStatusSchema = object({
60234
60994
  following: boolean(),
60235
60995
  flash: boolean(),
60236
60996
  lightMode: NavigationLightModeSchema,
60237
- lightLevel: number().min(40).max(100),
60997
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
60238
60998
  /** Ms epoch when the slice was last updated. */
60239
60999
  lastChangedAt: number()
60240
61000
  });
@@ -60280,13 +61040,13 @@ var NetworkLinkStatusSchema = object({
60280
61040
  * one whose reading has not landed must not be drawn at 0 %. Consumers
60281
61041
  * SKIP a null rather than coerce it.
60282
61042
  */
60283
- signalPercent: number().min(0).max(100).nullable(),
61043
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60284
61044
  /** Raw received signal strength in dBm, when the firmware reports one. */
60285
- rssiDbm: number().optional(),
61045
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
60286
61046
  /** Network name of a wireless link, when the firmware reports it. */
60287
61047
  ssid: string().optional(),
60288
61048
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
60289
- lastUpdated: number()
61049
+ lastUpdated: number().meta(readingMeta("none"))
60290
61050
  });
60291
61051
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
60292
61052
  deviceId: number(),
@@ -60605,7 +61365,7 @@ object({
60605
61365
  lastError: string().nullable(),
60606
61366
  /** Number of deliveries currently buffered server-side (rate-limited
60607
61367
  * notifiers may queue). 0 when the provider sends synchronously. */
60608
- queueDepth: number().int().nonnegative(),
61368
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
60609
61369
  /** Per-feature capability matrix — authoritative source for the
60610
61370
  * compose-form field gating. */
60611
61371
  supports: NotifierSupportsSchema
@@ -60655,15 +61415,8 @@ DeviceType.Notifier, method(NotifierSendInputSchema, NotifierSendResultSchema, {
60655
61415
  acceptedAt: number()
60656
61416
  });
60657
61417
  object({
60658
- value: number(),
60659
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
60660
- * Populated live from the upstream source on each state push.
60661
- * Absent for unitless sensors. */
60662
- unit: string().optional(),
60663
- /** Suggested decimal places for numeric display.
60664
- * Populated live from the upstream source when provided.
60665
- * Falls back to auto-formatting when absent. */
60666
- precision: number().int().min(0).max(10).optional(),
61418
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
61419
+ value: number().nullable().meta(readingMeta("device")),
60667
61420
  /** Ms epoch when the slice was last updated. */
60668
61421
  lastFetchedAt: number()
60669
61422
  });
@@ -60953,23 +61706,26 @@ object({
60953
61706
  /** Food currently in the bowl (grams). Null when the device has not
60954
61707
  * reported a reading yet. On dual-hopper models this is the combined
60955
61708
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
60956
- foodLevel: number().nullable(),
61709
+ foodLevel: number().nullable().meta(readingMeta("none")),
60957
61710
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
60958
61711
  * single-hopper models. */
60959
- food1: number().nullable(),
61712
+ food1: number().nullable().meta(readingMeta("none")),
60960
61713
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
60961
61714
  * single-hopper models. */
60962
- food2: number().nullable(),
61715
+ food2: number().nullable().meta(readingMeta("none")),
60963
61716
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
60964
61717
  * (`device_class: problem`, on = low). True when the bowl is empty /
60965
61718
  * below the feeder's low threshold. */
60966
61719
  lowFood: boolean(),
60967
61720
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
60968
61721
  * device has no battery reading. */
60969
- batteryPower: number().min(0).max(100).nullable(),
60970
- /** Days of desiccant life remaining. Null when the model has no
60971
- * desiccant sensor. */
60972
- desiccantLeftDays: number().nullable(),
61722
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61723
+ /**
61724
+ * Desiccant life remaining, stored in seconds (duration base, D944).
61725
+ * Providers convert from the days the feeder reports. Null when the
61726
+ * model has no desiccant sensor.
61727
+ */
61728
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
60973
61729
  /** True while a feed is in progress. */
60974
61730
  feeding: boolean(),
60975
61731
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -60980,7 +61736,7 @@ object({
60980
61736
  * with `errorCode` for consumers that want the raw integer. */
60981
61737
  error: string().nullable(),
60982
61738
  /** Raw device error code (0 / null = no error). */
60983
- errorCode: number().nullable(),
61739
+ errorCode: number().nullable().meta(readingMeta("none")),
60984
61740
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
60985
61741
  isDualHopper: boolean(),
60986
61742
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -60990,7 +61746,7 @@ object({
60990
61746
  /** Play a chime when dispensing. */
60991
61747
  feedSound: boolean(),
60992
61748
  /** Speaker / prompt volume level (device-scaled integer). */
60993
- volume: number(),
61749
+ volume: number().meta(readingMeta("none")),
60994
61750
  /** Ms epoch when the slice was last refreshed from the cloud. */
60995
61751
  lastFetchedAt: number()
60996
61752
  });
@@ -61427,38 +62183,37 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
61427
62183
  });
61428
62184
  object({
61429
62185
  /** Instantaneous power draw in watts. */
61430
- watts: number().optional().meta({
61431
- unit: "W",
61432
- label: "Power"
61433
- }),
62186
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
61434
62187
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
61435
- kwhTotal: number().optional().meta({
61436
- unit: "kWh",
61437
- label: "Energy"
61438
- }),
62188
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
61439
62189
  /** Voltage in volts. */
61440
- volts: number().optional(),
62190
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
61441
62191
  /** Current in amperes. */
61442
- amps: number().optional(),
61443
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
61444
- va: number().optional(),
62192
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
62193
+ /** Apparent power in volt-amperes. */
62194
+ va: number().optional().meta(readingMeta("apparent-power")),
61445
62195
  /** Power factor (0..1) if reported. */
61446
- powerFactor: number().min(0).max(1).optional(),
62196
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
61447
62197
  /** Ms epoch when the slice was last updated. */
61448
- lastFetchedAt: number(),
61449
- /** Live display unit of the single metric this slice carries (e.g. HA
61450
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
61451
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
61452
- * unit per slice is unambiguous. The UI prefers this over the role's
61453
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
61454
- unit: string().optional(),
61455
- /** Suggested decimal places for numeric display.
61456
- * Populated live from the upstream source when provided (e.g. HA
61457
- * `attributes.suggested_display_precision`). Falls back to
61458
- * auto-formatting when absent. */
61459
- precision: number().int().min(0).max(10).optional()
62198
+ lastFetchedAt: number()
61460
62199
  });
61461
- DeviceType.Sensor;
62200
+ [
62201
+ DeviceType.Sensor,
62202
+ DeviceType.Switch,
62203
+ DeviceType.Light,
62204
+ DeviceType.Fan,
62205
+ DeviceType.Climate,
62206
+ DeviceType.Thermostat,
62207
+ DeviceType.Humidifier,
62208
+ DeviceType.WaterHeater,
62209
+ DeviceType.MediaPlayer,
62210
+ DeviceType.Washer,
62211
+ DeviceType.Dryer,
62212
+ DeviceType.Dishwasher,
62213
+ DeviceType.Vacuum,
62214
+ DeviceType.LawnMower,
62215
+ DeviceType.PetFeeder
62216
+ ].filter((type) => type !== DeviceType.Sensor);
61462
62217
  /**
61463
62218
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
61464
62219
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -61477,11 +62232,11 @@ DeviceType.Sensor;
61477
62232
  */
61478
62233
  var GpsLocationSchema = object({
61479
62234
  /** Latitude in decimal degrees, -90..90. */
61480
- latitude: number().min(-90).max(90),
62235
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
61481
62236
  /** Longitude in decimal degrees, -180..180. */
61482
- longitude: number().min(-180).max(180),
62237
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
61483
62238
  /** Reported accuracy in meters (lower = better). */
61484
- accuracyMeters: number().nonnegative()
62239
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
61485
62240
  });
61486
62241
  object({
61487
62242
  /** `home` / `not_home` / any user-defined zone name. */
@@ -61493,25 +62248,16 @@ object({
61493
62248
  gps: GpsLocationSchema.nullable(),
61494
62249
  /** Optional battery level of the tracking device (0..100). Null
61495
62250
  * when not reported. */
61496
- batteryPercent: number().min(0).max(100).nullable(),
62251
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61497
62252
  /** Ms epoch when the slice was last updated. */
61498
62253
  lastChangedAt: number()
61499
62254
  });
61500
62255
  DeviceType.Presence;
61501
62256
  object({
61502
- /** Current pressure in hPa. */
61503
- hpa: number(),
62257
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
62258
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
61504
62259
  /** Ms epoch when the slice was last updated. */
61505
- lastFetchedAt: number(),
61506
- /** Live display unit from the upstream source (e.g. HA
61507
- * `attributes.unit_of_measurement`). The UI prefers this over the
61508
- * role's canonical unit. Absent → fall back to the canonical unit. */
61509
- unit: string().optional(),
61510
- /** Suggested decimal places for numeric display.
61511
- * Populated live from the upstream source when provided (e.g. HA
61512
- * `attributes.suggested_display_precision`). Falls back to
61513
- * auto-formatting when absent. */
61514
- precision: number().int().min(0).max(10).optional()
62260
+ lastFetchedAt: number()
61515
62261
  });
61516
62262
  DeviceType.Sensor;
61517
62263
  /**
@@ -61844,8 +62590,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
61844
62590
  }), _void(), { kind: "mutation" });
61845
62591
  var PtzAutotrackSettingsSchema = object({
61846
62592
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
61847
- stopDelaySeconds: number().int().min(0).max(300),
61848
- disappearDelaySeconds: number().int().min(0).max(300)
62593
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
62594
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
61849
62595
  });
61850
62596
  /**
61851
62597
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -61902,7 +62648,7 @@ object({
61902
62648
  /** null = unknown — never coerced to false (D393). */
61903
62649
  running: boolean().nullable(),
61904
62650
  /** Epoch ms the current boolean began; null while `running` is null. */
61905
- since: number().nullable(),
62651
+ since: number().nullable().meta(readingMeta("none")),
61906
62652
  source: RunningStateSourceSchema.meta({ label: "Source" }),
61907
62653
  phase: string().nullable(),
61908
62654
  /** null until a reading has been published (D393). */
@@ -61914,7 +62660,7 @@ object({
61914
62660
  phase: string().nullable()
61915
62661
  });
61916
62662
  Object.values(DeviceType);
61917
- DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
62663
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
61918
62664
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
61919
62665
  kind: "mutation",
61920
62666
  auth: "admin"
@@ -63100,9 +63846,9 @@ var RecordTriggerSchema = _enum([
63100
63846
  */
63101
63847
  var RecordWindowSchema = object({
63102
63848
  trigger: RecordTriggerSchema,
63103
- day: number().int().min(0).max(6),
63104
- startMinute: number().int().min(0).max(1439),
63105
- endMinute: number().int().min(1).max(1440)
63849
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
63850
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
63851
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
63106
63852
  });
63107
63853
  /** Status of one physical volume, as the camera itself describes it. */
63108
63854
  var OnboardStorageVolumeSchema = object({
@@ -63171,10 +63917,10 @@ object({
63171
63917
  /** Master "record to the card at all" switch. */
63172
63918
  enabled: boolean().nullable(),
63173
63919
  overwriteWhenFull: boolean().nullable(),
63174
- preRecordSec: number().nullable(),
63175
- postRecordSec: number().nullable(),
63920
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
63921
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
63176
63922
  /** Length of one recorded file, in minutes. */
63177
- segmentMinutes: number().nullable(),
63923
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
63178
63924
  /** The primary track's weekly windows, flattened. */
63179
63925
  windows: array(RecordWindowSchema),
63180
63926
  /**
@@ -63190,7 +63936,7 @@ object({
63190
63936
  * provider reporting a non-zero count also reports the schedule as not
63191
63937
  * writable.
63192
63938
  */
63193
- unreadableWindows: number(),
63939
+ unreadableWindows: number().meta(readingMeta("count")),
63194
63940
  /**
63195
63941
  * The camera is scheduled to record and has NO usable storage.
63196
63942
  *
@@ -63827,7 +64573,7 @@ var CamStreamDescriptorSchema = object({
63827
64573
  url: string().optional(),
63828
64574
  codec: string().optional(),
63829
64575
  resolution: CamStreamResolutionSchema.optional(),
63830
- fps: number().positive().optional(),
64576
+ fps: number().positive().optional().meta(readingMeta("rate")),
63831
64577
  label: string().optional(),
63832
64578
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
63833
64579
  deviceFeatures: array(string()).optional(),
@@ -63857,18 +64603,18 @@ var StreamProfileSchema = _enum([
63857
64603
  ]);
63858
64604
  /** The current encoding config of one stream profile. */
63859
64605
  var StreamProfileConfigSchema = object({
63860
- width: number(),
63861
- height: number(),
64606
+ width: number().meta(readingMeta("none")),
64607
+ height: number().meta(readingMeta("none")),
63862
64608
  codec: _enum(["h264", "h265"]),
63863
- framerate: number(),
63864
- bitrate: number(),
64609
+ framerate: number().meta(readingMeta("rate")),
64610
+ bitrate: number().meta(readingMeta("bitrate")),
63865
64611
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
63866
64612
  encoderProfile: _enum([
63867
64613
  "high",
63868
64614
  "main",
63869
64615
  "baseline"
63870
64616
  ]).optional(),
63871
- gop: number().optional(),
64617
+ gop: number().optional().meta(readingMeta("none")),
63872
64618
  /**
63873
64619
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
63874
64620
  *
@@ -63953,7 +64699,7 @@ object({
63953
64699
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
63954
64700
  lastChangedAt: number()
63955
64701
  });
63956
- DeviceType.Switch, DeviceType.Siren, DeviceType.Light, method(object({
64702
+ DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, method(object({
63957
64703
  deviceId: number(),
63958
64704
  on: boolean()
63959
64705
  }), _void(), {
@@ -64511,23 +65257,13 @@ object({
64511
65257
  });
64512
65258
  DeviceType.Sensor;
64513
65259
  object({
64514
- /** Current temperature in Celsius. */
64515
- celsius: number().meta({
64516
- unit: "°C",
65260
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
65261
+ celsius: number().nullable().meta(readingMeta("temperature", {
65262
+ precision: 1,
64517
65263
  label: "Temperature"
64518
- }),
65264
+ })),
64519
65265
  /** Ms epoch when the slice was last updated (push or poll). */
64520
- lastFetchedAt: number(),
64521
- /** Live display unit from the upstream source (e.g. HA
64522
- * `attributes.unit_of_measurement`). The UI prefers this over the
64523
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
64524
- * Absent → the UI falls back to the role's canonical unit. */
64525
- unit: string().optional(),
64526
- /** Suggested decimal places for numeric display.
64527
- * Populated live from the upstream source when provided (e.g. HA
64528
- * `attributes.suggested_display_precision`). Falls back to
64529
- * auto-formatting when absent. */
64530
- precision: number().int().min(0).max(10).optional()
65266
+ lastFetchedAt: number()
64531
65267
  });
64532
65268
  DeviceType.Sensor, DeviceType.Thermostat;
64533
65269
  /**
@@ -64779,16 +65515,16 @@ object({
64779
65515
  ]),
64780
65516
  /** 0..100 battery percentage. Null when the device has no battery
64781
65517
  * reading. */
64782
- batteryLevel: number().min(0).max(100).nullable(),
65518
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64783
65519
  /** Current fan-speed token (provider-verbatim). Null when unknown or
64784
65520
  * the vacuum has no speed control. */
64785
65521
  fanSpeed: string().nullable(),
64786
65522
  /** Speed tokens the hardware accepts — drives the UI selector. */
64787
65523
  availableFanSpeeds: array(string()),
64788
65524
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64789
- progressPercent: number().min(0).max(100).nullable(),
65525
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64790
65526
  /** Current error code (0 / null = no error). */
64791
- errorCode: number().nullable(),
65527
+ errorCode: number().nullable().meta(readingMeta("none")),
64792
65528
  /** Human label for {@link errorCode}, or null when none / undecodable. */
64793
65529
  errorLabel: string().nullable(),
64794
65530
  /** Ms epoch when the slice was last updated. */
@@ -64827,7 +65563,7 @@ object({
64827
65563
  ]),
64828
65564
  /** 0 = fully closed, 100 = fully open. Null when the device has no
64829
65565
  * intermediate position surface. */
64830
- position: number().min(0).max(100).nullable(),
65566
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64831
65567
  /** Ms epoch when the slice was last updated. */
64832
65568
  lastChangedAt: number()
64833
65569
  });
@@ -64855,9 +65591,9 @@ object({
64855
65591
  DeviceType.Sensor;
64856
65592
  object({
64857
65593
  /** Current measured temperature. Null when not reported. */
64858
- currentTemp: number().nullable(),
65594
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
64859
65595
  /** Target temperature setpoint. Null when no setpoint surface. */
64860
- targetTemp: number().nullable(),
65596
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
64861
65597
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
64862
65598
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
64863
65599
  * device reports an unknown state. */
@@ -64868,9 +65604,9 @@ object({
64868
65604
  * has no away surface. */
64869
65605
  away: boolean().nullable(),
64870
65606
  /** HA `min_temp` attribute. Null when not reported. */
64871
- minTemp: number().nullable(),
65607
+ minTemp: number().nullable().meta(readingMeta("temperature")),
64872
65608
  /** HA `max_temp` attribute. Null when not reported. */
64873
- maxTemp: number().nullable(),
65609
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
64874
65610
  /** Ms epoch when the slice was last updated. */
64875
65611
  lastChangedAt: number()
64876
65612
  });
@@ -64894,25 +65630,18 @@ DeviceType.WaterHeater, method(object({
64894
65630
  auth: "protected"
64895
65631
  });
64896
65632
  object({
64897
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
64898
- * when no condition has been reported yet. */
65633
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
64899
65634
  condition: string().nullable(),
64900
- /** Current temperature in the reported unit. Null when not provided. */
64901
- temperature: number().nullable(),
64902
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
64903
- temperatureUnit: string().nullable(),
65635
+ /** Current temperature in Celsius. Null when not provided or refused. */
65636
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
64904
65637
  /** Relative humidity (0..100). Null when not provided. */
64905
- humidity: number().min(0).max(100).nullable(),
64906
- /** Barometric pressure in the reported unit. Null when not provided. */
64907
- pressure: number().nullable(),
64908
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
64909
- pressureUnit: string().nullable(),
64910
- /** Wind speed in the reported unit. Null when not provided. */
64911
- windSpeed: number().nullable(),
64912
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
64913
- windSpeedUnit: string().nullable(),
65638
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
65639
+ /** Barometric pressure in hPa. Null when not provided or refused. */
65640
+ pressure: number().nullable().meta(readingMeta("pressure")),
65641
+ /** Wind speed in metres per second. Null when not provided or refused. */
65642
+ windSpeed: number().nullable().meta(readingMeta("speed")),
64914
65643
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
64915
- windBearing: number().nullable(),
65644
+ windBearing: number().nullable().meta(readingMeta("angle")),
64916
65645
  /** Ms epoch when the slice was last updated. */
64917
65646
  lastFetchedAt: number()
64918
65647
  });
@@ -65662,8 +66391,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
65662
66391
  kind: "mutation",
65663
66392
  auth: "admin"
65664
66393
  });
66394
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
65665
66395
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
65666
- 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;
66396
+ 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;
65667
66397
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
65668
66398
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
65669
66399
  new Set(CAP_NAMES);
@@ -65716,7 +66446,7 @@ var AccessoryKind = {
65716
66446
  PrivacyMask: DeviceRole.PrivacyMask
65717
66447
  };
65718
66448
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
65719
- 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;
66449
+ 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;
65720
66450
  new Set(Object.values(DeviceType));
65721
66451
  Object.freeze({
65722
66452
  "accessories.getStatus": {
@@ -65824,6 +66554,13 @@ Object.freeze({
65824
66554
  access: "view",
65825
66555
  role: "admin"
65826
66556
  },
66557
+ "addons.getPackageDocs": {
66558
+ capName: "addons",
66559
+ capScope: "system",
66560
+ addonId: null,
66561
+ access: "view",
66562
+ role: "admin"
66563
+ },
65827
66564
  "addons.getVersions": {
65828
66565
  capName: "addons",
65829
66566
  capScope: "system",
@@ -68932,6 +69669,27 @@ Object.freeze({
68932
69669
  access: "view",
68933
69670
  role: "viewer"
68934
69671
  },
69672
+ "groupActive.getStatus": {
69673
+ capName: "group-active",
69674
+ capScope: "device",
69675
+ addonId: null,
69676
+ access: "view",
69677
+ role: "viewer"
69678
+ },
69679
+ "groupCommands.getStatus": {
69680
+ capName: "group-commands",
69681
+ capScope: "device",
69682
+ addonId: null,
69683
+ access: "view",
69684
+ role: "viewer"
69685
+ },
69686
+ "groupStatus.getStatus": {
69687
+ capName: "group-status",
69688
+ capScope: "device",
69689
+ addonId: null,
69690
+ access: "view",
69691
+ role: "viewer"
69692
+ },
68935
69693
  "heatingValve.getStatus": {
68936
69694
  capName: "heating-valve",
68937
69695
  capScope: "device",
@@ -69884,6 +70642,13 @@ Object.freeze({
69884
70642
  access: "create",
69885
70643
  role: "admin"
69886
70644
  },
70645
+ "meter.getStatus": {
70646
+ capName: "meter",
70647
+ capScope: "device",
70648
+ addonId: null,
70649
+ access: "view",
70650
+ role: "viewer"
70651
+ },
69887
70652
  "metricsProvider.collectSnapshot": {
69888
70653
  capName: "metrics-provider",
69889
70654
  capScope: "system",
@@ -77982,7 +78747,39 @@ var APPLIANCE_CAP_VOCABULARY = {
77982
78747
  phrase: t("When {device} finishes", "Quando {device} finisce"),
77983
78748
  icon: "square"
77984
78749
  }
77985
- }
78750
+ },
78751
+ overrides: { byType: {
78752
+ [DeviceType.Washer]: {
78753
+ state: { running: {
78754
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
78755
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
78756
+ } },
78757
+ events: {
78758
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
78759
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
78760
+ }
78761
+ },
78762
+ [DeviceType.Dryer]: {
78763
+ state: { running: {
78764
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
78765
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
78766
+ } },
78767
+ events: {
78768
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
78769
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
78770
+ }
78771
+ },
78772
+ [DeviceType.Dishwasher]: {
78773
+ state: { running: {
78774
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
78775
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
78776
+ } },
78777
+ events: {
78778
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
78779
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
78780
+ }
78781
+ }
78782
+ } }
77986
78783
  },
77987
78784
  update: {
77988
78785
  scope: "device",
@@ -78754,6 +79551,41 @@ var SENSOR_CAP_VOCABULARY = {
78754
79551
  icon: "flame"
78755
79552
  }]
78756
79553
  },
79554
+ "group-active": {
79555
+ scope: "device",
79556
+ noun: t("Group state", "Stato del gruppo")
79557
+ },
79558
+ "group-commands": {
79559
+ scope: "device",
79560
+ noun: t("Group command", "Comando di gruppo")
79561
+ },
79562
+ "group-status": {
79563
+ scope: "device",
79564
+ noun: t("Group", "Gruppo"),
79565
+ state: [
79566
+ {
79567
+ kind: "numeric",
79568
+ field: "included",
79569
+ quantity: t("the members reporting", "i membri che rispondono"),
79570
+ label: t("Reporting", "Rispondono"),
79571
+ icon: "users"
79572
+ },
79573
+ {
79574
+ kind: "numeric",
79575
+ field: "excluded",
79576
+ quantity: t("the members left out", "i membri esclusi"),
79577
+ label: t("Excluded", "Esclusi"),
79578
+ icon: "users"
79579
+ },
79580
+ {
79581
+ kind: "numeric",
79582
+ field: "total",
79583
+ quantity: t("the member count", "il numero di membri"),
79584
+ label: t("Members", "Membri"),
79585
+ icon: "sigma"
79586
+ }
79587
+ ]
79588
+ },
78757
79589
  "humidity-sensor": {
78758
79590
  scope: "device",
78759
79591
  noun: t("Humidity", "Umidità"),
@@ -78779,6 +79611,17 @@ var SENSOR_CAP_VOCABULARY = {
78779
79611
  }],
78780
79612
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
78781
79613
  },
79614
+ meter: {
79615
+ scope: "device",
79616
+ noun: t("Meter", "Contatore"),
79617
+ state: [{
79618
+ kind: "numeric",
79619
+ field: "value",
79620
+ quantity: t("the total", "il totale"),
79621
+ label: t("Total", "Totale"),
79622
+ icon: "gauge"
79623
+ }]
79624
+ },
78782
79625
  "numeric-sensor": {
78783
79626
  scope: "device",
78784
79627
  noun: t("Sensor", "Sensore"),
@@ -78787,7 +79630,6 @@ var SENSOR_CAP_VOCABULARY = {
78787
79630
  field: "value",
78788
79631
  quantity: t("the reading", "il valore"),
78789
79632
  label: t("Reading", "Valore"),
78790
- unitField: "unit",
78791
79633
  icon: "gauge"
78792
79634
  }]
78793
79635
  },
@@ -78923,7 +79765,7 @@ var SENSOR_CAP_VOCABULARY = {
78923
79765
  field: "temperature",
78924
79766
  quantity: t("the outdoor temperature", "la temperatura esterna"),
78925
79767
  label: t("Temperature", "Temperatura"),
78926
- unitField: "temperatureUnit",
79768
+ unit: "°C",
78927
79769
  icon: "thermometer"
78928
79770
  },
78929
79771
  {
@@ -78939,7 +79781,7 @@ var SENSOR_CAP_VOCABULARY = {
78939
79781
  field: "windSpeed",
78940
79782
  quantity: t("the wind speed", "la velocità del vento"),
78941
79783
  label: t("Wind", "Vento"),
78942
- unitField: "windSpeedUnit",
79784
+ unit: "m/s",
78943
79785
  icon: "wind"
78944
79786
  },
78945
79787
  {
@@ -78947,7 +79789,7 @@ var SENSOR_CAP_VOCABULARY = {
78947
79789
  field: "pressure",
78948
79790
  quantity: t("the air pressure", "la pressione atmosferica"),
78949
79791
  label: t("Pressure", "Pressione"),
78950
- unitField: "pressureUnit",
79792
+ unit: "hPa",
78951
79793
  icon: "gauge"
78952
79794
  }
78953
79795
  ]
@@ -79252,8 +80094,47 @@ function projectAction(cap, action) {
79252
80094
  };
79253
80095
  }
79254
80096
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
79255
- 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;
80097
+ 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;
79256
80098
  DeviceFeature.BatteryOperated;
80099
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
80100
+ 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.";
80101
+ 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.";
80102
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
80103
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
80104
+ 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.";
80105
+ function withheld(reason) {
80106
+ return {
80107
+ [DeviceType.Washer]: {
80108
+ exported: false,
80109
+ reason
80110
+ },
80111
+ [DeviceType.Dryer]: {
80112
+ exported: false,
80113
+ reason
80114
+ },
80115
+ [DeviceType.Dishwasher]: {
80116
+ exported: false,
80117
+ reason
80118
+ }
80119
+ };
80120
+ }
80121
+ function offered(received) {
80122
+ return {
80123
+ [DeviceType.Washer]: {
80124
+ exported: true,
80125
+ as: received
80126
+ },
80127
+ [DeviceType.Dryer]: {
80128
+ exported: true,
80129
+ as: received
80130
+ },
80131
+ [DeviceType.Dishwasher]: {
80132
+ exported: true,
80133
+ as: received
80134
+ }
80135
+ };
80136
+ }
80137
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
79257
80138
  Object.freeze({
79258
80139
  "accessories.getStatus": [{
79259
80140
  name: "deviceId",
@@ -80220,6 +81101,21 @@ Object.freeze({
80220
81101
  form: "single",
80221
81102
  optional: false
80222
81103
  }],
81104
+ "groupActive.getStatus": [{
81105
+ name: "deviceId",
81106
+ form: "single",
81107
+ optional: false
81108
+ }],
81109
+ "groupCommands.getStatus": [{
81110
+ name: "deviceId",
81111
+ form: "single",
81112
+ optional: false
81113
+ }],
81114
+ "groupStatus.getStatus": [{
81115
+ name: "deviceId",
81116
+ form: "single",
81117
+ optional: false
81118
+ }],
80223
81119
  "heatingValve.getStatus": [{
80224
81120
  name: "deviceId",
80225
81121
  form: "single",
@@ -80420,6 +81316,11 @@ Object.freeze({
80420
81316
  form: "single",
80421
81317
  optional: false
80422
81318
  }],
81319
+ "meter.getStatus": [{
81320
+ name: "deviceId",
81321
+ form: "single",
81322
+ optional: false
81323
+ }],
80423
81324
  "motion.getStatus": [{
80424
81325
  name: "deviceId",
80425
81326
  form: "single",
@@ -82027,7 +82928,6 @@ Object.freeze({
82027
82928
  "network-access": "ingress",
82028
82929
  "smtp-provider": "email"
82029
82930
  });
82030
- DeviceType.Camera;
82031
82931
  /**
82032
82932
  * A saved Customize assignment (D936). The schema is the type: callers use
82033
82933
  * `z.infer`, they do not restate the shape.
@@ -82204,6 +83104,8 @@ new Map(Object.entries({
82204
83104
  "detection-blind": "device-events",
82205
83105
  "motion-noisy": "device-events",
82206
83106
  "motion-calm": "device-events",
83107
+ "package-llm-silent": "device-events",
83108
+ "package-llm-answered": "device-events",
82207
83109
  "recording-stalled": "device-events",
82208
83110
  "recording-healthy": "device-events",
82209
83111
  "recording-degraded": "device-events",
@@ -82287,6 +83189,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
82287
83189
  label: "Motion without a subject",
82288
83190
  kinds: ["motion-noisy", "motion-calm"]
82289
83191
  },
83192
+ {
83193
+ id: "package-llm",
83194
+ family: "device-events",
83195
+ label: "Package confirm",
83196
+ kinds: ["package-llm-silent", "package-llm-answered"]
83197
+ },
82290
83198
  {
82291
83199
  id: "recording",
82292
83200
  family: "device-events",
@@ -82468,6 +83376,8 @@ var DEVICE_EVENTS = [
82468
83376
  "detection-blind",
82469
83377
  "motion-noisy",
82470
83378
  "motion-calm",
83379
+ "package-llm-silent",
83380
+ "package-llm-answered",
82471
83381
  "recording-stalled",
82472
83382
  "recording-healthy",
82473
83383
  "recording-degraded",
@@ -82736,6 +83646,15 @@ new Map([
82736
83646
  snapshotLabel: "Cameras with constant motion now",
82737
83647
  snapshotSection: null
82738
83648
  },
83649
+ {
83650
+ id: "package-llm-confirm",
83651
+ family: "device-events",
83652
+ label: "Package confirm (not answering / answering again)",
83653
+ bad: ["package-llm-silent"],
83654
+ recovery: ["package-llm-answered"],
83655
+ snapshotLabel: "Cameras whose package confirm is not answering",
83656
+ snapshotSection: null
83657
+ },
82739
83658
  {
82740
83659
  id: "recording-health",
82741
83660
  family: "device-events",