@camstack/addon-waste-collections 0.1.29 → 0.1.31

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.
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { spawn } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
- //#region ../types/dist/event-category-DHXxB8t9.mjs
5
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
6
6
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
7
7
  EventCategory["SystemBoot"] = "system.boot";
8
8
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -328,6 +328,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
328
328
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
329
329
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
330
330
  /**
331
+ * A camera's package LLM confirm has failed closed three times in a row
332
+ * (D931, 2026-10-26). One edge per camera until
333
+ * {@link PipelinePackageLlmAnswered}.
334
+ */
335
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
336
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
337
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
338
+ /**
331
339
  * Per-camera pipeline config was mutated by the orchestrator
332
340
  * (a settings change via `setAgentInferenceDevices` /
333
341
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5570,7 +5578,7 @@ var ZodIssueCode = {
5570
5578
  var ZodFirstPartyTypeKind;
5571
5579
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5572
5580
  //#endregion
5573
- //#region ../types/dist/sleep-BErAxhul.mjs
5581
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5574
5582
  /**
5575
5583
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5576
5584
  * window to float samples (D455).
@@ -8322,6 +8330,99 @@ function defineEntryKind(name, spec) {
8322
8330
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8323
8331
  return defined;
8324
8332
  }
8333
+ var READING_QUANTITIES = [
8334
+ "temperature",
8335
+ "pressure",
8336
+ "speed",
8337
+ "precipitation",
8338
+ "precipitation-rate",
8339
+ "length",
8340
+ "area",
8341
+ "illuminance",
8342
+ "irradiance",
8343
+ "power",
8344
+ "energy",
8345
+ "voltage",
8346
+ "current",
8347
+ "apparent-power",
8348
+ "humidity",
8349
+ "percentage",
8350
+ "concentration-volume",
8351
+ "concentration-mass",
8352
+ "concentration-ppb",
8353
+ "volume",
8354
+ "duration",
8355
+ "duration-ms",
8356
+ "information",
8357
+ "sound-level",
8358
+ "signal-level",
8359
+ "colour-temperature",
8360
+ "angle",
8361
+ "bitrate",
8362
+ "rate",
8363
+ "mass",
8364
+ "index",
8365
+ "ratio",
8366
+ "count",
8367
+ "none",
8368
+ "device"
8369
+ ];
8370
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8371
+ new Set(READING_QUANTITIES);
8372
+ /**
8373
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8374
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8375
+ */
8376
+ var QUANTITY_BASE = {
8377
+ temperature: "°C",
8378
+ pressure: "hPa",
8379
+ speed: "m/s",
8380
+ precipitation: "mm",
8381
+ "precipitation-rate": "mm/h",
8382
+ length: "m",
8383
+ area: "m²",
8384
+ illuminance: "lx",
8385
+ irradiance: "W/m²",
8386
+ power: "W",
8387
+ energy: "kWh",
8388
+ voltage: "V",
8389
+ current: "A",
8390
+ "apparent-power": "VA",
8391
+ humidity: "%",
8392
+ percentage: "%",
8393
+ "concentration-volume": "ppm",
8394
+ "concentration-mass": "µg/m³",
8395
+ "concentration-ppb": "ppb",
8396
+ volume: "L",
8397
+ duration: "s",
8398
+ "duration-ms": "ms",
8399
+ information: "B",
8400
+ "sound-level": "dBFS",
8401
+ "signal-level": "dBm",
8402
+ "colour-temperature": "mired",
8403
+ angle: "°",
8404
+ bitrate: "kbps",
8405
+ rate: "fps",
8406
+ mass: "kg",
8407
+ index: "none",
8408
+ ratio: "none",
8409
+ count: "none",
8410
+ none: "none",
8411
+ device: "device"
8412
+ };
8413
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8414
+ function baseUnitOf(quantity) {
8415
+ const symbol = QUANTITY_BASE[quantity];
8416
+ if (symbol === "none" || symbol === "device") return void 0;
8417
+ return symbol;
8418
+ }
8419
+ function readingMeta(quantity, extras) {
8420
+ return {
8421
+ quantity,
8422
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8423
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8424
+ };
8425
+ }
8325
8426
  /** Shared Zod schemas used across streaming capabilities. */
8326
8427
  var CamProfileSchema = _enum([
8327
8428
  "high",
@@ -8349,8 +8450,8 @@ var CamStreamKindSchema = _enum([
8349
8450
  "derived"
8350
8451
  ]);
8351
8452
  var CamStreamResolutionSchema = object({
8352
- width: number().int().positive(),
8353
- height: number().int().positive()
8453
+ width: number().int().positive().meta(readingMeta("none")),
8454
+ height: number().int().positive().meta(readingMeta("none"))
8354
8455
  });
8355
8456
  var CameraStreamSchema = object({
8356
8457
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8886,6 +8987,9 @@ var CAP_NAMES = Object.freeze([
8886
8987
  "filesystem-browse",
8887
8988
  "flood",
8888
8989
  "gas",
8990
+ "group-active",
8991
+ "group-commands",
8992
+ "group-status",
8889
8993
  "heating-valve",
8890
8994
  "humidifier",
8891
8995
  "humidity-sensor",
@@ -8906,6 +9010,7 @@ var CAP_NAMES = Object.freeze([
8906
9010
  "login-method",
8907
9011
  "media-player",
8908
9012
  "mesh-network",
9013
+ "meter",
8909
9014
  "metrics-provider",
8910
9015
  "model-convert",
8911
9016
  "model-distributor",
@@ -9015,14 +9120,6 @@ Object.freeze({
9015
9120
  {
9016
9121
  "path": "aqi",
9017
9122
  "valueType": "number"
9018
- },
9019
- {
9020
- "path": "unit",
9021
- "valueType": "string"
9022
- },
9023
- {
9024
- "path": "precision",
9025
- "valueType": "number"
9026
9123
  }
9027
9124
  ],
9028
9125
  "alarm-panel": [
@@ -9058,20 +9155,10 @@ Object.freeze({
9058
9155
  "valueType": "boolean"
9059
9156
  }
9060
9157
  ],
9061
- "ambient-light-sensor": [
9062
- {
9063
- "path": "lux",
9064
- "valueType": "number"
9065
- },
9066
- {
9067
- "path": "unit",
9068
- "valueType": "string"
9069
- },
9070
- {
9071
- "path": "precision",
9072
- "valueType": "number"
9073
- }
9074
- ],
9158
+ "ambient-light-sensor": [{
9159
+ "path": "lux",
9160
+ "valueType": "number"
9161
+ }],
9075
9162
  "audio-metrics": [
9076
9163
  {
9077
9164
  "path": "ts",
@@ -9151,6 +9238,10 @@ Object.freeze({
9151
9238
  "path": "lastUpdated",
9152
9239
  "valueType": "number"
9153
9240
  },
9241
+ {
9242
+ "path": "volts",
9243
+ "valueType": "number"
9244
+ },
9154
9245
  {
9155
9246
  "path": "lastContactAt",
9156
9247
  "valueType": "number"
@@ -9393,10 +9484,6 @@ Object.freeze({
9393
9484
  "datetime"
9394
9485
  ]
9395
9486
  },
9396
- {
9397
- "path": "unit",
9398
- "valueType": "string"
9399
- },
9400
9487
  {
9401
9488
  "path": "min",
9402
9489
  "valueType": "number"
@@ -9408,19 +9495,6 @@ Object.freeze({
9408
9495
  {
9409
9496
  "path": "step",
9410
9497
  "valueType": "number"
9411
- },
9412
- {
9413
- "path": "precision",
9414
- "valueType": "number"
9415
- },
9416
- {
9417
- "path": "format",
9418
- "valueType": "enum",
9419
- "enumValues": [
9420
- "date",
9421
- "time",
9422
- "datetime"
9423
- ]
9424
9498
  }
9425
9499
  ],
9426
9500
  "cover": [
@@ -9661,13 +9735,8 @@ Object.freeze({
9661
9735
  "path": "value",
9662
9736
  "valueType": "string"
9663
9737
  }, {
9664
- "path": "format",
9665
- "valueType": "enum",
9666
- "enumValues": [
9667
- "date",
9668
- "time",
9669
- "datetime"
9670
- ]
9738
+ "path": "timestamp",
9739
+ "valueType": "boolean"
9671
9740
  }],
9672
9741
  "event-emitter": [
9673
9742
  {
@@ -9740,6 +9809,24 @@ Object.freeze({
9740
9809
  "path": "detected",
9741
9810
  "valueType": "boolean"
9742
9811
  }],
9812
+ "group-active": [{
9813
+ "path": "active",
9814
+ "valueType": "boolean"
9815
+ }],
9816
+ "group-status": [
9817
+ {
9818
+ "path": "included",
9819
+ "valueType": "number"
9820
+ },
9821
+ {
9822
+ "path": "excluded",
9823
+ "valueType": "number"
9824
+ },
9825
+ {
9826
+ "path": "total",
9827
+ "valueType": "number"
9828
+ }
9829
+ ],
9743
9830
  "heating-valve": [{
9744
9831
  "path": "position",
9745
9832
  "valueType": "number"
@@ -9774,20 +9861,10 @@ Object.freeze({
9774
9861
  "valueType": "number"
9775
9862
  }
9776
9863
  ],
9777
- "humidity-sensor": [
9778
- {
9779
- "path": "percent",
9780
- "valueType": "number"
9781
- },
9782
- {
9783
- "path": "unit",
9784
- "valueType": "string"
9785
- },
9786
- {
9787
- "path": "precision",
9788
- "valueType": "number"
9789
- }
9790
- ],
9864
+ "humidity-sensor": [{
9865
+ "path": "percent",
9866
+ "valueType": "number"
9867
+ }],
9791
9868
  "image": [{
9792
9869
  "path": "url",
9793
9870
  "valueType": "string"
@@ -9995,6 +10072,10 @@ Object.freeze({
9995
10072
  ]
9996
10073
  }
9997
10074
  ],
10075
+ "meter": [{
10076
+ "path": "value",
10077
+ "valueType": "number"
10078
+ }],
9998
10079
  "motion": [
9999
10080
  {
10000
10081
  "path": "detected",
@@ -10126,20 +10207,10 @@ Object.freeze({
10126
10207
  "valueType": "boolean"
10127
10208
  }
10128
10209
  ],
10129
- "numeric-sensor": [
10130
- {
10131
- "path": "value",
10132
- "valueType": "number"
10133
- },
10134
- {
10135
- "path": "unit",
10136
- "valueType": "string"
10137
- },
10138
- {
10139
- "path": "precision",
10140
- "valueType": "number"
10141
- }
10142
- ],
10210
+ "numeric-sensor": [{
10211
+ "path": "value",
10212
+ "valueType": "number"
10213
+ }],
10143
10214
  "pet-feeder": [
10144
10215
  {
10145
10216
  "path": "foodLevel",
@@ -10231,14 +10302,6 @@ Object.freeze({
10231
10302
  {
10232
10303
  "path": "powerFactor",
10233
10304
  "valueType": "number"
10234
- },
10235
- {
10236
- "path": "unit",
10237
- "valueType": "string"
10238
- },
10239
- {
10240
- "path": "precision",
10241
- "valueType": "number"
10242
10305
  }
10243
10306
  ],
10244
10307
  "presence": [
@@ -10267,20 +10330,10 @@ Object.freeze({
10267
10330
  "valueType": "number"
10268
10331
  }
10269
10332
  ],
10270
- "pressure-sensor": [
10271
- {
10272
- "path": "hpa",
10273
- "valueType": "number"
10274
- },
10275
- {
10276
- "path": "unit",
10277
- "valueType": "string"
10278
- },
10279
- {
10280
- "path": "precision",
10281
- "valueType": "number"
10282
- }
10283
- ],
10333
+ "pressure-sensor": [{
10334
+ "path": "hpa",
10335
+ "valueType": "number"
10336
+ }],
10284
10337
  "privacy-mask": [{
10285
10338
  "path": "enabled",
10286
10339
  "valueType": "boolean"
@@ -10591,20 +10644,10 @@ Object.freeze({
10591
10644
  "path": "tampered",
10592
10645
  "valueType": "boolean"
10593
10646
  }],
10594
- "temperature-sensor": [
10595
- {
10596
- "path": "celsius",
10597
- "valueType": "number"
10598
- },
10599
- {
10600
- "path": "unit",
10601
- "valueType": "string"
10602
- },
10603
- {
10604
- "path": "precision",
10605
- "valueType": "number"
10606
- }
10607
- ],
10647
+ "temperature-sensor": [{
10648
+ "path": "celsius",
10649
+ "valueType": "number"
10650
+ }],
10608
10651
  "update": [
10609
10652
  {
10610
10653
  "path": "currentVersion",
@@ -10715,10 +10758,6 @@ Object.freeze({
10715
10758
  "path": "temperature",
10716
10759
  "valueType": "number"
10717
10760
  },
10718
- {
10719
- "path": "temperatureUnit",
10720
- "valueType": "string"
10721
- },
10722
10761
  {
10723
10762
  "path": "humidity",
10724
10763
  "valueType": "number"
@@ -10727,18 +10766,10 @@ Object.freeze({
10727
10766
  "path": "pressure",
10728
10767
  "valueType": "number"
10729
10768
  },
10730
- {
10731
- "path": "pressureUnit",
10732
- "valueType": "string"
10733
- },
10734
10769
  {
10735
10770
  "path": "windSpeed",
10736
10771
  "valueType": "number"
10737
10772
  },
10738
- {
10739
- "path": "windSpeedUnit",
10740
- "valueType": "string"
10741
- },
10742
10773
  {
10743
10774
  "path": "windBearing",
10744
10775
  "valueType": "number"
@@ -10788,7 +10819,7 @@ object({
10788
10819
  params: record(string(), union([string(), number()])).optional()
10789
10820
  });
10790
10821
  //#endregion
10791
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10822
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10792
10823
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10793
10824
  DeviceType["Camera"] = "camera";
10794
10825
  DeviceType["Hub"] = "hub";
@@ -10882,8 +10913,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10882
10913
  DeviceType["PetFeeder"] = "pet-feeder";
10883
10914
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10884
10915
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10885
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10916
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10917
+ * A plug-built dishwasher carries running-state and power instead. */
10886
10918
  DeviceType["Dishwasher"] = "dishwasher";
10919
+ /** Washing machine. A plug-built one is running while its power stays
10920
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10921
+ DeviceType["Washer"] = "washer";
10922
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10923
+ DeviceType["Dryer"] = "dryer";
10887
10924
  return DeviceType;
10888
10925
  }({});
10889
10926
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -11131,7 +11168,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11131
11168
  return DeviceRole;
11132
11169
  }({});
11133
11170
  //#endregion
11134
- //#region ../types/dist/composition-kEl9mevu.mjs
11171
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
11135
11172
  /**
11136
11173
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11137
11174
  *
@@ -11446,6 +11483,13 @@ var table = {
11446
11483
  apply: () => {
11447
11484
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11448
11485
  }
11486
+ },
11487
+ schmitt: {
11488
+ minArgs: 3,
11489
+ maxArgs: 5,
11490
+ apply: () => {
11491
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11492
+ }
11449
11493
  }
11450
11494
  };
11451
11495
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11886,15 +11930,232 @@ function validateExpressionSource(src) {
11886
11930
  }
11887
11931
  return null;
11888
11932
  }
11889
- /** A composed device's stableId is this prefix plus the block id. */
11890
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11891
- /** Longest snippet a `code` source may carry (slice 4). */
11892
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11933
+ /**
11934
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11935
+ *
11936
+ * Standby of the control board is under 5 W; a motor or a heater is far
11937
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11938
+ * a cycle, so running starts only after 30 s above the line.
11939
+ *
11940
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11941
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11942
+ * report "finished" in the middle of the program, so the stop dwell is
11943
+ * 10 minutes on every type. The number is stored per type so one machine
11944
+ * can move later without moving the others.
11945
+ */
11946
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11947
+ var APPLIANCE_KINDS = [
11948
+ "washer",
11949
+ "dryer",
11950
+ "dishwasher"
11951
+ ];
11952
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11953
+ /**
11954
+ * Statistics and integrals over a history read (D944).
11955
+ *
11956
+ * Pure: the same points always give the same answer, so a restart recomputes
11957
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11958
+ * never the mean of the samples. A hole with no held value is not integrated
11959
+ * as zero. A truncated read is not published.
11960
+ */
11961
+ var STATISTIC_STATS = [
11962
+ "min",
11963
+ "max",
11964
+ "mean",
11965
+ "sum",
11966
+ "count",
11967
+ "change"
11968
+ ];
11969
+ /**
11970
+ * The range a statistics or integral helper asks history for (D944).
11971
+ *
11972
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11973
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11974
+ * is strictly longer than the longest that range can be. Forever qualifies.
11975
+ * Equal to a tier's retention is a refusal, not a short answer.
11976
+ */
11977
+ var MINUTE_MS = 6e4;
11978
+ var HOUR_MS = 60 * MINUTE_MS;
11979
+ var DAY_MS$1 = 24 * HOUR_MS;
11980
+ var ROLLING_SPANS = [
11981
+ "1h",
11982
+ "24h",
11983
+ "2d",
11984
+ "7d",
11985
+ "30d",
11986
+ "custom"
11987
+ ];
11988
+ var CALENDAR_PERIODS = [
11989
+ "today",
11990
+ "yesterday",
11991
+ "this-week",
11992
+ "this-month"
11993
+ ];
11994
+ var RESET_PERIODS = [
11995
+ "never",
11996
+ "daily",
11997
+ "weekly",
11998
+ "monthly"
11999
+ ];
12000
+ 5 * MINUTE_MS;
12001
+ 24 * HOUR_MS, 2 * DAY_MS$1, 7 * DAY_MS$1, 30 * DAY_MS$1;
12002
+ 7 * DAY_MS$1, 30 * DAY_MS$1, 365 * DAY_MS$1;
12003
+ var rollingWindowObject = object({
12004
+ kind: literal("rolling"),
12005
+ span: _enum(ROLLING_SPANS),
12006
+ customMinutes: number().int().min(5).optional()
12007
+ }).check((payload) => {
12008
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
12009
+ code: "custom",
12010
+ message: "a custom range needs a duration in minutes",
12011
+ path: ["customMinutes"],
12012
+ input: payload.value
12013
+ });
12014
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
12015
+ code: "custom",
12016
+ message: "only a custom range carries a duration",
12017
+ path: ["customMinutes"],
12018
+ input: payload.value
12019
+ });
12020
+ });
12021
+ var RollingWindowSchema = rollingWindowObject;
12022
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
12023
+ kind: literal("calendar"),
12024
+ period: _enum(CALENDAR_PERIODS)
12025
+ })]);
12026
+ /**
12027
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
12028
+ * id. Split out so helper schemas can name it without a cycle through
12029
+ * `composition.ts` (D659).
12030
+ */
11893
12031
  var CompositionSourceRefSchema = object({
11894
12032
  /** The owning addon; `stableId` is unique only within it. */
11895
12033
  addonId: string().min(1),
11896
12034
  stableId: string().min(1)
11897
12035
  });
12036
+ var GROUP_TEMPLATE_IDS = [
12037
+ "temperature",
12038
+ "humidity",
12039
+ "pressure",
12040
+ "illuminance",
12041
+ "power",
12042
+ "energy",
12043
+ "battery",
12044
+ "numeric",
12045
+ "motion",
12046
+ "contact",
12047
+ "leak",
12048
+ "smoke",
12049
+ "occupancy",
12050
+ "binary",
12051
+ "switch",
12052
+ "light",
12053
+ "fan",
12054
+ "cover",
12055
+ "valve",
12056
+ "lock",
12057
+ "media-player",
12058
+ "climate"
12059
+ ];
12060
+ var NUMERIC_AGGREGATES = [
12061
+ "mean",
12062
+ "median",
12063
+ "min",
12064
+ "max",
12065
+ "sum"
12066
+ ];
12067
+ var BINARY_AGGREGATES = ["any", "all"];
12068
+ /** How an actuator group decides it is on. Unknown members never count as off. */
12069
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
12070
+ var AGGREGATE_REDUCERS = [
12071
+ "mean",
12072
+ "median",
12073
+ "min",
12074
+ "max",
12075
+ "sum",
12076
+ "any",
12077
+ "all"
12078
+ ];
12079
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
12080
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
12081
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
12082
+ /** One member. The field is stored so occupancy can name motion or binary. */
12083
+ var HelperMemberSchema = object({
12084
+ source: CompositionSourceRefSchema,
12085
+ cap: string().min(1),
12086
+ fieldPath: string().min(1)
12087
+ });
12088
+ var HelperSpecSchema = discriminatedUnion("kind", [
12089
+ object({
12090
+ kind: literal("group"),
12091
+ templateId: GroupTemplateIdSchema,
12092
+ aggregate: AggregateReducerSchema,
12093
+ members: array(HelperMemberSchema).min(1).max(64),
12094
+ /** Actuator groups only. Readings leave this unset. */
12095
+ active: ActiveCriterionSchema.optional()
12096
+ }),
12097
+ object({
12098
+ kind: literal("appliance"),
12099
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
12100
+ appliance: _enum(APPLIANCE_KINDS),
12101
+ thresholdWatts: number().positive(),
12102
+ startDwellMs: number().int().nonnegative(),
12103
+ stopDwellMs: number().int().nonnegative(),
12104
+ member: HelperMemberSchema
12105
+ }),
12106
+ object({
12107
+ kind: literal("statistic"),
12108
+ stat: _enum(STATISTIC_STATS),
12109
+ window: StatisticWindowSchema,
12110
+ source: HelperMemberSchema
12111
+ }),
12112
+ object({
12113
+ kind: literal("integral"),
12114
+ reset: _enum(RESET_PERIODS),
12115
+ output: _enum(["energy", "meter"]),
12116
+ source: HelperMemberSchema
12117
+ }),
12118
+ object({
12119
+ kind: literal("threshold"),
12120
+ source: HelperMemberSchema,
12121
+ high: number(),
12122
+ low: number(),
12123
+ onForMs: number().int().nonnegative(),
12124
+ offForMs: number().int().nonnegative()
12125
+ }),
12126
+ object({
12127
+ kind: literal("arithmetic"),
12128
+ op: _enum([
12129
+ "sum",
12130
+ "difference",
12131
+ "ratio"
12132
+ ]),
12133
+ inputs: array(HelperMemberSchema).min(2).max(64)
12134
+ }),
12135
+ object({
12136
+ kind: literal("expression"),
12137
+ expr: string().min(1),
12138
+ inputs: array(object({
12139
+ name: string().min(1),
12140
+ member: HelperMemberSchema
12141
+ })).min(1).max(64),
12142
+ output: _enum(["number", "boolean"])
12143
+ }),
12144
+ object({
12145
+ kind: literal("derivative"),
12146
+ window: RollingWindowSchema,
12147
+ per: _enum([
12148
+ "second",
12149
+ "minute",
12150
+ "hour"
12151
+ ]),
12152
+ source: HelperMemberSchema
12153
+ })
12154
+ ]);
12155
+ /** A composed device's stableId is this prefix plus the block id. */
12156
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12157
+ /** Longest snippet a `code` source may carry (slice 4). */
12158
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11898
12159
  /** One field of one capability of one source device. */
11899
12160
  var CompositionFieldReadSchema = object({
11900
12161
  source: CompositionSourceRefSchema,
@@ -11938,7 +12199,59 @@ var CompositionCodeSourceSchema = object({
11938
12199
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11939
12200
  CompositionFromSourceSchema,
11940
12201
  CompositionExpressionSourceSchema,
11941
- CompositionCodeSourceSchema
12202
+ CompositionCodeSourceSchema,
12203
+ object({
12204
+ kind: literal("aggregate"),
12205
+ reducer: _enum([
12206
+ "mean",
12207
+ "median",
12208
+ "min",
12209
+ "max",
12210
+ "sum",
12211
+ "any",
12212
+ "all"
12213
+ ]),
12214
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12215
+ /**
12216
+ * Set when the reads are an enum state, not a yes/no: the listed states
12217
+ * count as active (a cover is `open`). A state outside the list is inactive;
12218
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
12219
+ */
12220
+ activeWhen: array(string().min(1)).min(1).optional(),
12221
+ /** `value` is the reading; the others are the member counts. */
12222
+ publish: _enum([
12223
+ "value",
12224
+ "included",
12225
+ "excluded",
12226
+ "total"
12227
+ ]),
12228
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12229
+ empty: _enum([
12230
+ "null",
12231
+ "omit",
12232
+ "offline"
12233
+ ])
12234
+ }),
12235
+ object({
12236
+ kind: literal("series"),
12237
+ read: CompositionFieldReadSchema,
12238
+ /** `value` is the statistic or the integral. The others are the gap counts. */
12239
+ publish: _enum([
12240
+ "value",
12241
+ "included",
12242
+ "excluded",
12243
+ "total"
12244
+ ]),
12245
+ /**
12246
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
12247
+ * device unseeded until history answers, so a restart cannot publish 0.
12248
+ */
12249
+ empty: _enum([
12250
+ "null",
12251
+ "omit",
12252
+ "offline"
12253
+ ])
12254
+ })
11942
12255
  ]);
11943
12256
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11944
12257
  kind: literal("forward"),
@@ -11984,7 +12297,13 @@ var CompositionSchema = object({
11984
12297
  kind: literal("existing"),
11985
12298
  device: CompositionSourceRefSchema
11986
12299
  })]),
11987
- features: array(CompositionFeatureSchema).min(1).max(16)
12300
+ features: array(CompositionFeatureSchema).min(1).max(16),
12301
+ /**
12302
+ * Present when the block is a helper template. Absent on a free
12303
+ * composition (the room example, device 4603). The editor reads this,
12304
+ * never the compiled features.
12305
+ */
12306
+ helper: HelperSpecSchema.optional()
11988
12307
  }).superRefine((composition, ctx) => {
11989
12308
  const seen = /* @__PURE__ */ new Set();
11990
12309
  composition.features.forEach((feature, index) => {
@@ -17091,7 +17410,22 @@ var BrokerProviderInfoSchema = object({
17091
17410
  */
17092
17411
  interactiveConnectPath: string().min(1).optional(),
17093
17412
  /** Relative hub path to disconnect the linked account (same popup pattern). */
17094
- interactiveDisconnectPath: string().min(1).optional()
17413
+ interactiveDisconnectPath: string().min(1).optional(),
17414
+ /**
17415
+ * The kind signs in through a server-held interactive session instead of
17416
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17417
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17418
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17419
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17420
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17421
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17422
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17423
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17424
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17425
+ * offers it as the default path; the fields the session fills are
17426
+ * marked `filledBySignIn` in the kind's settings schema.
17427
+ */
17428
+ signIn: object({ basePath: string().min(1) }).optional()
17095
17429
  }))
17096
17430
  });
17097
17431
  var ListInputSchema = object({
@@ -18086,7 +18420,12 @@ var CompositionProblemSchema = object({
18086
18420
  "item-field-reserved",
18087
18421
  "target-missing",
18088
18422
  "target-unreadable",
18089
- "not-replaceable"
18423
+ "not-replaceable",
18424
+ "native-capability-replace",
18425
+ "mixed-quantity",
18426
+ "helper-mismatch",
18427
+ "member-constraint",
18428
+ "retention"
18090
18429
  ]),
18091
18430
  path: string(),
18092
18431
  message: string()
@@ -18107,7 +18446,8 @@ var CompositionUnavailableModeSchema = _enum([
18107
18446
  "null",
18108
18447
  "offline",
18109
18448
  "unknown",
18110
- "release"
18449
+ "release",
18450
+ "omit"
18111
18451
  ]);
18112
18452
  /**
18113
18453
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18144,9 +18484,11 @@ var CompositionFieldPlanSchema = object({
18144
18484
  reads: array(CompositionFieldReadSchema)
18145
18485
  });
18146
18486
  /**
18147
- * What a feature does to its target (D663): `new` builds a capability on a new
18148
- * device; on an existing device it `add`s a capability the device lacks, or
18149
- * `replace`s named fields of one it has natively.
18487
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18488
+ * new device; on an existing device it `add`s a capability the device lacks.
18489
+ * Replacing a capability the device already serves is refused
18490
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18491
+ * wire for a claim the hub already holds.
18150
18492
  */
18151
18493
  var CompositionFeatureModeSchema = _enum([
18152
18494
  "new",
@@ -18317,8 +18659,392 @@ Object.freeze({
18317
18659
  latchedAt: {
18318
18660
  kind: "fixed",
18319
18661
  result: ["number", "null"]
18662
+ },
18663
+ schmitt: {
18664
+ kind: "fixed",
18665
+ result: ["boolean", "null"]
18320
18666
  }
18321
18667
  });
18668
+ /**
18669
+ * One catalog row per base reading a group helper can build (D944 §3).
18670
+ * Data, not a form default written into the block.
18671
+ */
18672
+ var NOW$2 = {
18673
+ fieldPath: "lastFetchedAt",
18674
+ expr: "now"
18675
+ };
18676
+ var CHANGED = {
18677
+ fieldPath: "lastChangedAt",
18678
+ expr: "now"
18679
+ };
18680
+ function numeric(input) {
18681
+ return {
18682
+ ...input,
18683
+ family: "numeric",
18684
+ deviceType: DeviceType.Sensor,
18685
+ memberDeviceType: DeviceType.Sensor,
18686
+ memberDeviceTypes: [DeviceType.Sensor],
18687
+ activeWhen: void 0,
18688
+ commandCaps: [],
18689
+ baseUnit: baseUnitOf(input.quantity),
18690
+ aggregates: NUMERIC_AGGREGATES,
18691
+ accept: [{
18692
+ cap: input.outputCap,
18693
+ fieldPath: input.outputField
18694
+ }]
18695
+ };
18696
+ }
18697
+ function binary(input) {
18698
+ return {
18699
+ ...input,
18700
+ family: "binary",
18701
+ deviceType: DeviceType.Sensor,
18702
+ memberDeviceType: DeviceType.Sensor,
18703
+ memberDeviceTypes: [DeviceType.Sensor],
18704
+ activeWhen: void 0,
18705
+ commandCaps: [],
18706
+ quantity: "none",
18707
+ baseUnit: void 0,
18708
+ aggregates: BINARY_AGGREGATES,
18709
+ empty: "offline"
18710
+ };
18711
+ }
18712
+ var GROUP_TEMPLATES = [
18713
+ numeric({
18714
+ id: "temperature",
18715
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18716
+ role: DeviceRole.TemperatureSensor,
18717
+ outputCap: "temperature-sensor",
18718
+ outputField: "celsius",
18719
+ quantity: "temperature",
18720
+ defaultAggregate: "mean",
18721
+ empty: "null",
18722
+ companions: [NOW$2]
18723
+ }),
18724
+ numeric({
18725
+ id: "humidity",
18726
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18727
+ role: DeviceRole.HumiditySensor,
18728
+ outputCap: "humidity-sensor",
18729
+ outputField: "percent",
18730
+ quantity: "humidity",
18731
+ defaultAggregate: "mean",
18732
+ empty: "null",
18733
+ companions: [NOW$2]
18734
+ }),
18735
+ numeric({
18736
+ id: "pressure",
18737
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18738
+ role: DeviceRole.PressureSensor,
18739
+ outputCap: "pressure-sensor",
18740
+ outputField: "hpa",
18741
+ quantity: "pressure",
18742
+ defaultAggregate: "mean",
18743
+ empty: "null",
18744
+ companions: [NOW$2]
18745
+ }),
18746
+ numeric({
18747
+ id: "illuminance",
18748
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18749
+ role: DeviceRole.AmbientLightSensor,
18750
+ outputCap: "ambient-light-sensor",
18751
+ outputField: "lux",
18752
+ quantity: "illuminance",
18753
+ defaultAggregate: "mean",
18754
+ empty: "null",
18755
+ companions: [NOW$2]
18756
+ }),
18757
+ numeric({
18758
+ id: "power",
18759
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18760
+ role: DeviceRole.PowerSensor,
18761
+ outputCap: "power-meter",
18762
+ outputField: "watts",
18763
+ quantity: "power",
18764
+ defaultAggregate: "sum",
18765
+ empty: "omit",
18766
+ companions: [NOW$2]
18767
+ }),
18768
+ numeric({
18769
+ id: "energy",
18770
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18771
+ role: DeviceRole.EnergySensor,
18772
+ outputCap: "power-meter",
18773
+ outputField: "kwhTotal",
18774
+ quantity: "energy",
18775
+ defaultAggregate: "sum",
18776
+ empty: "omit",
18777
+ companions: [NOW$2]
18778
+ }),
18779
+ numeric({
18780
+ id: "battery",
18781
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18782
+ role: DeviceRole.BatterySensor,
18783
+ outputCap: "battery",
18784
+ outputField: "percentage",
18785
+ quantity: "percentage",
18786
+ defaultAggregate: "mean",
18787
+ empty: "null",
18788
+ companions: [
18789
+ {
18790
+ fieldPath: "charging",
18791
+ expr: "\"unknown\""
18792
+ },
18793
+ {
18794
+ fieldPath: "sleeping",
18795
+ expr: "false"
18796
+ },
18797
+ {
18798
+ fieldPath: "lastUpdated",
18799
+ expr: "now"
18800
+ }
18801
+ ]
18802
+ }),
18803
+ numeric({
18804
+ id: "numeric",
18805
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18806
+ role: DeviceRole.NumericSensor,
18807
+ outputCap: "numeric-sensor",
18808
+ outputField: "value",
18809
+ quantity: "device",
18810
+ defaultAggregate: "mean",
18811
+ empty: "null",
18812
+ companions: [NOW$2]
18813
+ }),
18814
+ binary({
18815
+ id: "motion",
18816
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18817
+ role: DeviceRole.MotionSensor,
18818
+ outputCap: "motion",
18819
+ outputField: "detected",
18820
+ defaultAggregate: "any",
18821
+ accept: [{
18822
+ cap: "motion",
18823
+ fieldPath: "detected"
18824
+ }],
18825
+ companions: [{
18826
+ fieldPath: "lastDetectedAt",
18827
+ expr: "null"
18828
+ }, {
18829
+ fieldPath: "autoClearAfterMs",
18830
+ expr: "null"
18831
+ }]
18832
+ }),
18833
+ binary({
18834
+ id: "contact",
18835
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18836
+ role: DeviceRole.ContactSensor,
18837
+ outputCap: "contact",
18838
+ outputField: "entryOpen",
18839
+ defaultAggregate: "any",
18840
+ accept: [{
18841
+ cap: "contact",
18842
+ fieldPath: "entryOpen"
18843
+ }],
18844
+ companions: [CHANGED]
18845
+ }),
18846
+ binary({
18847
+ id: "leak",
18848
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18849
+ role: DeviceRole.LeakSensor,
18850
+ outputCap: "flood",
18851
+ outputField: "flooded",
18852
+ defaultAggregate: "any",
18853
+ accept: [{
18854
+ cap: "flood",
18855
+ fieldPath: "flooded"
18856
+ }],
18857
+ companions: [CHANGED]
18858
+ }),
18859
+ binary({
18860
+ id: "smoke",
18861
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18862
+ role: DeviceRole.SmokeSensor,
18863
+ outputCap: "smoke",
18864
+ outputField: "detected",
18865
+ defaultAggregate: "any",
18866
+ accept: [{
18867
+ cap: "smoke",
18868
+ fieldPath: "detected"
18869
+ }],
18870
+ companions: [CHANGED]
18871
+ }),
18872
+ binary({
18873
+ id: "occupancy",
18874
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18875
+ role: DeviceRole.BinarySensor,
18876
+ outputCap: "binary",
18877
+ outputField: "on",
18878
+ defaultAggregate: "any",
18879
+ accept: [{
18880
+ cap: "motion",
18881
+ fieldPath: "detected"
18882
+ }, {
18883
+ cap: "binary",
18884
+ fieldPath: "on"
18885
+ }],
18886
+ companions: [CHANGED]
18887
+ }),
18888
+ binary({
18889
+ id: "binary",
18890
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18891
+ role: DeviceRole.BinarySensor,
18892
+ outputCap: "binary",
18893
+ outputField: "on",
18894
+ defaultAggregate: "any",
18895
+ accept: [{
18896
+ cap: "binary",
18897
+ fieldPath: "on"
18898
+ }],
18899
+ companions: [CHANGED]
18900
+ }),
18901
+ actuator({
18902
+ id: "switch",
18903
+ 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.",
18904
+ deviceType: DeviceType.Switch,
18905
+ outputCap: "switch",
18906
+ outputField: "on",
18907
+ member: {
18908
+ cap: "switch",
18909
+ fieldPath: "on"
18910
+ },
18911
+ commandCaps: ["switch"]
18912
+ }),
18913
+ actuator({
18914
+ id: "light",
18915
+ 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.",
18916
+ deviceType: DeviceType.Light,
18917
+ outputCap: "switch",
18918
+ outputField: "on",
18919
+ member: {
18920
+ cap: "switch",
18921
+ fieldPath: "on"
18922
+ },
18923
+ commandCaps: [
18924
+ "switch",
18925
+ "brightness",
18926
+ "color"
18927
+ ]
18928
+ }),
18929
+ actuator({
18930
+ id: "fan",
18931
+ 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.",
18932
+ deviceType: DeviceType.Fan,
18933
+ outputCap: "switch",
18934
+ outputField: "on",
18935
+ member: {
18936
+ cap: "switch",
18937
+ fieldPath: "on"
18938
+ },
18939
+ commandCaps: ["switch", "fan-control"]
18940
+ }),
18941
+ actuator({
18942
+ id: "cover",
18943
+ 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.",
18944
+ deviceType: DeviceType.Cover,
18945
+ outputCap: "group-active",
18946
+ outputField: "active",
18947
+ member: {
18948
+ cap: "cover",
18949
+ fieldPath: "state"
18950
+ },
18951
+ activeWhen: [
18952
+ "open",
18953
+ "opening",
18954
+ "stopped"
18955
+ ],
18956
+ commandCaps: ["cover"]
18957
+ }),
18958
+ actuator({
18959
+ id: "valve",
18960
+ 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.",
18961
+ deviceType: DeviceType.Valve,
18962
+ outputCap: "group-active",
18963
+ outputField: "active",
18964
+ member: {
18965
+ cap: "valve",
18966
+ fieldPath: "state"
18967
+ },
18968
+ activeWhen: [
18969
+ "open",
18970
+ "opening",
18971
+ "stopped"
18972
+ ],
18973
+ commandCaps: ["valve"]
18974
+ }),
18975
+ actuator({
18976
+ id: "lock",
18977
+ 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.",
18978
+ deviceType: DeviceType.Lock,
18979
+ outputCap: "group-active",
18980
+ outputField: "active",
18981
+ member: {
18982
+ cap: "lock-control",
18983
+ fieldPath: "state"
18984
+ },
18985
+ activeWhen: ["locked"],
18986
+ commandCaps: ["lock-control"]
18987
+ }),
18988
+ actuator({
18989
+ id: "media-player",
18990
+ 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.",
18991
+ deviceType: DeviceType.MediaPlayer,
18992
+ outputCap: "group-active",
18993
+ outputField: "active",
18994
+ member: {
18995
+ cap: "media-player",
18996
+ fieldPath: "state"
18997
+ },
18998
+ activeWhen: ["playing", "buffering"],
18999
+ commandCaps: ["media-player"]
19000
+ }),
19001
+ actuator({
19002
+ id: "climate",
19003
+ 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.",
19004
+ deviceType: DeviceType.Thermostat,
19005
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
19006
+ outputCap: "group-active",
19007
+ outputField: "active",
19008
+ member: {
19009
+ cap: "climate-control",
19010
+ fieldPath: "mode"
19011
+ },
19012
+ activeWhen: [
19013
+ "heat",
19014
+ "cool",
19015
+ "auto",
19016
+ "heat_cool",
19017
+ "fan_only",
19018
+ "dry"
19019
+ ],
19020
+ commandCaps: ["climate-control"]
19021
+ })
19022
+ ];
19023
+ function actuator(input) {
19024
+ return {
19025
+ id: input.id,
19026
+ summary: input.summary,
19027
+ deviceType: input.deviceType,
19028
+ outputCap: input.outputCap,
19029
+ outputField: input.outputField,
19030
+ commandCaps: input.commandCaps,
19031
+ family: "actuator",
19032
+ role: void 0,
19033
+ memberDeviceType: input.deviceType,
19034
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
19035
+ activeWhen: input.activeWhen,
19036
+ quantity: "none",
19037
+ baseUnit: void 0,
19038
+ aggregates: BINARY_AGGREGATES,
19039
+ defaultAggregate: "any",
19040
+ empty: "offline",
19041
+ accept: [input.member],
19042
+ companions: [CHANGED]
19043
+ };
19044
+ }
19045
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19046
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19047
+ DeviceType.Camera;
18322
19048
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18323
19049
  /**
18324
19050
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20032,12 +20758,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20032
20758
  * providers detect renames by matching `uniqueId` and patch `id`
20033
20759
  * in-place so dispatch keeps working.
20034
20760
  *
20035
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20036
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20037
- * state push and are the single source of truth. The `device_class` tag is
20038
- * an HA-specific input that is consumed at the discovery boundary
20039
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20040
- * stored after that point.
20761
+ * **Display unit and precision are NOT stored here.** They are a read-time
20762
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20763
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20764
+ * neither is repeated on each sample. The `device_class` tag is an
20765
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20766
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20041
20767
  *
20042
20768
  * The `raw` escape hatch carries provider-specific attributes that don't
20043
20769
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20051,6 +20777,14 @@ var SourceInfoSchema = object({
20051
20777
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20052
20778
  * Used to detect rename when `id` changes. */
20053
20779
  uniqueId: string().optional(),
20780
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20781
+ quantity: ReadingQuantitySchema.optional(),
20782
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20783
+ dateTimeShape: _enum([
20784
+ "date",
20785
+ "time",
20786
+ "datetime"
20787
+ ]).optional(),
20054
20788
  /** Provider-specific extras that don't fit the structured slots.
20055
20789
  * Whitelist what goes here per provider — do NOT dump entire HA
20056
20790
  * attribute blobs (would explode the meta JSON column). */
@@ -40963,7 +41697,8 @@ method(object({
40963
41697
  "enum"
40964
41698
  ]),
40965
41699
  enumValues: array(string()).optional(),
40966
- item: boolean().optional()
41700
+ item: boolean().optional(),
41701
+ quantity: ReadingQuantitySchema.optional()
40967
41702
  })).readonly(),
40968
41703
  itemArray: object({
40969
41704
  path: string(),
@@ -43324,7 +44059,7 @@ var NetworkAccessStatusSchema = object({
43324
44059
  });
43325
44060
  /**
43326
44061
  * Optional, richer endpoint shape returned by providers that expose
43327
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44062
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43328
44063
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43329
44064
  * the originating provider config (mode + sourcePort) so the
43330
44065
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44763,8 +45498,8 @@ var MaskPolygonShapeSchema = object({
44763
45498
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44764
45499
  var MaskGridShapeSchema = object({
44765
45500
  kind: literal("grid"),
44766
- gridWidth: number(),
44767
- gridHeight: number(),
45501
+ gridWidth: number().meta(readingMeta("none")),
45502
+ gridHeight: number().meta(readingMeta("none")),
44768
45503
  cells: array(boolean())
44769
45504
  });
44770
45505
  discriminatedUnion("kind", [
@@ -44876,6 +45611,8 @@ var NcSystemEventKindSchema = _enum([
44876
45611
  "detection-blind",
44877
45612
  "motion-noisy",
44878
45613
  "motion-calm",
45614
+ "package-llm-silent",
45615
+ "package-llm-answered",
44879
45616
  "recording-stalled",
44880
45617
  "recording-healthy",
44881
45618
  "recording-degraded",
@@ -45965,7 +46702,20 @@ var NcRuleInputSchema = object({
45965
46702
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
45966
46703
  * path does not run Zod defaults.
45967
46704
  */
45968
- groupingSec: number().int().min(5).max(60).optional()
46705
+ groupingSec: number().int().min(5).max(60).optional(),
46706
+ /**
46707
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46708
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46709
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46710
+ * same mark the Library writes, and records whose notification it was and
46711
+ * when. A channel with no buttons gets the same two choices as links.
46712
+ *
46713
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46714
+ * cap path): absent and `false` both mean off, which is what every rule
46715
+ * authored before this field meant. An editor turns it off by writing
46716
+ * `false`, so it needs no `clear` key.
46717
+ */
46718
+ askVerdict: boolean().optional()
45969
46719
  });
45970
46720
  /**
45971
46721
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -55890,6 +56640,28 @@ var PackageVersionInfoSchema = object({
55890
56640
  deprecated: string().optional(),
55891
56641
  distTags: array(string()).readonly()
55892
56642
  });
56643
+ /**
56644
+ * One published document. `ok` is markdown from the registry. `missing` means
56645
+ * the registry answered and the file was not there. `error` is a registry or
56646
+ * tarball failure — never an empty string standing in for either.
56647
+ */
56648
+ var PackageDocBodySchema = discriminatedUnion("status", [
56649
+ object({
56650
+ status: literal("ok"),
56651
+ markdown: string()
56652
+ }),
56653
+ object({ status: literal("missing") }),
56654
+ object({
56655
+ status: literal("error"),
56656
+ message: string()
56657
+ })
56658
+ ]);
56659
+ var PackageDocsSchema = object({
56660
+ name: string(),
56661
+ version: string(),
56662
+ readme: PackageDocBodySchema,
56663
+ changelog: PackageDocBodySchema
56664
+ });
55893
56665
  var SearchResultSchema = object({
55894
56666
  name: string(),
55895
56667
  version: string(),
@@ -56108,7 +56880,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56108
56880
  }), object({ success: literal(true) }), {
56109
56881
  kind: "mutation",
56110
56882
  auth: "admin"
56111
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
56883
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
56884
+ name: string().min(1),
56885
+ version: string().min(1).optional()
56886
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56112
56887
  kind: "mutation",
56113
56888
  auth: "admin"
56114
56889
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56158,46 +56933,27 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56158
56933
  }), LogStreamEntrySchema, { kind: "subscription" });
56159
56934
  object({
56160
56935
  /** Carbon dioxide concentration in ppm. */
56161
- co2Ppm: number().min(0).optional(),
56936
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56162
56937
  /** Total volatile organic compounds in ppb. */
56163
- vocPpb: number().min(0).optional(),
56938
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56164
56939
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56165
- pm25: number().min(0).optional(),
56940
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56166
56941
  /** Particulate matter ≤ 10 μm in µg/m³. */
56167
- pm10: number().min(0).optional(),
56942
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56168
56943
  /** Composite AQI value (typically 0..500). */
56169
- aqi: number().optional(),
56944
+ aqi: number().optional().meta(readingMeta("index")),
56170
56945
  /** Ms epoch when the slice was last updated. */
56171
- lastFetchedAt: number(),
56172
- /** Live display unit of the single metric this slice carries (e.g. HA
56173
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56174
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56175
- * per slice is unambiguous. */
56176
- unit: string().optional(),
56177
- /** Suggested decimal places for numeric display.
56178
- * Populated live from the upstream source when provided (e.g. HA
56179
- * `attributes.suggested_display_precision`). Falls back to
56180
- * auto-formatting when absent. */
56181
- precision: number().int().min(0).max(10).optional()
56946
+ lastFetchedAt: number()
56182
56947
  });
56183
56948
  DeviceType.Sensor;
56184
56949
  object({
56185
- /** Current illuminance in lux (lx). */
56186
- lux: number().min(0).meta({
56187
- unit: "lx",
56950
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56951
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56952
+ precision: 0,
56188
56953
  label: "Illuminance"
56189
- }),
56954
+ })),
56190
56955
  /** Ms epoch when the slice was last updated. */
56191
- lastFetchedAt: number(),
56192
- /** Live display unit from the upstream source (e.g. HA
56193
- * `attributes.unit_of_measurement`). The UI prefers this over the
56194
- * role's canonical unit. Absent → fall back to the canonical unit. */
56195
- unit: string().optional(),
56196
- /** Suggested decimal places for numeric display.
56197
- * Populated live from the upstream source when provided (e.g. HA
56198
- * `attributes.suggested_display_precision`). Falls back to
56199
- * auto-formatting when absent. */
56200
- precision: number().int().min(0).max(10).optional()
56956
+ lastFetchedAt: number()
56201
56957
  });
56202
56958
  DeviceType.Sensor;
56203
56959
  /**
@@ -56342,10 +57098,7 @@ var BatteryStatusSchema = object({
56342
57098
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56343
57099
  * declines to band a non-finite reading, which is the correct shape.
56344
57100
  */
56345
- percentage: number().min(0).max(100).nullable().meta({
56346
- unit: "%",
56347
- label: "Charge"
56348
- }),
57101
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56349
57102
  /**
56350
57103
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56351
57104
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56372,7 +57125,14 @@ var BatteryStatusSchema = object({
56372
57125
  */
56373
57126
  sleeping: boolean().meta({ label: "Sleeping" }),
56374
57127
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56375
- lastUpdated: number(),
57128
+ lastUpdated: number().meta(readingMeta("none")),
57129
+ /**
57130
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57131
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57132
+ * that only report charge. A second reading on this cap, not a second
57133
+ * `numeric-sensor` on the same device.
57134
+ */
57135
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56376
57136
  /**
56377
57137
  * Ms epoch of the last time the device PROVED it was reachable — a
56378
57138
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56412,7 +57172,7 @@ var BatteryStatusSchema = object({
56412
57172
  */
56413
57173
  binary: boolean().optional().meta({ label: "Binary indicator" })
56414
57174
  });
56415
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat;
57175
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter;
56416
57176
  method(object({
56417
57177
  deviceId: number(),
56418
57178
  /** Bound on the wait. Sensible range 3000–10000ms. */
@@ -56432,7 +57192,7 @@ object({
56432
57192
  DeviceType.Sensor;
56433
57193
  object({
56434
57194
  /** Current level as 0..100 inclusive. Firmware-reported. */
56435
- percentage: number().min(0).max(100),
57195
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56436
57196
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56437
57197
  lastChangedAt: number()
56438
57198
  });
@@ -56827,21 +57587,21 @@ object({
56827
57587
  /** Single setpoint in Celsius. Used by single-target modes
56828
57588
  * (heat / cool / dry). Null when the device is in a range mode
56829
57589
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
56830
- target: number().nullable(),
57590
+ target: number().nullable().meta(readingMeta("temperature")),
56831
57591
  /** Upper bound of the dual setpoint in Celsius. Populated only
56832
57592
  * in `heat_cool` mode. */
56833
- targetHigh: number().nullable(),
57593
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
56834
57594
  /** Lower bound of the dual setpoint in Celsius. Populated only
56835
57595
  * in `heat_cool` mode. */
56836
- targetLow: number().nullable(),
57596
+ targetLow: number().nullable().meta(readingMeta("temperature")),
56837
57597
  /** Target relative humidity (0..100). Null when the device has no
56838
57598
  * humidity control. */
56839
- targetHumidity: number().min(0).max(100).nullable(),
57599
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56840
57600
  /** Current measured temperature in Celsius. Read-only — pushed by
56841
57601
  * the device's internal sensor. */
56842
- currentTemp: number().nullable(),
57602
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
56843
57603
  /** Current measured relative humidity (0..100). Read-only. */
56844
- currentHumidity: number().min(0).max(100).nullable(),
57604
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56845
57605
  /** Vertical louver swing on/off. Null when the device has no vertical
56846
57606
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
56847
57607
  swingVertical: boolean().nullable(),
@@ -56849,11 +57609,11 @@ object({
56849
57609
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
56850
57610
  swingHorizontal: boolean().nullable(),
56851
57611
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
56852
- minTemp: number().nullable().optional(),
57612
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
56853
57613
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
56854
- maxTemp: number().nullable().optional(),
57614
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
56855
57615
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
56856
- tempStep: number().positive().nullable().optional(),
57616
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
56857
57617
  /** Ms epoch when the slice was last updated (push or command). */
56858
57618
  lastFetchedAt: number()
56859
57619
  });
@@ -56932,17 +57692,17 @@ DeviceType.Thermostat, DeviceType.Climate, method(object({
56932
57692
  * modes to expose in the picker without a slice fetch.
56933
57693
  */
56934
57694
  var RgbTripletSchema = object({
56935
- r: number().int().min(0).max(255),
56936
- g: number().int().min(0).max(255),
56937
- b: number().int().min(0).max(255)
57695
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57696
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57697
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
56938
57698
  });
56939
57699
  var HsvTripletSchema = object({
56940
57700
  /** Hue in degrees, 0..360. */
56941
- h: number().min(0).max(360),
57701
+ h: number().min(0).max(360).meta(readingMeta("none")),
56942
57702
  /** Saturation as 0..100 inclusive. */
56943
- s: number().min(0).max(100),
57703
+ s: number().min(0).max(100).meta(readingMeta("none")),
56944
57704
  /** Value/brightness as 0..100 inclusive. */
56945
- v: number().min(0).max(100)
57705
+ v: number().min(0).max(100).meta(readingMeta("none"))
56946
57706
  });
56947
57707
  var ColorInputSchema = discriminatedUnion("mode", [
56948
57708
  object({
@@ -56971,7 +57731,7 @@ object({
56971
57731
  /** Populated when `mode === 'hsv'`. */
56972
57732
  hsv: HsvTripletSchema.optional(),
56973
57733
  /** Populated when `mode === 'mired'`. */
56974
- mireds: number().int().optional(),
57734
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
56975
57735
  /** Ms epoch of the last operator-driven change. */
56976
57736
  lastChangedAt: number()
56977
57737
  });
@@ -57095,18 +57855,12 @@ _enum([
57095
57855
  "empty",
57096
57856
  "full"
57097
57857
  ]);
57098
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57099
- * each. */
57100
- var ConsumableRemainingUnitSchema = _enum([
57101
- "days",
57102
- "hours",
57103
- "cycles"
57104
- ]);
57105
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57106
- * days). `value` null = unknown (D393). */
57858
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57859
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57860
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57107
57861
  var ConsumableRemainingSchema = object({
57108
- value: number().min(0).nullable(),
57109
- unit: ConsumableRemainingUnitSchema
57862
+ value: number().min(0).nullable().meta(readingMeta("device")),
57863
+ kind: ConsumableRemainingKindSchema
57110
57864
  });
57111
57865
  var ConsumablesStatusSchema = object({
57112
57866
  items: array(object({
@@ -57115,7 +57869,7 @@ var ConsumablesStatusSchema = object({
57115
57869
  /** Display name. */
57116
57870
  label: string().min(1),
57117
57871
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57118
- level: number().min(0).max(100).nullable(),
57872
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57119
57873
  /** The provider's discrete verdict when known (binary mode). */
57120
57874
  status: ConsumableStatusSchema.nullable(),
57121
57875
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57132,7 +57886,7 @@ var ConsumablesStatusSchema = object({
57132
57886
  })),
57133
57887
  lastChangedAt: number()
57134
57888
  });
57135
- Object.values(DeviceType), [...ConsumableRemainingUnitSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57889
+ Object.values(DeviceType), [...ConsumableRemainingKindSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57136
57890
  Object.values(DeviceType), method(object({
57137
57891
  deviceId: number().int().nonnegative(),
57138
57892
  key: string().min(1)
@@ -57157,39 +57911,17 @@ object({
57157
57911
  /** Current value. Numeric kind → number; select/text/datetime → string.
57158
57912
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
57159
57913
  * or time-only — HA accepts all three forms). */
57160
- value: union([number(), string()]),
57914
+ value: union([number().meta(readingMeta("device")), string()]),
57161
57915
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
57162
57916
  options: array(string()),
57163
57917
  /** Ms epoch when the slice was last updated. */
57164
57918
  lastChangedAt: number(),
57165
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
57166
- * Populated live from HA `attributes.unit_of_measurement` on each push.
57167
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57168
- unit: string().optional(),
57169
- /** Minimum allowed value. Populated live from HA `attributes.min`.
57170
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57171
- min: number().optional(),
57172
- /** Maximum allowed value. Populated live from HA `attributes.max`.
57173
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57174
- max: number().optional(),
57175
- /** Step increment. Populated live from HA `attributes.step`.
57176
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57177
- step: number().optional(),
57178
- /** Suggested decimal places for numeric display. Populated live from HA
57179
- * `attributes.suggested_display_precision`, or derived from `step` when the
57180
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
57181
- * other kinds. Falls back to auto-formatting when absent. */
57182
- precision: number().int().min(0).max(10).optional(),
57183
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
57184
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
57185
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
57186
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
57187
- * `has_date`/`has_time` attributes on each push. */
57188
- format: _enum([
57189
- "date",
57190
- "time",
57191
- "datetime"
57192
- ]).optional()
57919
+ /** Minimum allowed value, in the device quantity's base unit. */
57920
+ min: number().optional().meta(readingMeta("device")),
57921
+ /** Maximum allowed value, in the device quantity's base unit. */
57922
+ max: number().optional().meta(readingMeta("device")),
57923
+ /** Step increment, in the device quantity's base unit. */
57924
+ step: number().optional().meta(readingMeta("device"))
57193
57925
  });
57194
57926
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
57195
57927
  object({
@@ -57227,9 +57959,9 @@ object({
57227
57959
  ]),
57228
57960
  /** 0 = fully closed, 100 = fully open. Null when the device has
57229
57961
  * no intermediate position surface. */
57230
- position: number().min(0).max(100).nullable(),
57962
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57231
57963
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
57232
- tiltPosition: number().min(0).max(100).nullable(),
57964
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57233
57965
  /** Ms epoch when the slice was last updated. */
57234
57966
  lastChangedAt: number()
57235
57967
  });
@@ -57287,9 +58019,9 @@ var NormalizedRangeSchema$1 = object({
57287
58019
  object({
57288
58020
  mode: DayNightModeSchema,
57289
58021
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
57290
- sensitivity: number().optional(),
58022
+ sensitivity: number().optional().meta(readingMeta("none")),
57291
58023
  /** Delay before the IR-cut filter flips, in seconds. */
57292
- switchDelaySec: number().optional(),
58024
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
57293
58025
  lastFetchedAt: number()
57294
58026
  });
57295
58027
  /**
@@ -57444,11 +58176,11 @@ object({
57444
58176
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
57445
58177
  availablePrograms: array(string()),
57446
58178
  /** Seconds remaining on the active cycle, or null when not running / not known. */
57447
- remainingSeconds: number().int().nonnegative().nullable(),
58179
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57448
58180
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
57449
- delayedStartSeconds: number().int().nonnegative().nullable(),
58181
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57450
58182
  /** Cycle completion 0..100, or null when not applicable. */
57451
- progressPercent: number().min(0).max(100).nullable(),
58183
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57452
58184
  /** True when the door is reported open; null when the door state is not known. */
57453
58185
  doorOpen: boolean().nullable(),
57454
58186
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -57472,7 +58204,7 @@ object({
57472
58204
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57473
58205
  machineCareDue: boolean().nullable(),
57474
58206
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57475
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
58207
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
57476
58208
  /** Option states: null when the appliance does not expose the option or has not said. */
57477
58209
  halfLoad: boolean().nullable(),
57478
58210
  hygienePlus: boolean().nullable(),
@@ -57487,7 +58219,7 @@ object({
57487
58219
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
57488
58220
  activeEvents: array(string()),
57489
58221
  /** Numeric error code when the appliance reports one; null otherwise. */
57490
- errorCode: number().nullable(),
58222
+ errorCode: number().nullable().meta(readingMeta("none")),
57491
58223
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
57492
58224
  errorLabel: string().nullable(),
57493
58225
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -57633,24 +58365,14 @@ object({
57633
58365
  /** Ms epoch of the last press. null = never observed since this provider started. */
57634
58366
  lastPressedAt: number().nullable(),
57635
58367
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
57636
- pressCountSinceStart: number()
58368
+ pressCountSinceStart: number().meta(readingMeta("count"))
57637
58369
  });
57638
58370
  object({
57639
58371
  deviceId: number(),
57640
58372
  timestamp: number()
57641
58373
  });
57642
58374
  DeviceType.Button, DeviceType.Camera;
57643
- /**
57644
- * Enum-state sensor — a string value picked from a finite option set.
57645
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
57646
- * states, weather conditions, contact-source channels, mode strings).
57647
- * The option set is stable across observations; it is persisted in the
57648
- * device config blob (via the control slice at adoption) and is NOT
57649
- * stored in `sourceInfo`.
57650
- */
57651
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
57652
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
57653
- var EnumSensorDateTimeFormatSchema = _enum([
58375
+ _enum([
57654
58376
  "date",
57655
58377
  "time",
57656
58378
  "datetime"
@@ -57658,12 +58380,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
57658
58380
  object({
57659
58381
  value: string(),
57660
58382
  /**
57661
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
57662
- * as a locale date/time/datetime; absent for plain enum sensors. HA
57663
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
57664
- * a time-only sensor → `'time'`.
58383
+ * Set when `value` is a timestamp rather than an enum state, so history
58384
+ * does not learn ISO strings as states. The display shape (date / time /
58385
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
57665
58386
  */
57666
- format: EnumSensorDateTimeFormatSchema.optional(),
58387
+ timestamp: boolean().optional(),
57667
58388
  /** Ms epoch when the slice was last updated. */
57668
58389
  lastFetchedAt: number()
57669
58390
  });
@@ -57682,13 +58403,13 @@ var EventFireSchema = object({
57682
58403
  deviceId: number(),
57683
58404
  eventType: string(),
57684
58405
  data: record(string(), unknown()).nullable(),
57685
- timestamp: number(),
57686
- seq: number()
58406
+ timestamp: number().meta(readingMeta("none")),
58407
+ seq: number().meta(readingMeta("count"))
57687
58408
  });
57688
58409
  object({
57689
58410
  eventTypes: array(string()),
57690
58411
  lastEvent: EventFireSchema.nullable(),
57691
- eventCountSinceStart: number()
58412
+ eventCountSinceStart: number().meta(readingMeta("count"))
57692
58413
  });
57693
58414
  DeviceType.EventEmitter;
57694
58415
  var IdentitySchema = object({
@@ -58077,10 +58798,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
58077
58798
  object({
58078
58799
  /** Active speed as 0..100 inclusive. Null when the device has no
58079
58800
  * speed surface (single-speed fan). */
58080
- percentage: number().min(0).max(100).nullable(),
58801
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58081
58802
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
58082
58803
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
58083
- percentageStep: number().positive().optional(),
58804
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
58084
58805
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
58085
58806
  * — empty string when no preset is active or supported. */
58086
58807
  preset: string(),
@@ -58162,7 +58883,7 @@ var FeatureProbeStatusSchema = object({
58162
58883
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
58163
58884
  model: string().nullable(),
58164
58885
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
58165
- channelCount: number().nullable(),
58886
+ channelCount: number().nullable().meta(readingMeta("count")),
58166
58887
  /**
58167
58888
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
58168
58889
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -58194,9 +58915,45 @@ object({
58194
58915
  lastChangedAt: number()
58195
58916
  });
58196
58917
  DeviceType.Sensor;
58918
+ object({
58919
+ /** One row per member that has been commanded. A later command replaces that member's row. */
58920
+ results: array(object({
58921
+ deviceId: number().int().nonnegative(),
58922
+ cap: string().min(1),
58923
+ method: string().min(1),
58924
+ ok: boolean(),
58925
+ error: string().optional(),
58926
+ /** Ms epoch of the attempt. */
58927
+ at: number()
58928
+ })) });
58929
+ DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
58930
+ object({
58931
+ active: boolean().meta({ label: "Active" }),
58932
+ /** Ms epoch when the slice was last updated. */
58933
+ lastChangedAt: number()
58934
+ });
58935
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Climate, DeviceType.Thermostat;
58936
+ object({
58937
+ /** Members whose current value entered the aggregate. */
58938
+ included: number().int().nonnegative().meta(readingMeta("count", {
58939
+ precision: 0,
58940
+ label: "Reporting"
58941
+ })),
58942
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
58943
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
58944
+ precision: 0,
58945
+ label: "Excluded"
58946
+ })),
58947
+ /** `included` + `excluded`. */
58948
+ total: number().int().nonnegative().meta(readingMeta("count", {
58949
+ precision: 0,
58950
+ label: "Members"
58951
+ }))
58952
+ });
58953
+ DeviceType.Sensor, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
58197
58954
  object({
58198
58955
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
58199
- position: number().min(0).max(100).nullable(),
58956
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58200
58957
  /** Ms epoch when the slice was last updated (push or poll). */
58201
58958
  lastFetchedAt: number()
58202
58959
  });
@@ -58205,9 +58962,9 @@ object({
58205
58962
  /** Whether the humidifier is currently on. */
58206
58963
  on: boolean(),
58207
58964
  /** Current measured relative humidity (0..100). Null when not reported. */
58208
- currentHumidity: number().min(0).max(100).nullable(),
58965
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58209
58966
  /** Target relative humidity (0..100). Null when no setpoint surface. */
58210
- targetHumidity: number().min(0).max(100).nullable(),
58967
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58211
58968
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
58212
58969
  * device has no mode surface. */
58213
58970
  mode: string().nullable(),
@@ -58217,9 +58974,9 @@ object({
58217
58974
  * `idle` / `off`). Null when the device doesn't report it. */
58218
58975
  action: string().nullable(),
58219
58976
  /** HA `min_humidity` attribute. Null → UI uses 0. */
58220
- minHumidity: number().nullable(),
58977
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
58221
58978
  /** HA `max_humidity` attribute. Null → UI uses 100. */
58222
- maxHumidity: number().nullable(),
58979
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
58223
58980
  /** Ms epoch when the slice was last updated. */
58224
58981
  lastChangedAt: number()
58225
58982
  });
@@ -58243,22 +59000,13 @@ DeviceType.Humidifier, method(object({
58243
59000
  auth: "protected"
58244
59001
  });
58245
59002
  object({
58246
- /** Current relative humidity, 0..100. */
58247
- percent: number().min(0).max(100).meta({
58248
- unit: "%",
59003
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
59004
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
59005
+ precision: 0,
58249
59006
  label: "Humidity"
58250
- }),
59007
+ })),
58251
59008
  /** Ms epoch when the slice was last updated. */
58252
- lastFetchedAt: number(),
58253
- /** Live display unit from the upstream source (e.g. HA
58254
- * `attributes.unit_of_measurement`). The UI prefers this over the
58255
- * role's canonical unit. Absent → fall back to the canonical unit. */
58256
- unit: string().optional(),
58257
- /** Suggested decimal places for numeric display.
58258
- * Populated live from the upstream source when provided (e.g. HA
58259
- * `attributes.suggested_display_precision`). Falls back to
58260
- * auto-formatting when absent. */
58261
- precision: number().int().min(0).max(10).optional()
59009
+ lastFetchedAt: number()
58262
59010
  });
58263
59011
  DeviceType.Sensor, DeviceType.Thermostat;
58264
59012
  object({
@@ -58266,7 +59014,7 @@ object({
58266
59014
  * entity exposes no `entity_picture` (yet). */
58267
59015
  url: string().nullable(),
58268
59016
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
58269
- lastUpdated: number().nullable()
59017
+ lastUpdated: number().nullable().meta(readingMeta("none"))
58270
59018
  });
58271
59019
  DeviceType.Image;
58272
59020
  /**
@@ -58322,19 +59070,19 @@ var NormalizedRangeSchema = object({
58322
59070
  });
58323
59071
  object({
58324
59072
  /** Normalized 0–100. */
58325
- brightness: number().optional(),
59073
+ brightness: number().optional().meta(readingMeta("none")),
58326
59074
  /** Normalized 0–100. */
58327
- contrast: number().optional(),
59075
+ contrast: number().optional().meta(readingMeta("none")),
58328
59076
  /** Normalized 0–100. */
58329
- saturation: number().optional(),
59077
+ saturation: number().optional().meta(readingMeta("none")),
58330
59078
  /** Normalized 0–100. */
58331
- sharpness: number().optional(),
59079
+ sharpness: number().optional().meta(readingMeta("none")),
58332
59080
  mirror: boolean().optional(),
58333
59081
  flip: boolean().optional(),
58334
59082
  rotate: ImageRotateSchema.optional(),
58335
59083
  whiteBalance: WhiteBalanceModeSchema.optional(),
58336
59084
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
58337
- warmth: number().optional(),
59085
+ warmth: number().optional().meta(readingMeta("none")),
58338
59086
  exposureMode: ExposureModeSchema.optional(),
58339
59087
  backlightMode: BacklightModeSchema.optional(),
58340
59088
  lastFetchedAt: number()
@@ -58579,10 +59327,10 @@ var IntercomAbilitySchema = object({
58579
59327
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
58580
59328
  codecs: array(string()),
58581
59329
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
58582
- sampleRate: number(),
59330
+ sampleRate: number().meta(readingMeta("none")),
58583
59331
  duplex: _enum(["half", "full"]),
58584
59332
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
58585
- maxBacklogMs: number()
59333
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
58586
59334
  });
58587
59335
  var IntercomStatusSchema = object({
58588
59336
  /** True while a talk session is open. */
@@ -58693,13 +59441,13 @@ object({
58693
59441
  activity: LawnMowerActivitySchema,
58694
59442
  /** 0..100 battery percentage. Null when the device has no battery
58695
59443
  * reading. */
58696
- batteryLevel: number().min(0).max(100).nullable(),
59444
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58697
59445
  /** 0..100 mowing-completion percentage of the current task, or null when no
58698
59446
  * task is active / progress is unavailable. */
58699
- progressPercent: number().min(0).max(100).nullable(),
59447
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58700
59448
  /** Current device/event code (dynamic — mostly status, sometimes an error),
58701
59449
  * or null when unknown. */
58702
- currentCode: number().nullable(),
59450
+ currentCode: number().nullable().meta(readingMeta("none")),
58703
59451
  /** Human label for {@link currentCode}, or null when undecodable. */
58704
59452
  currentCodeLabel: string().nullable(),
58705
59453
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -59109,7 +59857,14 @@ DeviceType.Lock, method(object({
59109
59857
  auth: "protected"
59110
59858
  }), method(object({
59111
59859
  deviceId: number().int().nonnegative(),
59112
- code: string().min(1).optional()
59860
+ code: string().min(1).optional(),
59861
+ /**
59862
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
59863
+ * with one call is refused unless the caller says it was confirmed. A
59864
+ * single lock ignores it. The refusal lives in the callee, so no
59865
+ * automation or API caller reaches the doors by skipping a dialog.
59866
+ */
59867
+ confirm: boolean().optional()
59113
59868
  }), _void(), {
59114
59869
  kind: "mutation",
59115
59870
  auth: "protected"
@@ -59117,6 +59872,11 @@ DeviceType.Lock, method(object({
59117
59872
  kind: "mutation",
59118
59873
  auth: "protected"
59119
59874
  });
59875
+ object({
59876
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
59877
+ lastFetchedAt: number()
59878
+ });
59879
+ DeviceType.Sensor;
59120
59880
  /**
59121
59881
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
59122
59882
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
@@ -59161,7 +59921,7 @@ object({
59161
59921
  state: MediaPlayerStateSchema,
59162
59922
  /** Volume as 0..100 inclusive. Null when the device has no volume
59163
59923
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
59164
- volumeLevel: number().min(0).max(100).nullable(),
59924
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59165
59925
  /** Mute toggle distinct from volume=0. Null when no mute surface.
59166
59926
  * Pair with `DeviceFeature.MediaPlayerMute`. */
59167
59927
  isMuted: boolean().nullable(),
@@ -59174,10 +59934,10 @@ object({
59174
59934
  currentMedia: MediaInfoSchema.nullable(),
59175
59935
  /** Current playback position in ms. Null when not seekable or
59176
59936
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
59177
- positionMs: number().int().nonnegative().nullable(),
59937
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59178
59938
  /** Total duration of the current media in ms. Null when unknown
59179
59939
  * (live stream) or nothing is playing. */
59180
- durationMs: number().int().nonnegative().nullable(),
59940
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59181
59941
  /** Shuffle toggle. Null when no shuffle surface.
59182
59942
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
59183
59943
  shuffle: boolean().nullable(),
@@ -59477,7 +60237,7 @@ object({
59477
60237
  * it, every provider seeded it at boot, and every session left it
59478
60238
  * `detected: true` after its detach.
59479
60239
  */
59480
- autoClearAfterMs: number().nullable()
60240
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
59481
60241
  });
59482
60242
  /**
59483
60243
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -59527,7 +60287,7 @@ var MotionZoneRegionSchema = object({
59527
60287
  });
59528
60288
  object({
59529
60289
  enabled: boolean(),
59530
- sensitivity: number(),
60290
+ sensitivity: number().meta(readingMeta("none")),
59531
60291
  /** Grid region(s). Today exactly one `grid` shape. */
59532
60292
  regions: array(MotionZoneRegionSchema),
59533
60293
  lastFetchedAt: number()
@@ -59831,7 +60591,7 @@ var NavigationStatusSchema = object({
59831
60591
  following: boolean(),
59832
60592
  flash: boolean(),
59833
60593
  lightMode: NavigationLightModeSchema,
59834
- lightLevel: number().min(40).max(100),
60594
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
59835
60595
  /** Ms epoch when the slice was last updated. */
59836
60596
  lastChangedAt: number()
59837
60597
  });
@@ -59877,13 +60637,13 @@ var NetworkLinkStatusSchema = object({
59877
60637
  * one whose reading has not landed must not be drawn at 0 %. Consumers
59878
60638
  * SKIP a null rather than coerce it.
59879
60639
  */
59880
- signalPercent: number().min(0).max(100).nullable(),
60640
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59881
60641
  /** Raw received signal strength in dBm, when the firmware reports one. */
59882
- rssiDbm: number().optional(),
60642
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
59883
60643
  /** Network name of a wireless link, when the firmware reports it. */
59884
60644
  ssid: string().optional(),
59885
60645
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
59886
- lastUpdated: number()
60646
+ lastUpdated: number().meta(readingMeta("none"))
59887
60647
  });
59888
60648
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
59889
60649
  deviceId: number(),
@@ -60202,7 +60962,7 @@ object({
60202
60962
  lastError: string().nullable(),
60203
60963
  /** Number of deliveries currently buffered server-side (rate-limited
60204
60964
  * notifiers may queue). 0 when the provider sends synchronously. */
60205
- queueDepth: number().int().nonnegative(),
60965
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
60206
60966
  /** Per-feature capability matrix — authoritative source for the
60207
60967
  * compose-form field gating. */
60208
60968
  supports: NotifierSupportsSchema
@@ -60252,15 +61012,8 @@ DeviceType.Notifier, method(NotifierSendInputSchema, NotifierSendResultSchema, {
60252
61012
  acceptedAt: number()
60253
61013
  });
60254
61014
  object({
60255
- value: number(),
60256
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
60257
- * Populated live from the upstream source on each state push.
60258
- * Absent for unitless sensors. */
60259
- unit: string().optional(),
60260
- /** Suggested decimal places for numeric display.
60261
- * Populated live from the upstream source when provided.
60262
- * Falls back to auto-formatting when absent. */
60263
- precision: number().int().min(0).max(10).optional(),
61015
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
61016
+ value: number().nullable().meta(readingMeta("device")),
60264
61017
  /** Ms epoch when the slice was last updated. */
60265
61018
  lastFetchedAt: number()
60266
61019
  });
@@ -60550,23 +61303,26 @@ object({
60550
61303
  /** Food currently in the bowl (grams). Null when the device has not
60551
61304
  * reported a reading yet. On dual-hopper models this is the combined
60552
61305
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
60553
- foodLevel: number().nullable(),
61306
+ foodLevel: number().nullable().meta(readingMeta("none")),
60554
61307
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
60555
61308
  * single-hopper models. */
60556
- food1: number().nullable(),
61309
+ food1: number().nullable().meta(readingMeta("none")),
60557
61310
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
60558
61311
  * single-hopper models. */
60559
- food2: number().nullable(),
61312
+ food2: number().nullable().meta(readingMeta("none")),
60560
61313
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
60561
61314
  * (`device_class: problem`, on = low). True when the bowl is empty /
60562
61315
  * below the feeder's low threshold. */
60563
61316
  lowFood: boolean(),
60564
61317
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
60565
61318
  * device has no battery reading. */
60566
- batteryPower: number().min(0).max(100).nullable(),
60567
- /** Days of desiccant life remaining. Null when the model has no
60568
- * desiccant sensor. */
60569
- desiccantLeftDays: number().nullable(),
61319
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61320
+ /**
61321
+ * Desiccant life remaining, stored in seconds (duration base, D944).
61322
+ * Providers convert from the days the feeder reports. Null when the
61323
+ * model has no desiccant sensor.
61324
+ */
61325
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
60570
61326
  /** True while a feed is in progress. */
60571
61327
  feeding: boolean(),
60572
61328
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -60577,7 +61333,7 @@ object({
60577
61333
  * with `errorCode` for consumers that want the raw integer. */
60578
61334
  error: string().nullable(),
60579
61335
  /** Raw device error code (0 / null = no error). */
60580
- errorCode: number().nullable(),
61336
+ errorCode: number().nullable().meta(readingMeta("none")),
60581
61337
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
60582
61338
  isDualHopper: boolean(),
60583
61339
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -60587,7 +61343,7 @@ object({
60587
61343
  /** Play a chime when dispensing. */
60588
61344
  feedSound: boolean(),
60589
61345
  /** Speaker / prompt volume level (device-scaled integer). */
60590
- volume: number(),
61346
+ volume: number().meta(readingMeta("none")),
60591
61347
  /** Ms epoch when the slice was last refreshed from the cloud. */
60592
61348
  lastFetchedAt: number()
60593
61349
  });
@@ -61024,38 +61780,37 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
61024
61780
  });
61025
61781
  object({
61026
61782
  /** Instantaneous power draw in watts. */
61027
- watts: number().optional().meta({
61028
- unit: "W",
61029
- label: "Power"
61030
- }),
61783
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
61031
61784
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
61032
- kwhTotal: number().optional().meta({
61033
- unit: "kWh",
61034
- label: "Energy"
61035
- }),
61785
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
61036
61786
  /** Voltage in volts. */
61037
- volts: number().optional(),
61787
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
61038
61788
  /** Current in amperes. */
61039
- amps: number().optional(),
61040
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
61041
- va: number().optional(),
61789
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
61790
+ /** Apparent power in volt-amperes. */
61791
+ va: number().optional().meta(readingMeta("apparent-power")),
61042
61792
  /** Power factor (0..1) if reported. */
61043
- powerFactor: number().min(0).max(1).optional(),
61793
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
61044
61794
  /** Ms epoch when the slice was last updated. */
61045
- lastFetchedAt: number(),
61046
- /** Live display unit of the single metric this slice carries (e.g. HA
61047
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
61048
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
61049
- * unit per slice is unambiguous. The UI prefers this over the role's
61050
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
61051
- unit: string().optional(),
61052
- /** Suggested decimal places for numeric display.
61053
- * Populated live from the upstream source when provided (e.g. HA
61054
- * `attributes.suggested_display_precision`). Falls back to
61055
- * auto-formatting when absent. */
61056
- precision: number().int().min(0).max(10).optional()
61795
+ lastFetchedAt: number()
61057
61796
  });
61058
- DeviceType.Sensor;
61797
+ [
61798
+ DeviceType.Sensor,
61799
+ DeviceType.Switch,
61800
+ DeviceType.Light,
61801
+ DeviceType.Fan,
61802
+ DeviceType.Climate,
61803
+ DeviceType.Thermostat,
61804
+ DeviceType.Humidifier,
61805
+ DeviceType.WaterHeater,
61806
+ DeviceType.MediaPlayer,
61807
+ DeviceType.Washer,
61808
+ DeviceType.Dryer,
61809
+ DeviceType.Dishwasher,
61810
+ DeviceType.Vacuum,
61811
+ DeviceType.LawnMower,
61812
+ DeviceType.PetFeeder
61813
+ ].filter((type) => type !== DeviceType.Sensor);
61059
61814
  /**
61060
61815
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
61061
61816
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -61074,11 +61829,11 @@ DeviceType.Sensor;
61074
61829
  */
61075
61830
  var GpsLocationSchema = object({
61076
61831
  /** Latitude in decimal degrees, -90..90. */
61077
- latitude: number().min(-90).max(90),
61832
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
61078
61833
  /** Longitude in decimal degrees, -180..180. */
61079
- longitude: number().min(-180).max(180),
61834
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
61080
61835
  /** Reported accuracy in meters (lower = better). */
61081
- accuracyMeters: number().nonnegative()
61836
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
61082
61837
  });
61083
61838
  object({
61084
61839
  /** `home` / `not_home` / any user-defined zone name. */
@@ -61090,25 +61845,16 @@ object({
61090
61845
  gps: GpsLocationSchema.nullable(),
61091
61846
  /** Optional battery level of the tracking device (0..100). Null
61092
61847
  * when not reported. */
61093
- batteryPercent: number().min(0).max(100).nullable(),
61848
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61094
61849
  /** Ms epoch when the slice was last updated. */
61095
61850
  lastChangedAt: number()
61096
61851
  });
61097
61852
  DeviceType.Presence;
61098
61853
  object({
61099
- /** Current pressure in hPa. */
61100
- hpa: number(),
61854
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
61855
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
61101
61856
  /** Ms epoch when the slice was last updated. */
61102
- lastFetchedAt: number(),
61103
- /** Live display unit from the upstream source (e.g. HA
61104
- * `attributes.unit_of_measurement`). The UI prefers this over the
61105
- * role's canonical unit. Absent → fall back to the canonical unit. */
61106
- unit: string().optional(),
61107
- /** Suggested decimal places for numeric display.
61108
- * Populated live from the upstream source when provided (e.g. HA
61109
- * `attributes.suggested_display_precision`). Falls back to
61110
- * auto-formatting when absent. */
61111
- precision: number().int().min(0).max(10).optional()
61857
+ lastFetchedAt: number()
61112
61858
  });
61113
61859
  DeviceType.Sensor;
61114
61860
  /**
@@ -61441,8 +62187,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
61441
62187
  }), _void(), { kind: "mutation" });
61442
62188
  var PtzAutotrackSettingsSchema = object({
61443
62189
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
61444
- stopDelaySeconds: number().int().min(0).max(300),
61445
- disappearDelaySeconds: number().int().min(0).max(300)
62190
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
62191
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
61446
62192
  });
61447
62193
  /**
61448
62194
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -61499,7 +62245,7 @@ object({
61499
62245
  /** null = unknown — never coerced to false (D393). */
61500
62246
  running: boolean().nullable(),
61501
62247
  /** Epoch ms the current boolean began; null while `running` is null. */
61502
- since: number().nullable(),
62248
+ since: number().nullable().meta(readingMeta("none")),
61503
62249
  source: RunningStateSourceSchema.meta({ label: "Source" }),
61504
62250
  phase: string().nullable(),
61505
62251
  /** null until a reading has been published (D393). */
@@ -61511,7 +62257,7 @@ object({
61511
62257
  phase: string().nullable()
61512
62258
  });
61513
62259
  Object.values(DeviceType);
61514
- DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
62260
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
61515
62261
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
61516
62262
  kind: "mutation",
61517
62263
  auth: "admin"
@@ -62697,9 +63443,9 @@ var RecordTriggerSchema = _enum([
62697
63443
  */
62698
63444
  var RecordWindowSchema = object({
62699
63445
  trigger: RecordTriggerSchema,
62700
- day: number().int().min(0).max(6),
62701
- startMinute: number().int().min(0).max(1439),
62702
- endMinute: number().int().min(1).max(1440)
63446
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
63447
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
63448
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
62703
63449
  });
62704
63450
  /** Status of one physical volume, as the camera itself describes it. */
62705
63451
  var OnboardStorageVolumeSchema = object({
@@ -62768,10 +63514,10 @@ object({
62768
63514
  /** Master "record to the card at all" switch. */
62769
63515
  enabled: boolean().nullable(),
62770
63516
  overwriteWhenFull: boolean().nullable(),
62771
- preRecordSec: number().nullable(),
62772
- postRecordSec: number().nullable(),
63517
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
63518
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
62773
63519
  /** Length of one recorded file, in minutes. */
62774
- segmentMinutes: number().nullable(),
63520
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
62775
63521
  /** The primary track's weekly windows, flattened. */
62776
63522
  windows: array(RecordWindowSchema),
62777
63523
  /**
@@ -62787,7 +63533,7 @@ object({
62787
63533
  * provider reporting a non-zero count also reports the schedule as not
62788
63534
  * writable.
62789
63535
  */
62790
- unreadableWindows: number(),
63536
+ unreadableWindows: number().meta(readingMeta("count")),
62791
63537
  /**
62792
63538
  * The camera is scheduled to record and has NO usable storage.
62793
63539
  *
@@ -63424,7 +64170,7 @@ var CamStreamDescriptorSchema = object({
63424
64170
  url: string().optional(),
63425
64171
  codec: string().optional(),
63426
64172
  resolution: CamStreamResolutionSchema.optional(),
63427
- fps: number().positive().optional(),
64173
+ fps: number().positive().optional().meta(readingMeta("rate")),
63428
64174
  label: string().optional(),
63429
64175
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
63430
64176
  deviceFeatures: array(string()).optional(),
@@ -63454,18 +64200,18 @@ var StreamProfileSchema = _enum([
63454
64200
  ]);
63455
64201
  /** The current encoding config of one stream profile. */
63456
64202
  var StreamProfileConfigSchema = object({
63457
- width: number(),
63458
- height: number(),
64203
+ width: number().meta(readingMeta("none")),
64204
+ height: number().meta(readingMeta("none")),
63459
64205
  codec: _enum(["h264", "h265"]),
63460
- framerate: number(),
63461
- bitrate: number(),
64206
+ framerate: number().meta(readingMeta("rate")),
64207
+ bitrate: number().meta(readingMeta("bitrate")),
63462
64208
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
63463
64209
  encoderProfile: _enum([
63464
64210
  "high",
63465
64211
  "main",
63466
64212
  "baseline"
63467
64213
  ]).optional(),
63468
- gop: number().optional(),
64214
+ gop: number().optional().meta(readingMeta("none")),
63469
64215
  /**
63470
64216
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
63471
64217
  *
@@ -63550,7 +64296,7 @@ object({
63550
64296
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
63551
64297
  lastChangedAt: number()
63552
64298
  });
63553
- DeviceType.Switch, DeviceType.Siren, DeviceType.Light, method(object({
64299
+ DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, method(object({
63554
64300
  deviceId: number(),
63555
64301
  on: boolean()
63556
64302
  }), _void(), {
@@ -64108,23 +64854,13 @@ object({
64108
64854
  });
64109
64855
  DeviceType.Sensor;
64110
64856
  object({
64111
- /** Current temperature in Celsius. */
64112
- celsius: number().meta({
64113
- unit: "°C",
64857
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
64858
+ celsius: number().nullable().meta(readingMeta("temperature", {
64859
+ precision: 1,
64114
64860
  label: "Temperature"
64115
- }),
64861
+ })),
64116
64862
  /** Ms epoch when the slice was last updated (push or poll). */
64117
- lastFetchedAt: number(),
64118
- /** Live display unit from the upstream source (e.g. HA
64119
- * `attributes.unit_of_measurement`). The UI prefers this over the
64120
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
64121
- * Absent → the UI falls back to the role's canonical unit. */
64122
- unit: string().optional(),
64123
- /** Suggested decimal places for numeric display.
64124
- * Populated live from the upstream source when provided (e.g. HA
64125
- * `attributes.suggested_display_precision`). Falls back to
64126
- * auto-formatting when absent. */
64127
- precision: number().int().min(0).max(10).optional()
64863
+ lastFetchedAt: number()
64128
64864
  });
64129
64865
  DeviceType.Sensor, DeviceType.Thermostat;
64130
64866
  /**
@@ -64376,16 +65112,16 @@ object({
64376
65112
  ]),
64377
65113
  /** 0..100 battery percentage. Null when the device has no battery
64378
65114
  * reading. */
64379
- batteryLevel: number().min(0).max(100).nullable(),
65115
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64380
65116
  /** Current fan-speed token (provider-verbatim). Null when unknown or
64381
65117
  * the vacuum has no speed control. */
64382
65118
  fanSpeed: string().nullable(),
64383
65119
  /** Speed tokens the hardware accepts — drives the UI selector. */
64384
65120
  availableFanSpeeds: array(string()),
64385
65121
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64386
- progressPercent: number().min(0).max(100).nullable(),
65122
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64387
65123
  /** Current error code (0 / null = no error). */
64388
- errorCode: number().nullable(),
65124
+ errorCode: number().nullable().meta(readingMeta("none")),
64389
65125
  /** Human label for {@link errorCode}, or null when none / undecodable. */
64390
65126
  errorLabel: string().nullable(),
64391
65127
  /** Ms epoch when the slice was last updated. */
@@ -64424,7 +65160,7 @@ object({
64424
65160
  ]),
64425
65161
  /** 0 = fully closed, 100 = fully open. Null when the device has no
64426
65162
  * intermediate position surface. */
64427
- position: number().min(0).max(100).nullable(),
65163
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64428
65164
  /** Ms epoch when the slice was last updated. */
64429
65165
  lastChangedAt: number()
64430
65166
  });
@@ -64452,9 +65188,9 @@ object({
64452
65188
  DeviceType.Sensor;
64453
65189
  object({
64454
65190
  /** Current measured temperature. Null when not reported. */
64455
- currentTemp: number().nullable(),
65191
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
64456
65192
  /** Target temperature setpoint. Null when no setpoint surface. */
64457
- targetTemp: number().nullable(),
65193
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
64458
65194
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
64459
65195
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
64460
65196
  * device reports an unknown state. */
@@ -64465,9 +65201,9 @@ object({
64465
65201
  * has no away surface. */
64466
65202
  away: boolean().nullable(),
64467
65203
  /** HA `min_temp` attribute. Null when not reported. */
64468
- minTemp: number().nullable(),
65204
+ minTemp: number().nullable().meta(readingMeta("temperature")),
64469
65205
  /** HA `max_temp` attribute. Null when not reported. */
64470
- maxTemp: number().nullable(),
65206
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
64471
65207
  /** Ms epoch when the slice was last updated. */
64472
65208
  lastChangedAt: number()
64473
65209
  });
@@ -64491,25 +65227,18 @@ DeviceType.WaterHeater, method(object({
64491
65227
  auth: "protected"
64492
65228
  });
64493
65229
  object({
64494
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
64495
- * when no condition has been reported yet. */
65230
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
64496
65231
  condition: string().nullable(),
64497
- /** Current temperature in the reported unit. Null when not provided. */
64498
- temperature: number().nullable(),
64499
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
64500
- temperatureUnit: string().nullable(),
65232
+ /** Current temperature in Celsius. Null when not provided or refused. */
65233
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
64501
65234
  /** Relative humidity (0..100). Null when not provided. */
64502
- humidity: number().min(0).max(100).nullable(),
64503
- /** Barometric pressure in the reported unit. Null when not provided. */
64504
- pressure: number().nullable(),
64505
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
64506
- pressureUnit: string().nullable(),
64507
- /** Wind speed in the reported unit. Null when not provided. */
64508
- windSpeed: number().nullable(),
64509
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
64510
- windSpeedUnit: string().nullable(),
65235
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
65236
+ /** Barometric pressure in hPa. Null when not provided or refused. */
65237
+ pressure: number().nullable().meta(readingMeta("pressure")),
65238
+ /** Wind speed in metres per second. Null when not provided or refused. */
65239
+ windSpeed: number().nullable().meta(readingMeta("speed")),
64511
65240
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
64512
- windBearing: number().nullable(),
65241
+ windBearing: number().nullable().meta(readingMeta("angle")),
64513
65242
  /** Ms epoch when the slice was last updated. */
64514
65243
  lastFetchedAt: number()
64515
65244
  });
@@ -65259,8 +65988,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
65259
65988
  kind: "mutation",
65260
65989
  auth: "admin"
65261
65990
  });
65991
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
65262
65992
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
65263
- 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;
65993
+ 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;
65264
65994
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
65265
65995
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
65266
65996
  new Set(CAP_NAMES);
@@ -65313,7 +66043,7 @@ var AccessoryKind = {
65313
66043
  PrivacyMask: DeviceRole.PrivacyMask
65314
66044
  };
65315
66045
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
65316
- 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;
66046
+ 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;
65317
66047
  new Set(Object.values(DeviceType));
65318
66048
  Object.freeze({
65319
66049
  "accessories.getStatus": {
@@ -65421,6 +66151,13 @@ Object.freeze({
65421
66151
  access: "view",
65422
66152
  role: "admin"
65423
66153
  },
66154
+ "addons.getPackageDocs": {
66155
+ capName: "addons",
66156
+ capScope: "system",
66157
+ addonId: null,
66158
+ access: "view",
66159
+ role: "admin"
66160
+ },
65424
66161
  "addons.getVersions": {
65425
66162
  capName: "addons",
65426
66163
  capScope: "system",
@@ -68529,6 +69266,27 @@ Object.freeze({
68529
69266
  access: "view",
68530
69267
  role: "viewer"
68531
69268
  },
69269
+ "groupActive.getStatus": {
69270
+ capName: "group-active",
69271
+ capScope: "device",
69272
+ addonId: null,
69273
+ access: "view",
69274
+ role: "viewer"
69275
+ },
69276
+ "groupCommands.getStatus": {
69277
+ capName: "group-commands",
69278
+ capScope: "device",
69279
+ addonId: null,
69280
+ access: "view",
69281
+ role: "viewer"
69282
+ },
69283
+ "groupStatus.getStatus": {
69284
+ capName: "group-status",
69285
+ capScope: "device",
69286
+ addonId: null,
69287
+ access: "view",
69288
+ role: "viewer"
69289
+ },
68532
69290
  "heatingValve.getStatus": {
68533
69291
  capName: "heating-valve",
68534
69292
  capScope: "device",
@@ -69481,6 +70239,13 @@ Object.freeze({
69481
70239
  access: "create",
69482
70240
  role: "admin"
69483
70241
  },
70242
+ "meter.getStatus": {
70243
+ capName: "meter",
70244
+ capScope: "device",
70245
+ addonId: null,
70246
+ access: "view",
70247
+ role: "viewer"
70248
+ },
69484
70249
  "metricsProvider.collectSnapshot": {
69485
70250
  capName: "metrics-provider",
69486
70251
  capScope: "system",
@@ -77579,7 +78344,39 @@ var APPLIANCE_CAP_VOCABULARY = {
77579
78344
  phrase: t("When {device} finishes", "Quando {device} finisce"),
77580
78345
  icon: "square"
77581
78346
  }
77582
- }
78347
+ },
78348
+ overrides: { byType: {
78349
+ [DeviceType.Washer]: {
78350
+ state: { running: {
78351
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
78352
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
78353
+ } },
78354
+ events: {
78355
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
78356
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
78357
+ }
78358
+ },
78359
+ [DeviceType.Dryer]: {
78360
+ state: { running: {
78361
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
78362
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
78363
+ } },
78364
+ events: {
78365
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
78366
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
78367
+ }
78368
+ },
78369
+ [DeviceType.Dishwasher]: {
78370
+ state: { running: {
78371
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
78372
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
78373
+ } },
78374
+ events: {
78375
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
78376
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
78377
+ }
78378
+ }
78379
+ } }
77583
78380
  },
77584
78381
  update: {
77585
78382
  scope: "device",
@@ -78351,6 +79148,41 @@ var SENSOR_CAP_VOCABULARY = {
78351
79148
  icon: "flame"
78352
79149
  }]
78353
79150
  },
79151
+ "group-active": {
79152
+ scope: "device",
79153
+ noun: t("Group state", "Stato del gruppo")
79154
+ },
79155
+ "group-commands": {
79156
+ scope: "device",
79157
+ noun: t("Group command", "Comando di gruppo")
79158
+ },
79159
+ "group-status": {
79160
+ scope: "device",
79161
+ noun: t("Group", "Gruppo"),
79162
+ state: [
79163
+ {
79164
+ kind: "numeric",
79165
+ field: "included",
79166
+ quantity: t("the members reporting", "i membri che rispondono"),
79167
+ label: t("Reporting", "Rispondono"),
79168
+ icon: "users"
79169
+ },
79170
+ {
79171
+ kind: "numeric",
79172
+ field: "excluded",
79173
+ quantity: t("the members left out", "i membri esclusi"),
79174
+ label: t("Excluded", "Esclusi"),
79175
+ icon: "users"
79176
+ },
79177
+ {
79178
+ kind: "numeric",
79179
+ field: "total",
79180
+ quantity: t("the member count", "il numero di membri"),
79181
+ label: t("Members", "Membri"),
79182
+ icon: "sigma"
79183
+ }
79184
+ ]
79185
+ },
78354
79186
  "humidity-sensor": {
78355
79187
  scope: "device",
78356
79188
  noun: t("Humidity", "Umidità"),
@@ -78376,6 +79208,17 @@ var SENSOR_CAP_VOCABULARY = {
78376
79208
  }],
78377
79209
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
78378
79210
  },
79211
+ meter: {
79212
+ scope: "device",
79213
+ noun: t("Meter", "Contatore"),
79214
+ state: [{
79215
+ kind: "numeric",
79216
+ field: "value",
79217
+ quantity: t("the total", "il totale"),
79218
+ label: t("Total", "Totale"),
79219
+ icon: "gauge"
79220
+ }]
79221
+ },
78379
79222
  "numeric-sensor": {
78380
79223
  scope: "device",
78381
79224
  noun: t("Sensor", "Sensore"),
@@ -78384,7 +79227,6 @@ var SENSOR_CAP_VOCABULARY = {
78384
79227
  field: "value",
78385
79228
  quantity: t("the reading", "il valore"),
78386
79229
  label: t("Reading", "Valore"),
78387
- unitField: "unit",
78388
79230
  icon: "gauge"
78389
79231
  }]
78390
79232
  },
@@ -78520,7 +79362,7 @@ var SENSOR_CAP_VOCABULARY = {
78520
79362
  field: "temperature",
78521
79363
  quantity: t("the outdoor temperature", "la temperatura esterna"),
78522
79364
  label: t("Temperature", "Temperatura"),
78523
- unitField: "temperatureUnit",
79365
+ unit: "°C",
78524
79366
  icon: "thermometer"
78525
79367
  },
78526
79368
  {
@@ -78536,7 +79378,7 @@ var SENSOR_CAP_VOCABULARY = {
78536
79378
  field: "windSpeed",
78537
79379
  quantity: t("the wind speed", "la velocità del vento"),
78538
79380
  label: t("Wind", "Vento"),
78539
- unitField: "windSpeedUnit",
79381
+ unit: "m/s",
78540
79382
  icon: "wind"
78541
79383
  },
78542
79384
  {
@@ -78544,7 +79386,7 @@ var SENSOR_CAP_VOCABULARY = {
78544
79386
  field: "pressure",
78545
79387
  quantity: t("the air pressure", "la pressione atmosferica"),
78546
79388
  label: t("Pressure", "Pressione"),
78547
- unitField: "pressureUnit",
79389
+ unit: "hPa",
78548
79390
  icon: "gauge"
78549
79391
  }
78550
79392
  ]
@@ -78849,8 +79691,47 @@ function projectAction(cap, action) {
78849
79691
  };
78850
79692
  }
78851
79693
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
78852
- 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;
79694
+ 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;
78853
79695
  DeviceFeature.BatteryOperated;
79696
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
79697
+ 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.";
79698
+ 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.";
79699
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
79700
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
79701
+ 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.";
79702
+ function withheld(reason) {
79703
+ return {
79704
+ [DeviceType.Washer]: {
79705
+ exported: false,
79706
+ reason
79707
+ },
79708
+ [DeviceType.Dryer]: {
79709
+ exported: false,
79710
+ reason
79711
+ },
79712
+ [DeviceType.Dishwasher]: {
79713
+ exported: false,
79714
+ reason
79715
+ }
79716
+ };
79717
+ }
79718
+ function offered(received) {
79719
+ return {
79720
+ [DeviceType.Washer]: {
79721
+ exported: true,
79722
+ as: received
79723
+ },
79724
+ [DeviceType.Dryer]: {
79725
+ exported: true,
79726
+ as: received
79727
+ },
79728
+ [DeviceType.Dishwasher]: {
79729
+ exported: true,
79730
+ as: received
79731
+ }
79732
+ };
79733
+ }
79734
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
78854
79735
  Object.freeze({
78855
79736
  "accessories.getStatus": [{
78856
79737
  name: "deviceId",
@@ -79817,6 +80698,21 @@ Object.freeze({
79817
80698
  form: "single",
79818
80699
  optional: false
79819
80700
  }],
80701
+ "groupActive.getStatus": [{
80702
+ name: "deviceId",
80703
+ form: "single",
80704
+ optional: false
80705
+ }],
80706
+ "groupCommands.getStatus": [{
80707
+ name: "deviceId",
80708
+ form: "single",
80709
+ optional: false
80710
+ }],
80711
+ "groupStatus.getStatus": [{
80712
+ name: "deviceId",
80713
+ form: "single",
80714
+ optional: false
80715
+ }],
79820
80716
  "heatingValve.getStatus": [{
79821
80717
  name: "deviceId",
79822
80718
  form: "single",
@@ -80017,6 +80913,11 @@ Object.freeze({
80017
80913
  form: "single",
80018
80914
  optional: false
80019
80915
  }],
80916
+ "meter.getStatus": [{
80917
+ name: "deviceId",
80918
+ form: "single",
80919
+ optional: false
80920
+ }],
80020
80921
  "motion.getStatus": [{
80021
80922
  name: "deviceId",
80022
80923
  form: "single",
@@ -81624,7 +82525,6 @@ Object.freeze({
81624
82525
  "network-access": "ingress",
81625
82526
  "smtp-provider": "email"
81626
82527
  });
81627
- DeviceType.Camera;
81628
82528
  /**
81629
82529
  * A saved Customize assignment (D936). The schema is the type: callers use
81630
82530
  * `z.infer`, they do not restate the shape.
@@ -81801,6 +82701,8 @@ new Map(Object.entries({
81801
82701
  "detection-blind": "device-events",
81802
82702
  "motion-noisy": "device-events",
81803
82703
  "motion-calm": "device-events",
82704
+ "package-llm-silent": "device-events",
82705
+ "package-llm-answered": "device-events",
81804
82706
  "recording-stalled": "device-events",
81805
82707
  "recording-healthy": "device-events",
81806
82708
  "recording-degraded": "device-events",
@@ -81884,6 +82786,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
81884
82786
  label: "Motion without a subject",
81885
82787
  kinds: ["motion-noisy", "motion-calm"]
81886
82788
  },
82789
+ {
82790
+ id: "package-llm",
82791
+ family: "device-events",
82792
+ label: "Package confirm",
82793
+ kinds: ["package-llm-silent", "package-llm-answered"]
82794
+ },
81887
82795
  {
81888
82796
  id: "recording",
81889
82797
  family: "device-events",
@@ -82065,6 +82973,8 @@ var DEVICE_EVENTS = [
82065
82973
  "detection-blind",
82066
82974
  "motion-noisy",
82067
82975
  "motion-calm",
82976
+ "package-llm-silent",
82977
+ "package-llm-answered",
82068
82978
  "recording-stalled",
82069
82979
  "recording-healthy",
82070
82980
  "recording-degraded",
@@ -82333,6 +83243,15 @@ new Map([
82333
83243
  snapshotLabel: "Cameras with constant motion now",
82334
83244
  snapshotSection: null
82335
83245
  },
83246
+ {
83247
+ id: "package-llm-confirm",
83248
+ family: "device-events",
83249
+ label: "Package confirm (not answering / answering again)",
83250
+ bad: ["package-llm-silent"],
83251
+ recovery: ["package-llm-answered"],
83252
+ snapshotLabel: "Cameras whose package confirm is not answering",
83253
+ snapshotSection: null
83254
+ },
82336
83255
  {
82337
83256
  id: "recording-health",
82338
83257
  family: "device-events",