@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.
@@ -6,7 +6,7 @@ let node_fs = require("node:fs");
6
6
  let node_path = require("node:path");
7
7
  let node_child_process = require("node:child_process");
8
8
  let node_url = require("node:url");
9
- //#region ../types/dist/event-category-DHXxB8t9.mjs
9
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
10
10
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
11
11
  EventCategory["SystemBoot"] = "system.boot";
12
12
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -332,6 +332,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
332
332
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
333
333
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
334
334
  /**
335
+ * A camera's package LLM confirm has failed closed three times in a row
336
+ * (D931, 2026-10-26). One edge per camera until
337
+ * {@link PipelinePackageLlmAnswered}.
338
+ */
339
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
340
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
341
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
342
+ /**
335
343
  * Per-camera pipeline config was mutated by the orchestrator
336
344
  * (a settings change via `setAgentInferenceDevices` /
337
345
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5574,7 +5582,7 @@ var ZodIssueCode = {
5574
5582
  var ZodFirstPartyTypeKind;
5575
5583
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5576
5584
  //#endregion
5577
- //#region ../types/dist/sleep-BErAxhul.mjs
5585
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5578
5586
  /**
5579
5587
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5580
5588
  * window to float samples (D455).
@@ -8326,6 +8334,99 @@ function defineEntryKind(name, spec) {
8326
8334
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8327
8335
  return defined;
8328
8336
  }
8337
+ var READING_QUANTITIES = [
8338
+ "temperature",
8339
+ "pressure",
8340
+ "speed",
8341
+ "precipitation",
8342
+ "precipitation-rate",
8343
+ "length",
8344
+ "area",
8345
+ "illuminance",
8346
+ "irradiance",
8347
+ "power",
8348
+ "energy",
8349
+ "voltage",
8350
+ "current",
8351
+ "apparent-power",
8352
+ "humidity",
8353
+ "percentage",
8354
+ "concentration-volume",
8355
+ "concentration-mass",
8356
+ "concentration-ppb",
8357
+ "volume",
8358
+ "duration",
8359
+ "duration-ms",
8360
+ "information",
8361
+ "sound-level",
8362
+ "signal-level",
8363
+ "colour-temperature",
8364
+ "angle",
8365
+ "bitrate",
8366
+ "rate",
8367
+ "mass",
8368
+ "index",
8369
+ "ratio",
8370
+ "count",
8371
+ "none",
8372
+ "device"
8373
+ ];
8374
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8375
+ new Set(READING_QUANTITIES);
8376
+ /**
8377
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8378
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8379
+ */
8380
+ var QUANTITY_BASE = {
8381
+ temperature: "°C",
8382
+ pressure: "hPa",
8383
+ speed: "m/s",
8384
+ precipitation: "mm",
8385
+ "precipitation-rate": "mm/h",
8386
+ length: "m",
8387
+ area: "m²",
8388
+ illuminance: "lx",
8389
+ irradiance: "W/m²",
8390
+ power: "W",
8391
+ energy: "kWh",
8392
+ voltage: "V",
8393
+ current: "A",
8394
+ "apparent-power": "VA",
8395
+ humidity: "%",
8396
+ percentage: "%",
8397
+ "concentration-volume": "ppm",
8398
+ "concentration-mass": "µg/m³",
8399
+ "concentration-ppb": "ppb",
8400
+ volume: "L",
8401
+ duration: "s",
8402
+ "duration-ms": "ms",
8403
+ information: "B",
8404
+ "sound-level": "dBFS",
8405
+ "signal-level": "dBm",
8406
+ "colour-temperature": "mired",
8407
+ angle: "°",
8408
+ bitrate: "kbps",
8409
+ rate: "fps",
8410
+ mass: "kg",
8411
+ index: "none",
8412
+ ratio: "none",
8413
+ count: "none",
8414
+ none: "none",
8415
+ device: "device"
8416
+ };
8417
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8418
+ function baseUnitOf(quantity) {
8419
+ const symbol = QUANTITY_BASE[quantity];
8420
+ if (symbol === "none" || symbol === "device") return void 0;
8421
+ return symbol;
8422
+ }
8423
+ function readingMeta(quantity, extras) {
8424
+ return {
8425
+ quantity,
8426
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8427
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8428
+ };
8429
+ }
8329
8430
  /** Shared Zod schemas used across streaming capabilities. */
8330
8431
  var CamProfileSchema = _enum([
8331
8432
  "high",
@@ -8353,8 +8454,8 @@ var CamStreamKindSchema = _enum([
8353
8454
  "derived"
8354
8455
  ]);
8355
8456
  var CamStreamResolutionSchema = object({
8356
- width: number().int().positive(),
8357
- height: number().int().positive()
8457
+ width: number().int().positive().meta(readingMeta("none")),
8458
+ height: number().int().positive().meta(readingMeta("none"))
8358
8459
  });
8359
8460
  var CameraStreamSchema = object({
8360
8461
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8890,6 +8991,9 @@ var CAP_NAMES = Object.freeze([
8890
8991
  "filesystem-browse",
8891
8992
  "flood",
8892
8993
  "gas",
8994
+ "group-active",
8995
+ "group-commands",
8996
+ "group-status",
8893
8997
  "heating-valve",
8894
8998
  "humidifier",
8895
8999
  "humidity-sensor",
@@ -8910,6 +9014,7 @@ var CAP_NAMES = Object.freeze([
8910
9014
  "login-method",
8911
9015
  "media-player",
8912
9016
  "mesh-network",
9017
+ "meter",
8913
9018
  "metrics-provider",
8914
9019
  "model-convert",
8915
9020
  "model-distributor",
@@ -9019,14 +9124,6 @@ Object.freeze({
9019
9124
  {
9020
9125
  "path": "aqi",
9021
9126
  "valueType": "number"
9022
- },
9023
- {
9024
- "path": "unit",
9025
- "valueType": "string"
9026
- },
9027
- {
9028
- "path": "precision",
9029
- "valueType": "number"
9030
9127
  }
9031
9128
  ],
9032
9129
  "alarm-panel": [
@@ -9062,20 +9159,10 @@ Object.freeze({
9062
9159
  "valueType": "boolean"
9063
9160
  }
9064
9161
  ],
9065
- "ambient-light-sensor": [
9066
- {
9067
- "path": "lux",
9068
- "valueType": "number"
9069
- },
9070
- {
9071
- "path": "unit",
9072
- "valueType": "string"
9073
- },
9074
- {
9075
- "path": "precision",
9076
- "valueType": "number"
9077
- }
9078
- ],
9162
+ "ambient-light-sensor": [{
9163
+ "path": "lux",
9164
+ "valueType": "number"
9165
+ }],
9079
9166
  "audio-metrics": [
9080
9167
  {
9081
9168
  "path": "ts",
@@ -9155,6 +9242,10 @@ Object.freeze({
9155
9242
  "path": "lastUpdated",
9156
9243
  "valueType": "number"
9157
9244
  },
9245
+ {
9246
+ "path": "volts",
9247
+ "valueType": "number"
9248
+ },
9158
9249
  {
9159
9250
  "path": "lastContactAt",
9160
9251
  "valueType": "number"
@@ -9397,10 +9488,6 @@ Object.freeze({
9397
9488
  "datetime"
9398
9489
  ]
9399
9490
  },
9400
- {
9401
- "path": "unit",
9402
- "valueType": "string"
9403
- },
9404
9491
  {
9405
9492
  "path": "min",
9406
9493
  "valueType": "number"
@@ -9412,19 +9499,6 @@ Object.freeze({
9412
9499
  {
9413
9500
  "path": "step",
9414
9501
  "valueType": "number"
9415
- },
9416
- {
9417
- "path": "precision",
9418
- "valueType": "number"
9419
- },
9420
- {
9421
- "path": "format",
9422
- "valueType": "enum",
9423
- "enumValues": [
9424
- "date",
9425
- "time",
9426
- "datetime"
9427
- ]
9428
9502
  }
9429
9503
  ],
9430
9504
  "cover": [
@@ -9665,13 +9739,8 @@ Object.freeze({
9665
9739
  "path": "value",
9666
9740
  "valueType": "string"
9667
9741
  }, {
9668
- "path": "format",
9669
- "valueType": "enum",
9670
- "enumValues": [
9671
- "date",
9672
- "time",
9673
- "datetime"
9674
- ]
9742
+ "path": "timestamp",
9743
+ "valueType": "boolean"
9675
9744
  }],
9676
9745
  "event-emitter": [
9677
9746
  {
@@ -9744,6 +9813,24 @@ Object.freeze({
9744
9813
  "path": "detected",
9745
9814
  "valueType": "boolean"
9746
9815
  }],
9816
+ "group-active": [{
9817
+ "path": "active",
9818
+ "valueType": "boolean"
9819
+ }],
9820
+ "group-status": [
9821
+ {
9822
+ "path": "included",
9823
+ "valueType": "number"
9824
+ },
9825
+ {
9826
+ "path": "excluded",
9827
+ "valueType": "number"
9828
+ },
9829
+ {
9830
+ "path": "total",
9831
+ "valueType": "number"
9832
+ }
9833
+ ],
9747
9834
  "heating-valve": [{
9748
9835
  "path": "position",
9749
9836
  "valueType": "number"
@@ -9778,20 +9865,10 @@ Object.freeze({
9778
9865
  "valueType": "number"
9779
9866
  }
9780
9867
  ],
9781
- "humidity-sensor": [
9782
- {
9783
- "path": "percent",
9784
- "valueType": "number"
9785
- },
9786
- {
9787
- "path": "unit",
9788
- "valueType": "string"
9789
- },
9790
- {
9791
- "path": "precision",
9792
- "valueType": "number"
9793
- }
9794
- ],
9868
+ "humidity-sensor": [{
9869
+ "path": "percent",
9870
+ "valueType": "number"
9871
+ }],
9795
9872
  "image": [{
9796
9873
  "path": "url",
9797
9874
  "valueType": "string"
@@ -9999,6 +10076,10 @@ Object.freeze({
9999
10076
  ]
10000
10077
  }
10001
10078
  ],
10079
+ "meter": [{
10080
+ "path": "value",
10081
+ "valueType": "number"
10082
+ }],
10002
10083
  "motion": [
10003
10084
  {
10004
10085
  "path": "detected",
@@ -10130,20 +10211,10 @@ Object.freeze({
10130
10211
  "valueType": "boolean"
10131
10212
  }
10132
10213
  ],
10133
- "numeric-sensor": [
10134
- {
10135
- "path": "value",
10136
- "valueType": "number"
10137
- },
10138
- {
10139
- "path": "unit",
10140
- "valueType": "string"
10141
- },
10142
- {
10143
- "path": "precision",
10144
- "valueType": "number"
10145
- }
10146
- ],
10214
+ "numeric-sensor": [{
10215
+ "path": "value",
10216
+ "valueType": "number"
10217
+ }],
10147
10218
  "pet-feeder": [
10148
10219
  {
10149
10220
  "path": "foodLevel",
@@ -10235,14 +10306,6 @@ Object.freeze({
10235
10306
  {
10236
10307
  "path": "powerFactor",
10237
10308
  "valueType": "number"
10238
- },
10239
- {
10240
- "path": "unit",
10241
- "valueType": "string"
10242
- },
10243
- {
10244
- "path": "precision",
10245
- "valueType": "number"
10246
10309
  }
10247
10310
  ],
10248
10311
  "presence": [
@@ -10271,20 +10334,10 @@ Object.freeze({
10271
10334
  "valueType": "number"
10272
10335
  }
10273
10336
  ],
10274
- "pressure-sensor": [
10275
- {
10276
- "path": "hpa",
10277
- "valueType": "number"
10278
- },
10279
- {
10280
- "path": "unit",
10281
- "valueType": "string"
10282
- },
10283
- {
10284
- "path": "precision",
10285
- "valueType": "number"
10286
- }
10287
- ],
10337
+ "pressure-sensor": [{
10338
+ "path": "hpa",
10339
+ "valueType": "number"
10340
+ }],
10288
10341
  "privacy-mask": [{
10289
10342
  "path": "enabled",
10290
10343
  "valueType": "boolean"
@@ -10595,20 +10648,10 @@ Object.freeze({
10595
10648
  "path": "tampered",
10596
10649
  "valueType": "boolean"
10597
10650
  }],
10598
- "temperature-sensor": [
10599
- {
10600
- "path": "celsius",
10601
- "valueType": "number"
10602
- },
10603
- {
10604
- "path": "unit",
10605
- "valueType": "string"
10606
- },
10607
- {
10608
- "path": "precision",
10609
- "valueType": "number"
10610
- }
10611
- ],
10651
+ "temperature-sensor": [{
10652
+ "path": "celsius",
10653
+ "valueType": "number"
10654
+ }],
10612
10655
  "update": [
10613
10656
  {
10614
10657
  "path": "currentVersion",
@@ -10719,10 +10762,6 @@ Object.freeze({
10719
10762
  "path": "temperature",
10720
10763
  "valueType": "number"
10721
10764
  },
10722
- {
10723
- "path": "temperatureUnit",
10724
- "valueType": "string"
10725
- },
10726
10765
  {
10727
10766
  "path": "humidity",
10728
10767
  "valueType": "number"
@@ -10731,18 +10770,10 @@ Object.freeze({
10731
10770
  "path": "pressure",
10732
10771
  "valueType": "number"
10733
10772
  },
10734
- {
10735
- "path": "pressureUnit",
10736
- "valueType": "string"
10737
- },
10738
10773
  {
10739
10774
  "path": "windSpeed",
10740
10775
  "valueType": "number"
10741
10776
  },
10742
- {
10743
- "path": "windSpeedUnit",
10744
- "valueType": "string"
10745
- },
10746
10777
  {
10747
10778
  "path": "windBearing",
10748
10779
  "valueType": "number"
@@ -10792,7 +10823,7 @@ object({
10792
10823
  params: record(string(), union([string(), number()])).optional()
10793
10824
  });
10794
10825
  //#endregion
10795
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10826
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10796
10827
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10797
10828
  DeviceType["Camera"] = "camera";
10798
10829
  DeviceType["Hub"] = "hub";
@@ -10886,8 +10917,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10886
10917
  DeviceType["PetFeeder"] = "pet-feeder";
10887
10918
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10888
10919
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10889
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10920
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10921
+ * A plug-built dishwasher carries running-state and power instead. */
10890
10922
  DeviceType["Dishwasher"] = "dishwasher";
10923
+ /** Washing machine. A plug-built one is running while its power stays
10924
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10925
+ DeviceType["Washer"] = "washer";
10926
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10927
+ DeviceType["Dryer"] = "dryer";
10891
10928
  return DeviceType;
10892
10929
  }({});
10893
10930
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -11135,7 +11172,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11135
11172
  return DeviceRole;
11136
11173
  }({});
11137
11174
  //#endregion
11138
- //#region ../types/dist/composition-kEl9mevu.mjs
11175
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
11139
11176
  /**
11140
11177
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11141
11178
  *
@@ -11450,6 +11487,13 @@ var table = {
11450
11487
  apply: () => {
11451
11488
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11452
11489
  }
11490
+ },
11491
+ schmitt: {
11492
+ minArgs: 3,
11493
+ maxArgs: 5,
11494
+ apply: () => {
11495
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11496
+ }
11453
11497
  }
11454
11498
  };
11455
11499
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11890,15 +11934,232 @@ function validateExpressionSource(src) {
11890
11934
  }
11891
11935
  return null;
11892
11936
  }
11893
- /** A composed device's stableId is this prefix plus the block id. */
11894
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11895
- /** Longest snippet a `code` source may carry (slice 4). */
11896
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11937
+ /**
11938
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11939
+ *
11940
+ * Standby of the control board is under 5 W; a motor or a heater is far
11941
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11942
+ * a cycle, so running starts only after 30 s above the line.
11943
+ *
11944
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11945
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11946
+ * report "finished" in the middle of the program, so the stop dwell is
11947
+ * 10 minutes on every type. The number is stored per type so one machine
11948
+ * can move later without moving the others.
11949
+ */
11950
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11951
+ var APPLIANCE_KINDS = [
11952
+ "washer",
11953
+ "dryer",
11954
+ "dishwasher"
11955
+ ];
11956
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11957
+ /**
11958
+ * Statistics and integrals over a history read (D944).
11959
+ *
11960
+ * Pure: the same points always give the same answer, so a restart recomputes
11961
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11962
+ * never the mean of the samples. A hole with no held value is not integrated
11963
+ * as zero. A truncated read is not published.
11964
+ */
11965
+ var STATISTIC_STATS = [
11966
+ "min",
11967
+ "max",
11968
+ "mean",
11969
+ "sum",
11970
+ "count",
11971
+ "change"
11972
+ ];
11973
+ /**
11974
+ * The range a statistics or integral helper asks history for (D944).
11975
+ *
11976
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11977
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11978
+ * is strictly longer than the longest that range can be. Forever qualifies.
11979
+ * Equal to a tier's retention is a refusal, not a short answer.
11980
+ */
11981
+ var MINUTE_MS = 6e4;
11982
+ var HOUR_MS = 60 * MINUTE_MS;
11983
+ var DAY_MS$1 = 24 * HOUR_MS;
11984
+ var ROLLING_SPANS = [
11985
+ "1h",
11986
+ "24h",
11987
+ "2d",
11988
+ "7d",
11989
+ "30d",
11990
+ "custom"
11991
+ ];
11992
+ var CALENDAR_PERIODS = [
11993
+ "today",
11994
+ "yesterday",
11995
+ "this-week",
11996
+ "this-month"
11997
+ ];
11998
+ var RESET_PERIODS = [
11999
+ "never",
12000
+ "daily",
12001
+ "weekly",
12002
+ "monthly"
12003
+ ];
12004
+ 5 * MINUTE_MS;
12005
+ 24 * HOUR_MS, 2 * DAY_MS$1, 7 * DAY_MS$1, 30 * DAY_MS$1;
12006
+ 7 * DAY_MS$1, 30 * DAY_MS$1, 365 * DAY_MS$1;
12007
+ var rollingWindowObject = object({
12008
+ kind: literal("rolling"),
12009
+ span: _enum(ROLLING_SPANS),
12010
+ customMinutes: number().int().min(5).optional()
12011
+ }).check((payload) => {
12012
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
12013
+ code: "custom",
12014
+ message: "a custom range needs a duration in minutes",
12015
+ path: ["customMinutes"],
12016
+ input: payload.value
12017
+ });
12018
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
12019
+ code: "custom",
12020
+ message: "only a custom range carries a duration",
12021
+ path: ["customMinutes"],
12022
+ input: payload.value
12023
+ });
12024
+ });
12025
+ var RollingWindowSchema = rollingWindowObject;
12026
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
12027
+ kind: literal("calendar"),
12028
+ period: _enum(CALENDAR_PERIODS)
12029
+ })]);
12030
+ /**
12031
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
12032
+ * id. Split out so helper schemas can name it without a cycle through
12033
+ * `composition.ts` (D659).
12034
+ */
11897
12035
  var CompositionSourceRefSchema = object({
11898
12036
  /** The owning addon; `stableId` is unique only within it. */
11899
12037
  addonId: string().min(1),
11900
12038
  stableId: string().min(1)
11901
12039
  });
12040
+ var GROUP_TEMPLATE_IDS = [
12041
+ "temperature",
12042
+ "humidity",
12043
+ "pressure",
12044
+ "illuminance",
12045
+ "power",
12046
+ "energy",
12047
+ "battery",
12048
+ "numeric",
12049
+ "motion",
12050
+ "contact",
12051
+ "leak",
12052
+ "smoke",
12053
+ "occupancy",
12054
+ "binary",
12055
+ "switch",
12056
+ "light",
12057
+ "fan",
12058
+ "cover",
12059
+ "valve",
12060
+ "lock",
12061
+ "media-player",
12062
+ "climate"
12063
+ ];
12064
+ var NUMERIC_AGGREGATES = [
12065
+ "mean",
12066
+ "median",
12067
+ "min",
12068
+ "max",
12069
+ "sum"
12070
+ ];
12071
+ var BINARY_AGGREGATES = ["any", "all"];
12072
+ /** How an actuator group decides it is on. Unknown members never count as off. */
12073
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
12074
+ var AGGREGATE_REDUCERS = [
12075
+ "mean",
12076
+ "median",
12077
+ "min",
12078
+ "max",
12079
+ "sum",
12080
+ "any",
12081
+ "all"
12082
+ ];
12083
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
12084
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
12085
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
12086
+ /** One member. The field is stored so occupancy can name motion or binary. */
12087
+ var HelperMemberSchema = object({
12088
+ source: CompositionSourceRefSchema,
12089
+ cap: string().min(1),
12090
+ fieldPath: string().min(1)
12091
+ });
12092
+ var HelperSpecSchema = discriminatedUnion("kind", [
12093
+ object({
12094
+ kind: literal("group"),
12095
+ templateId: GroupTemplateIdSchema,
12096
+ aggregate: AggregateReducerSchema,
12097
+ members: array(HelperMemberSchema).min(1).max(64),
12098
+ /** Actuator groups only. Readings leave this unset. */
12099
+ active: ActiveCriterionSchema.optional()
12100
+ }),
12101
+ object({
12102
+ kind: literal("appliance"),
12103
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
12104
+ appliance: _enum(APPLIANCE_KINDS),
12105
+ thresholdWatts: number().positive(),
12106
+ startDwellMs: number().int().nonnegative(),
12107
+ stopDwellMs: number().int().nonnegative(),
12108
+ member: HelperMemberSchema
12109
+ }),
12110
+ object({
12111
+ kind: literal("statistic"),
12112
+ stat: _enum(STATISTIC_STATS),
12113
+ window: StatisticWindowSchema,
12114
+ source: HelperMemberSchema
12115
+ }),
12116
+ object({
12117
+ kind: literal("integral"),
12118
+ reset: _enum(RESET_PERIODS),
12119
+ output: _enum(["energy", "meter"]),
12120
+ source: HelperMemberSchema
12121
+ }),
12122
+ object({
12123
+ kind: literal("threshold"),
12124
+ source: HelperMemberSchema,
12125
+ high: number(),
12126
+ low: number(),
12127
+ onForMs: number().int().nonnegative(),
12128
+ offForMs: number().int().nonnegative()
12129
+ }),
12130
+ object({
12131
+ kind: literal("arithmetic"),
12132
+ op: _enum([
12133
+ "sum",
12134
+ "difference",
12135
+ "ratio"
12136
+ ]),
12137
+ inputs: array(HelperMemberSchema).min(2).max(64)
12138
+ }),
12139
+ object({
12140
+ kind: literal("expression"),
12141
+ expr: string().min(1),
12142
+ inputs: array(object({
12143
+ name: string().min(1),
12144
+ member: HelperMemberSchema
12145
+ })).min(1).max(64),
12146
+ output: _enum(["number", "boolean"])
12147
+ }),
12148
+ object({
12149
+ kind: literal("derivative"),
12150
+ window: RollingWindowSchema,
12151
+ per: _enum([
12152
+ "second",
12153
+ "minute",
12154
+ "hour"
12155
+ ]),
12156
+ source: HelperMemberSchema
12157
+ })
12158
+ ]);
12159
+ /** A composed device's stableId is this prefix plus the block id. */
12160
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12161
+ /** Longest snippet a `code` source may carry (slice 4). */
12162
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11902
12163
  /** One field of one capability of one source device. */
11903
12164
  var CompositionFieldReadSchema = object({
11904
12165
  source: CompositionSourceRefSchema,
@@ -11942,7 +12203,59 @@ var CompositionCodeSourceSchema = object({
11942
12203
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11943
12204
  CompositionFromSourceSchema,
11944
12205
  CompositionExpressionSourceSchema,
11945
- CompositionCodeSourceSchema
12206
+ CompositionCodeSourceSchema,
12207
+ object({
12208
+ kind: literal("aggregate"),
12209
+ reducer: _enum([
12210
+ "mean",
12211
+ "median",
12212
+ "min",
12213
+ "max",
12214
+ "sum",
12215
+ "any",
12216
+ "all"
12217
+ ]),
12218
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12219
+ /**
12220
+ * Set when the reads are an enum state, not a yes/no: the listed states
12221
+ * count as active (a cover is `open`). A state outside the list is inactive;
12222
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
12223
+ */
12224
+ activeWhen: array(string().min(1)).min(1).optional(),
12225
+ /** `value` is the reading; the others are the member counts. */
12226
+ publish: _enum([
12227
+ "value",
12228
+ "included",
12229
+ "excluded",
12230
+ "total"
12231
+ ]),
12232
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12233
+ empty: _enum([
12234
+ "null",
12235
+ "omit",
12236
+ "offline"
12237
+ ])
12238
+ }),
12239
+ object({
12240
+ kind: literal("series"),
12241
+ read: CompositionFieldReadSchema,
12242
+ /** `value` is the statistic or the integral. The others are the gap counts. */
12243
+ publish: _enum([
12244
+ "value",
12245
+ "included",
12246
+ "excluded",
12247
+ "total"
12248
+ ]),
12249
+ /**
12250
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
12251
+ * device unseeded until history answers, so a restart cannot publish 0.
12252
+ */
12253
+ empty: _enum([
12254
+ "null",
12255
+ "omit",
12256
+ "offline"
12257
+ ])
12258
+ })
11946
12259
  ]);
11947
12260
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11948
12261
  kind: literal("forward"),
@@ -11988,7 +12301,13 @@ var CompositionSchema = object({
11988
12301
  kind: literal("existing"),
11989
12302
  device: CompositionSourceRefSchema
11990
12303
  })]),
11991
- features: array(CompositionFeatureSchema).min(1).max(16)
12304
+ features: array(CompositionFeatureSchema).min(1).max(16),
12305
+ /**
12306
+ * Present when the block is a helper template. Absent on a free
12307
+ * composition (the room example, device 4603). The editor reads this,
12308
+ * never the compiled features.
12309
+ */
12310
+ helper: HelperSpecSchema.optional()
11992
12311
  }).superRefine((composition, ctx) => {
11993
12312
  const seen = /* @__PURE__ */ new Set();
11994
12313
  composition.features.forEach((feature, index) => {
@@ -17095,7 +17414,22 @@ var BrokerProviderInfoSchema = object({
17095
17414
  */
17096
17415
  interactiveConnectPath: string().min(1).optional(),
17097
17416
  /** Relative hub path to disconnect the linked account (same popup pattern). */
17098
- interactiveDisconnectPath: string().min(1).optional()
17417
+ interactiveDisconnectPath: string().min(1).optional(),
17418
+ /**
17419
+ * The kind signs in through a server-held interactive session instead of
17420
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17421
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17422
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17423
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17424
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17425
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17426
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17427
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17428
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17429
+ * offers it as the default path; the fields the session fills are
17430
+ * marked `filledBySignIn` in the kind's settings schema.
17431
+ */
17432
+ signIn: object({ basePath: string().min(1) }).optional()
17099
17433
  }))
17100
17434
  });
17101
17435
  var ListInputSchema = object({
@@ -18090,7 +18424,12 @@ var CompositionProblemSchema = object({
18090
18424
  "item-field-reserved",
18091
18425
  "target-missing",
18092
18426
  "target-unreadable",
18093
- "not-replaceable"
18427
+ "not-replaceable",
18428
+ "native-capability-replace",
18429
+ "mixed-quantity",
18430
+ "helper-mismatch",
18431
+ "member-constraint",
18432
+ "retention"
18094
18433
  ]),
18095
18434
  path: string(),
18096
18435
  message: string()
@@ -18111,7 +18450,8 @@ var CompositionUnavailableModeSchema = _enum([
18111
18450
  "null",
18112
18451
  "offline",
18113
18452
  "unknown",
18114
- "release"
18453
+ "release",
18454
+ "omit"
18115
18455
  ]);
18116
18456
  /**
18117
18457
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18148,9 +18488,11 @@ var CompositionFieldPlanSchema = object({
18148
18488
  reads: array(CompositionFieldReadSchema)
18149
18489
  });
18150
18490
  /**
18151
- * What a feature does to its target (D663): `new` builds a capability on a new
18152
- * device; on an existing device it `add`s a capability the device lacks, or
18153
- * `replace`s named fields of one it has natively.
18491
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18492
+ * new device; on an existing device it `add`s a capability the device lacks.
18493
+ * Replacing a capability the device already serves is refused
18494
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18495
+ * wire for a claim the hub already holds.
18154
18496
  */
18155
18497
  var CompositionFeatureModeSchema = _enum([
18156
18498
  "new",
@@ -18321,8 +18663,392 @@ Object.freeze({
18321
18663
  latchedAt: {
18322
18664
  kind: "fixed",
18323
18665
  result: ["number", "null"]
18666
+ },
18667
+ schmitt: {
18668
+ kind: "fixed",
18669
+ result: ["boolean", "null"]
18324
18670
  }
18325
18671
  });
18672
+ /**
18673
+ * One catalog row per base reading a group helper can build (D944 §3).
18674
+ * Data, not a form default written into the block.
18675
+ */
18676
+ var NOW$2 = {
18677
+ fieldPath: "lastFetchedAt",
18678
+ expr: "now"
18679
+ };
18680
+ var CHANGED = {
18681
+ fieldPath: "lastChangedAt",
18682
+ expr: "now"
18683
+ };
18684
+ function numeric(input) {
18685
+ return {
18686
+ ...input,
18687
+ family: "numeric",
18688
+ deviceType: DeviceType.Sensor,
18689
+ memberDeviceType: DeviceType.Sensor,
18690
+ memberDeviceTypes: [DeviceType.Sensor],
18691
+ activeWhen: void 0,
18692
+ commandCaps: [],
18693
+ baseUnit: baseUnitOf(input.quantity),
18694
+ aggregates: NUMERIC_AGGREGATES,
18695
+ accept: [{
18696
+ cap: input.outputCap,
18697
+ fieldPath: input.outputField
18698
+ }]
18699
+ };
18700
+ }
18701
+ function binary(input) {
18702
+ return {
18703
+ ...input,
18704
+ family: "binary",
18705
+ deviceType: DeviceType.Sensor,
18706
+ memberDeviceType: DeviceType.Sensor,
18707
+ memberDeviceTypes: [DeviceType.Sensor],
18708
+ activeWhen: void 0,
18709
+ commandCaps: [],
18710
+ quantity: "none",
18711
+ baseUnit: void 0,
18712
+ aggregates: BINARY_AGGREGATES,
18713
+ empty: "offline"
18714
+ };
18715
+ }
18716
+ var GROUP_TEMPLATES = [
18717
+ numeric({
18718
+ id: "temperature",
18719
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18720
+ role: DeviceRole.TemperatureSensor,
18721
+ outputCap: "temperature-sensor",
18722
+ outputField: "celsius",
18723
+ quantity: "temperature",
18724
+ defaultAggregate: "mean",
18725
+ empty: "null",
18726
+ companions: [NOW$2]
18727
+ }),
18728
+ numeric({
18729
+ id: "humidity",
18730
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18731
+ role: DeviceRole.HumiditySensor,
18732
+ outputCap: "humidity-sensor",
18733
+ outputField: "percent",
18734
+ quantity: "humidity",
18735
+ defaultAggregate: "mean",
18736
+ empty: "null",
18737
+ companions: [NOW$2]
18738
+ }),
18739
+ numeric({
18740
+ id: "pressure",
18741
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18742
+ role: DeviceRole.PressureSensor,
18743
+ outputCap: "pressure-sensor",
18744
+ outputField: "hpa",
18745
+ quantity: "pressure",
18746
+ defaultAggregate: "mean",
18747
+ empty: "null",
18748
+ companions: [NOW$2]
18749
+ }),
18750
+ numeric({
18751
+ id: "illuminance",
18752
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18753
+ role: DeviceRole.AmbientLightSensor,
18754
+ outputCap: "ambient-light-sensor",
18755
+ outputField: "lux",
18756
+ quantity: "illuminance",
18757
+ defaultAggregate: "mean",
18758
+ empty: "null",
18759
+ companions: [NOW$2]
18760
+ }),
18761
+ numeric({
18762
+ id: "power",
18763
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18764
+ role: DeviceRole.PowerSensor,
18765
+ outputCap: "power-meter",
18766
+ outputField: "watts",
18767
+ quantity: "power",
18768
+ defaultAggregate: "sum",
18769
+ empty: "omit",
18770
+ companions: [NOW$2]
18771
+ }),
18772
+ numeric({
18773
+ id: "energy",
18774
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18775
+ role: DeviceRole.EnergySensor,
18776
+ outputCap: "power-meter",
18777
+ outputField: "kwhTotal",
18778
+ quantity: "energy",
18779
+ defaultAggregate: "sum",
18780
+ empty: "omit",
18781
+ companions: [NOW$2]
18782
+ }),
18783
+ numeric({
18784
+ id: "battery",
18785
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18786
+ role: DeviceRole.BatterySensor,
18787
+ outputCap: "battery",
18788
+ outputField: "percentage",
18789
+ quantity: "percentage",
18790
+ defaultAggregate: "mean",
18791
+ empty: "null",
18792
+ companions: [
18793
+ {
18794
+ fieldPath: "charging",
18795
+ expr: "\"unknown\""
18796
+ },
18797
+ {
18798
+ fieldPath: "sleeping",
18799
+ expr: "false"
18800
+ },
18801
+ {
18802
+ fieldPath: "lastUpdated",
18803
+ expr: "now"
18804
+ }
18805
+ ]
18806
+ }),
18807
+ numeric({
18808
+ id: "numeric",
18809
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18810
+ role: DeviceRole.NumericSensor,
18811
+ outputCap: "numeric-sensor",
18812
+ outputField: "value",
18813
+ quantity: "device",
18814
+ defaultAggregate: "mean",
18815
+ empty: "null",
18816
+ companions: [NOW$2]
18817
+ }),
18818
+ binary({
18819
+ id: "motion",
18820
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18821
+ role: DeviceRole.MotionSensor,
18822
+ outputCap: "motion",
18823
+ outputField: "detected",
18824
+ defaultAggregate: "any",
18825
+ accept: [{
18826
+ cap: "motion",
18827
+ fieldPath: "detected"
18828
+ }],
18829
+ companions: [{
18830
+ fieldPath: "lastDetectedAt",
18831
+ expr: "null"
18832
+ }, {
18833
+ fieldPath: "autoClearAfterMs",
18834
+ expr: "null"
18835
+ }]
18836
+ }),
18837
+ binary({
18838
+ id: "contact",
18839
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18840
+ role: DeviceRole.ContactSensor,
18841
+ outputCap: "contact",
18842
+ outputField: "entryOpen",
18843
+ defaultAggregate: "any",
18844
+ accept: [{
18845
+ cap: "contact",
18846
+ fieldPath: "entryOpen"
18847
+ }],
18848
+ companions: [CHANGED]
18849
+ }),
18850
+ binary({
18851
+ id: "leak",
18852
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18853
+ role: DeviceRole.LeakSensor,
18854
+ outputCap: "flood",
18855
+ outputField: "flooded",
18856
+ defaultAggregate: "any",
18857
+ accept: [{
18858
+ cap: "flood",
18859
+ fieldPath: "flooded"
18860
+ }],
18861
+ companions: [CHANGED]
18862
+ }),
18863
+ binary({
18864
+ id: "smoke",
18865
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18866
+ role: DeviceRole.SmokeSensor,
18867
+ outputCap: "smoke",
18868
+ outputField: "detected",
18869
+ defaultAggregate: "any",
18870
+ accept: [{
18871
+ cap: "smoke",
18872
+ fieldPath: "detected"
18873
+ }],
18874
+ companions: [CHANGED]
18875
+ }),
18876
+ binary({
18877
+ id: "occupancy",
18878
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18879
+ role: DeviceRole.BinarySensor,
18880
+ outputCap: "binary",
18881
+ outputField: "on",
18882
+ defaultAggregate: "any",
18883
+ accept: [{
18884
+ cap: "motion",
18885
+ fieldPath: "detected"
18886
+ }, {
18887
+ cap: "binary",
18888
+ fieldPath: "on"
18889
+ }],
18890
+ companions: [CHANGED]
18891
+ }),
18892
+ binary({
18893
+ id: "binary",
18894
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18895
+ role: DeviceRole.BinarySensor,
18896
+ outputCap: "binary",
18897
+ outputField: "on",
18898
+ defaultAggregate: "any",
18899
+ accept: [{
18900
+ cap: "binary",
18901
+ fieldPath: "on"
18902
+ }],
18903
+ companions: [CHANGED]
18904
+ }),
18905
+ actuator({
18906
+ id: "switch",
18907
+ 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.",
18908
+ deviceType: DeviceType.Switch,
18909
+ outputCap: "switch",
18910
+ outputField: "on",
18911
+ member: {
18912
+ cap: "switch",
18913
+ fieldPath: "on"
18914
+ },
18915
+ commandCaps: ["switch"]
18916
+ }),
18917
+ actuator({
18918
+ id: "light",
18919
+ 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.",
18920
+ deviceType: DeviceType.Light,
18921
+ outputCap: "switch",
18922
+ outputField: "on",
18923
+ member: {
18924
+ cap: "switch",
18925
+ fieldPath: "on"
18926
+ },
18927
+ commandCaps: [
18928
+ "switch",
18929
+ "brightness",
18930
+ "color"
18931
+ ]
18932
+ }),
18933
+ actuator({
18934
+ id: "fan",
18935
+ 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.",
18936
+ deviceType: DeviceType.Fan,
18937
+ outputCap: "switch",
18938
+ outputField: "on",
18939
+ member: {
18940
+ cap: "switch",
18941
+ fieldPath: "on"
18942
+ },
18943
+ commandCaps: ["switch", "fan-control"]
18944
+ }),
18945
+ actuator({
18946
+ id: "cover",
18947
+ 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.",
18948
+ deviceType: DeviceType.Cover,
18949
+ outputCap: "group-active",
18950
+ outputField: "active",
18951
+ member: {
18952
+ cap: "cover",
18953
+ fieldPath: "state"
18954
+ },
18955
+ activeWhen: [
18956
+ "open",
18957
+ "opening",
18958
+ "stopped"
18959
+ ],
18960
+ commandCaps: ["cover"]
18961
+ }),
18962
+ actuator({
18963
+ id: "valve",
18964
+ 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.",
18965
+ deviceType: DeviceType.Valve,
18966
+ outputCap: "group-active",
18967
+ outputField: "active",
18968
+ member: {
18969
+ cap: "valve",
18970
+ fieldPath: "state"
18971
+ },
18972
+ activeWhen: [
18973
+ "open",
18974
+ "opening",
18975
+ "stopped"
18976
+ ],
18977
+ commandCaps: ["valve"]
18978
+ }),
18979
+ actuator({
18980
+ id: "lock",
18981
+ 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.",
18982
+ deviceType: DeviceType.Lock,
18983
+ outputCap: "group-active",
18984
+ outputField: "active",
18985
+ member: {
18986
+ cap: "lock-control",
18987
+ fieldPath: "state"
18988
+ },
18989
+ activeWhen: ["locked"],
18990
+ commandCaps: ["lock-control"]
18991
+ }),
18992
+ actuator({
18993
+ id: "media-player",
18994
+ 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.",
18995
+ deviceType: DeviceType.MediaPlayer,
18996
+ outputCap: "group-active",
18997
+ outputField: "active",
18998
+ member: {
18999
+ cap: "media-player",
19000
+ fieldPath: "state"
19001
+ },
19002
+ activeWhen: ["playing", "buffering"],
19003
+ commandCaps: ["media-player"]
19004
+ }),
19005
+ actuator({
19006
+ id: "climate",
19007
+ 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.",
19008
+ deviceType: DeviceType.Thermostat,
19009
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
19010
+ outputCap: "group-active",
19011
+ outputField: "active",
19012
+ member: {
19013
+ cap: "climate-control",
19014
+ fieldPath: "mode"
19015
+ },
19016
+ activeWhen: [
19017
+ "heat",
19018
+ "cool",
19019
+ "auto",
19020
+ "heat_cool",
19021
+ "fan_only",
19022
+ "dry"
19023
+ ],
19024
+ commandCaps: ["climate-control"]
19025
+ })
19026
+ ];
19027
+ function actuator(input) {
19028
+ return {
19029
+ id: input.id,
19030
+ summary: input.summary,
19031
+ deviceType: input.deviceType,
19032
+ outputCap: input.outputCap,
19033
+ outputField: input.outputField,
19034
+ commandCaps: input.commandCaps,
19035
+ family: "actuator",
19036
+ role: void 0,
19037
+ memberDeviceType: input.deviceType,
19038
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
19039
+ activeWhen: input.activeWhen,
19040
+ quantity: "none",
19041
+ baseUnit: void 0,
19042
+ aggregates: BINARY_AGGREGATES,
19043
+ defaultAggregate: "any",
19044
+ empty: "offline",
19045
+ accept: [input.member],
19046
+ companions: [CHANGED]
19047
+ };
19048
+ }
19049
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19050
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19051
+ DeviceType.Camera;
18326
19052
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18327
19053
  /**
18328
19054
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20036,12 +20762,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20036
20762
  * providers detect renames by matching `uniqueId` and patch `id`
20037
20763
  * in-place so dispatch keeps working.
20038
20764
  *
20039
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20040
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20041
- * state push and are the single source of truth. The `device_class` tag is
20042
- * an HA-specific input that is consumed at the discovery boundary
20043
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20044
- * stored after that point.
20765
+ * **Display unit and precision are NOT stored here.** They are a read-time
20766
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20767
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20768
+ * neither is repeated on each sample. The `device_class` tag is an
20769
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20770
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20045
20771
  *
20046
20772
  * The `raw` escape hatch carries provider-specific attributes that don't
20047
20773
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20055,6 +20781,14 @@ var SourceInfoSchema = object({
20055
20781
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20056
20782
  * Used to detect rename when `id` changes. */
20057
20783
  uniqueId: string().optional(),
20784
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20785
+ quantity: ReadingQuantitySchema.optional(),
20786
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20787
+ dateTimeShape: _enum([
20788
+ "date",
20789
+ "time",
20790
+ "datetime"
20791
+ ]).optional(),
20058
20792
  /** Provider-specific extras that don't fit the structured slots.
20059
20793
  * Whitelist what goes here per provider — do NOT dump entire HA
20060
20794
  * attribute blobs (would explode the meta JSON column). */
@@ -40967,7 +41701,8 @@ method(object({
40967
41701
  "enum"
40968
41702
  ]),
40969
41703
  enumValues: array(string()).optional(),
40970
- item: boolean().optional()
41704
+ item: boolean().optional(),
41705
+ quantity: ReadingQuantitySchema.optional()
40971
41706
  })).readonly(),
40972
41707
  itemArray: object({
40973
41708
  path: string(),
@@ -43328,7 +44063,7 @@ var NetworkAccessStatusSchema = object({
43328
44063
  });
43329
44064
  /**
43330
44065
  * Optional, richer endpoint shape returned by providers that expose
43331
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44066
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43332
44067
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43333
44068
  * the originating provider config (mode + sourcePort) so the
43334
44069
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44767,8 +45502,8 @@ var MaskPolygonShapeSchema = object({
44767
45502
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44768
45503
  var MaskGridShapeSchema = object({
44769
45504
  kind: literal("grid"),
44770
- gridWidth: number(),
44771
- gridHeight: number(),
45505
+ gridWidth: number().meta(readingMeta("none")),
45506
+ gridHeight: number().meta(readingMeta("none")),
44772
45507
  cells: array(boolean())
44773
45508
  });
44774
45509
  discriminatedUnion("kind", [
@@ -44880,6 +45615,8 @@ var NcSystemEventKindSchema = _enum([
44880
45615
  "detection-blind",
44881
45616
  "motion-noisy",
44882
45617
  "motion-calm",
45618
+ "package-llm-silent",
45619
+ "package-llm-answered",
44883
45620
  "recording-stalled",
44884
45621
  "recording-healthy",
44885
45622
  "recording-degraded",
@@ -45969,7 +46706,20 @@ var NcRuleInputSchema = object({
45969
46706
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
45970
46707
  * path does not run Zod defaults.
45971
46708
  */
45972
- groupingSec: number().int().min(5).max(60).optional()
46709
+ groupingSec: number().int().min(5).max(60).optional(),
46710
+ /**
46711
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46712
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46713
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46714
+ * same mark the Library writes, and records whose notification it was and
46715
+ * when. A channel with no buttons gets the same two choices as links.
46716
+ *
46717
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46718
+ * cap path): absent and `false` both mean off, which is what every rule
46719
+ * authored before this field meant. An editor turns it off by writing
46720
+ * `false`, so it needs no `clear` key.
46721
+ */
46722
+ askVerdict: boolean().optional()
45973
46723
  });
45974
46724
  /**
45975
46725
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -55894,6 +56644,28 @@ var PackageVersionInfoSchema = object({
55894
56644
  deprecated: string().optional(),
55895
56645
  distTags: array(string()).readonly()
55896
56646
  });
56647
+ /**
56648
+ * One published document. `ok` is markdown from the registry. `missing` means
56649
+ * the registry answered and the file was not there. `error` is a registry or
56650
+ * tarball failure — never an empty string standing in for either.
56651
+ */
56652
+ var PackageDocBodySchema = discriminatedUnion("status", [
56653
+ object({
56654
+ status: literal("ok"),
56655
+ markdown: string()
56656
+ }),
56657
+ object({ status: literal("missing") }),
56658
+ object({
56659
+ status: literal("error"),
56660
+ message: string()
56661
+ })
56662
+ ]);
56663
+ var PackageDocsSchema = object({
56664
+ name: string(),
56665
+ version: string(),
56666
+ readme: PackageDocBodySchema,
56667
+ changelog: PackageDocBodySchema
56668
+ });
55897
56669
  var SearchResultSchema = object({
55898
56670
  name: string(),
55899
56671
  version: string(),
@@ -56112,7 +56884,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56112
56884
  }), object({ success: literal(true) }), {
56113
56885
  kind: "mutation",
56114
56886
  auth: "admin"
56115
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
56887
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
56888
+ name: string().min(1),
56889
+ version: string().min(1).optional()
56890
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56116
56891
  kind: "mutation",
56117
56892
  auth: "admin"
56118
56893
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56162,46 +56937,27 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56162
56937
  }), LogStreamEntrySchema, { kind: "subscription" });
56163
56938
  object({
56164
56939
  /** Carbon dioxide concentration in ppm. */
56165
- co2Ppm: number().min(0).optional(),
56940
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56166
56941
  /** Total volatile organic compounds in ppb. */
56167
- vocPpb: number().min(0).optional(),
56942
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56168
56943
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56169
- pm25: number().min(0).optional(),
56944
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56170
56945
  /** Particulate matter ≤ 10 μm in µg/m³. */
56171
- pm10: number().min(0).optional(),
56946
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56172
56947
  /** Composite AQI value (typically 0..500). */
56173
- aqi: number().optional(),
56948
+ aqi: number().optional().meta(readingMeta("index")),
56174
56949
  /** Ms epoch when the slice was last updated. */
56175
- lastFetchedAt: number(),
56176
- /** Live display unit of the single metric this slice carries (e.g. HA
56177
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56178
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56179
- * per slice is unambiguous. */
56180
- unit: string().optional(),
56181
- /** Suggested decimal places for numeric display.
56182
- * Populated live from the upstream source when provided (e.g. HA
56183
- * `attributes.suggested_display_precision`). Falls back to
56184
- * auto-formatting when absent. */
56185
- precision: number().int().min(0).max(10).optional()
56950
+ lastFetchedAt: number()
56186
56951
  });
56187
56952
  DeviceType.Sensor;
56188
56953
  object({
56189
- /** Current illuminance in lux (lx). */
56190
- lux: number().min(0).meta({
56191
- unit: "lx",
56954
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56955
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56956
+ precision: 0,
56192
56957
  label: "Illuminance"
56193
- }),
56958
+ })),
56194
56959
  /** Ms epoch when the slice was last updated. */
56195
- lastFetchedAt: number(),
56196
- /** Live display unit from the upstream source (e.g. HA
56197
- * `attributes.unit_of_measurement`). The UI prefers this over the
56198
- * role's canonical unit. Absent → fall back to the canonical unit. */
56199
- unit: string().optional(),
56200
- /** Suggested decimal places for numeric display.
56201
- * Populated live from the upstream source when provided (e.g. HA
56202
- * `attributes.suggested_display_precision`). Falls back to
56203
- * auto-formatting when absent. */
56204
- precision: number().int().min(0).max(10).optional()
56960
+ lastFetchedAt: number()
56205
56961
  });
56206
56962
  DeviceType.Sensor;
56207
56963
  /**
@@ -56346,10 +57102,7 @@ var BatteryStatusSchema = object({
56346
57102
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56347
57103
  * declines to band a non-finite reading, which is the correct shape.
56348
57104
  */
56349
- percentage: number().min(0).max(100).nullable().meta({
56350
- unit: "%",
56351
- label: "Charge"
56352
- }),
57105
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56353
57106
  /**
56354
57107
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56355
57108
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56376,7 +57129,14 @@ var BatteryStatusSchema = object({
56376
57129
  */
56377
57130
  sleeping: boolean().meta({ label: "Sleeping" }),
56378
57131
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56379
- lastUpdated: number(),
57132
+ lastUpdated: number().meta(readingMeta("none")),
57133
+ /**
57134
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57135
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57136
+ * that only report charge. A second reading on this cap, not a second
57137
+ * `numeric-sensor` on the same device.
57138
+ */
57139
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56380
57140
  /**
56381
57141
  * Ms epoch of the last time the device PROVED it was reachable — a
56382
57142
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56416,7 +57176,7 @@ var BatteryStatusSchema = object({
56416
57176
  */
56417
57177
  binary: boolean().optional().meta({ label: "Binary indicator" })
56418
57178
  });
56419
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat;
57179
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter;
56420
57180
  method(object({
56421
57181
  deviceId: number(),
56422
57182
  /** Bound on the wait. Sensible range 3000–10000ms. */
@@ -56436,7 +57196,7 @@ object({
56436
57196
  DeviceType.Sensor;
56437
57197
  object({
56438
57198
  /** Current level as 0..100 inclusive. Firmware-reported. */
56439
- percentage: number().min(0).max(100),
57199
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56440
57200
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56441
57201
  lastChangedAt: number()
56442
57202
  });
@@ -56831,21 +57591,21 @@ object({
56831
57591
  /** Single setpoint in Celsius. Used by single-target modes
56832
57592
  * (heat / cool / dry). Null when the device is in a range mode
56833
57593
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
56834
- target: number().nullable(),
57594
+ target: number().nullable().meta(readingMeta("temperature")),
56835
57595
  /** Upper bound of the dual setpoint in Celsius. Populated only
56836
57596
  * in `heat_cool` mode. */
56837
- targetHigh: number().nullable(),
57597
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
56838
57598
  /** Lower bound of the dual setpoint in Celsius. Populated only
56839
57599
  * in `heat_cool` mode. */
56840
- targetLow: number().nullable(),
57600
+ targetLow: number().nullable().meta(readingMeta("temperature")),
56841
57601
  /** Target relative humidity (0..100). Null when the device has no
56842
57602
  * humidity control. */
56843
- targetHumidity: number().min(0).max(100).nullable(),
57603
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56844
57604
  /** Current measured temperature in Celsius. Read-only — pushed by
56845
57605
  * the device's internal sensor. */
56846
- currentTemp: number().nullable(),
57606
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
56847
57607
  /** Current measured relative humidity (0..100). Read-only. */
56848
- currentHumidity: number().min(0).max(100).nullable(),
57608
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56849
57609
  /** Vertical louver swing on/off. Null when the device has no vertical
56850
57610
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
56851
57611
  swingVertical: boolean().nullable(),
@@ -56853,11 +57613,11 @@ object({
56853
57613
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
56854
57614
  swingHorizontal: boolean().nullable(),
56855
57615
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
56856
- minTemp: number().nullable().optional(),
57616
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
56857
57617
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
56858
- maxTemp: number().nullable().optional(),
57618
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
56859
57619
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
56860
- tempStep: number().positive().nullable().optional(),
57620
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
56861
57621
  /** Ms epoch when the slice was last updated (push or command). */
56862
57622
  lastFetchedAt: number()
56863
57623
  });
@@ -56936,17 +57696,17 @@ DeviceType.Thermostat, DeviceType.Climate, method(object({
56936
57696
  * modes to expose in the picker without a slice fetch.
56937
57697
  */
56938
57698
  var RgbTripletSchema = object({
56939
- r: number().int().min(0).max(255),
56940
- g: number().int().min(0).max(255),
56941
- b: number().int().min(0).max(255)
57699
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57700
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57701
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
56942
57702
  });
56943
57703
  var HsvTripletSchema = object({
56944
57704
  /** Hue in degrees, 0..360. */
56945
- h: number().min(0).max(360),
57705
+ h: number().min(0).max(360).meta(readingMeta("none")),
56946
57706
  /** Saturation as 0..100 inclusive. */
56947
- s: number().min(0).max(100),
57707
+ s: number().min(0).max(100).meta(readingMeta("none")),
56948
57708
  /** Value/brightness as 0..100 inclusive. */
56949
- v: number().min(0).max(100)
57709
+ v: number().min(0).max(100).meta(readingMeta("none"))
56950
57710
  });
56951
57711
  var ColorInputSchema = discriminatedUnion("mode", [
56952
57712
  object({
@@ -56975,7 +57735,7 @@ object({
56975
57735
  /** Populated when `mode === 'hsv'`. */
56976
57736
  hsv: HsvTripletSchema.optional(),
56977
57737
  /** Populated when `mode === 'mired'`. */
56978
- mireds: number().int().optional(),
57738
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
56979
57739
  /** Ms epoch of the last operator-driven change. */
56980
57740
  lastChangedAt: number()
56981
57741
  });
@@ -57099,18 +57859,12 @@ _enum([
57099
57859
  "empty",
57100
57860
  "full"
57101
57861
  ]);
57102
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57103
- * each. */
57104
- var ConsumableRemainingUnitSchema = _enum([
57105
- "days",
57106
- "hours",
57107
- "cycles"
57108
- ]);
57109
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57110
- * days). `value` null = unknown (D393). */
57862
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57863
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57864
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57111
57865
  var ConsumableRemainingSchema = object({
57112
- value: number().min(0).nullable(),
57113
- unit: ConsumableRemainingUnitSchema
57866
+ value: number().min(0).nullable().meta(readingMeta("device")),
57867
+ kind: ConsumableRemainingKindSchema
57114
57868
  });
57115
57869
  var ConsumablesStatusSchema = object({
57116
57870
  items: array(object({
@@ -57119,7 +57873,7 @@ var ConsumablesStatusSchema = object({
57119
57873
  /** Display name. */
57120
57874
  label: string().min(1),
57121
57875
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57122
- level: number().min(0).max(100).nullable(),
57876
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57123
57877
  /** The provider's discrete verdict when known (binary mode). */
57124
57878
  status: ConsumableStatusSchema.nullable(),
57125
57879
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57136,7 +57890,7 @@ var ConsumablesStatusSchema = object({
57136
57890
  })),
57137
57891
  lastChangedAt: number()
57138
57892
  });
57139
- Object.values(DeviceType), [...ConsumableRemainingUnitSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57893
+ Object.values(DeviceType), [...ConsumableRemainingKindSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57140
57894
  Object.values(DeviceType), method(object({
57141
57895
  deviceId: number().int().nonnegative(),
57142
57896
  key: string().min(1)
@@ -57161,39 +57915,17 @@ object({
57161
57915
  /** Current value. Numeric kind → number; select/text/datetime → string.
57162
57916
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
57163
57917
  * or time-only — HA accepts all three forms). */
57164
- value: union([number(), string()]),
57918
+ value: union([number().meta(readingMeta("device")), string()]),
57165
57919
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
57166
57920
  options: array(string()),
57167
57921
  /** Ms epoch when the slice was last updated. */
57168
57922
  lastChangedAt: number(),
57169
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
57170
- * Populated live from HA `attributes.unit_of_measurement` on each push.
57171
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57172
- unit: string().optional(),
57173
- /** Minimum allowed value. Populated live from HA `attributes.min`.
57174
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57175
- min: number().optional(),
57176
- /** Maximum allowed value. Populated live from HA `attributes.max`.
57177
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57178
- max: number().optional(),
57179
- /** Step increment. Populated live from HA `attributes.step`.
57180
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57181
- step: number().optional(),
57182
- /** Suggested decimal places for numeric display. Populated live from HA
57183
- * `attributes.suggested_display_precision`, or derived from `step` when the
57184
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
57185
- * other kinds. Falls back to auto-formatting when absent. */
57186
- precision: number().int().min(0).max(10).optional(),
57187
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
57188
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
57189
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
57190
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
57191
- * `has_date`/`has_time` attributes on each push. */
57192
- format: _enum([
57193
- "date",
57194
- "time",
57195
- "datetime"
57196
- ]).optional()
57923
+ /** Minimum allowed value, in the device quantity's base unit. */
57924
+ min: number().optional().meta(readingMeta("device")),
57925
+ /** Maximum allowed value, in the device quantity's base unit. */
57926
+ max: number().optional().meta(readingMeta("device")),
57927
+ /** Step increment, in the device quantity's base unit. */
57928
+ step: number().optional().meta(readingMeta("device"))
57197
57929
  });
57198
57930
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
57199
57931
  object({
@@ -57231,9 +57963,9 @@ object({
57231
57963
  ]),
57232
57964
  /** 0 = fully closed, 100 = fully open. Null when the device has
57233
57965
  * no intermediate position surface. */
57234
- position: number().min(0).max(100).nullable(),
57966
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57235
57967
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
57236
- tiltPosition: number().min(0).max(100).nullable(),
57968
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57237
57969
  /** Ms epoch when the slice was last updated. */
57238
57970
  lastChangedAt: number()
57239
57971
  });
@@ -57291,9 +58023,9 @@ var NormalizedRangeSchema$1 = object({
57291
58023
  object({
57292
58024
  mode: DayNightModeSchema,
57293
58025
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
57294
- sensitivity: number().optional(),
58026
+ sensitivity: number().optional().meta(readingMeta("none")),
57295
58027
  /** Delay before the IR-cut filter flips, in seconds. */
57296
- switchDelaySec: number().optional(),
58028
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
57297
58029
  lastFetchedAt: number()
57298
58030
  });
57299
58031
  /**
@@ -57448,11 +58180,11 @@ object({
57448
58180
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
57449
58181
  availablePrograms: array(string()),
57450
58182
  /** Seconds remaining on the active cycle, or null when not running / not known. */
57451
- remainingSeconds: number().int().nonnegative().nullable(),
58183
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57452
58184
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
57453
- delayedStartSeconds: number().int().nonnegative().nullable(),
58185
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57454
58186
  /** Cycle completion 0..100, or null when not applicable. */
57455
- progressPercent: number().min(0).max(100).nullable(),
58187
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57456
58188
  /** True when the door is reported open; null when the door state is not known. */
57457
58189
  doorOpen: boolean().nullable(),
57458
58190
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -57476,7 +58208,7 @@ object({
57476
58208
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57477
58209
  machineCareDue: boolean().nullable(),
57478
58210
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57479
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
58211
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
57480
58212
  /** Option states: null when the appliance does not expose the option or has not said. */
57481
58213
  halfLoad: boolean().nullable(),
57482
58214
  hygienePlus: boolean().nullable(),
@@ -57491,7 +58223,7 @@ object({
57491
58223
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
57492
58224
  activeEvents: array(string()),
57493
58225
  /** Numeric error code when the appliance reports one; null otherwise. */
57494
- errorCode: number().nullable(),
58226
+ errorCode: number().nullable().meta(readingMeta("none")),
57495
58227
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
57496
58228
  errorLabel: string().nullable(),
57497
58229
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -57637,24 +58369,14 @@ object({
57637
58369
  /** Ms epoch of the last press. null = never observed since this provider started. */
57638
58370
  lastPressedAt: number().nullable(),
57639
58371
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
57640
- pressCountSinceStart: number()
58372
+ pressCountSinceStart: number().meta(readingMeta("count"))
57641
58373
  });
57642
58374
  object({
57643
58375
  deviceId: number(),
57644
58376
  timestamp: number()
57645
58377
  });
57646
58378
  DeviceType.Button, DeviceType.Camera;
57647
- /**
57648
- * Enum-state sensor — a string value picked from a finite option set.
57649
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
57650
- * states, weather conditions, contact-source channels, mode strings).
57651
- * The option set is stable across observations; it is persisted in the
57652
- * device config blob (via the control slice at adoption) and is NOT
57653
- * stored in `sourceInfo`.
57654
- */
57655
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
57656
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
57657
- var EnumSensorDateTimeFormatSchema = _enum([
58379
+ _enum([
57658
58380
  "date",
57659
58381
  "time",
57660
58382
  "datetime"
@@ -57662,12 +58384,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
57662
58384
  object({
57663
58385
  value: string(),
57664
58386
  /**
57665
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
57666
- * as a locale date/time/datetime; absent for plain enum sensors. HA
57667
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
57668
- * a time-only sensor → `'time'`.
58387
+ * Set when `value` is a timestamp rather than an enum state, so history
58388
+ * does not learn ISO strings as states. The display shape (date / time /
58389
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
57669
58390
  */
57670
- format: EnumSensorDateTimeFormatSchema.optional(),
58391
+ timestamp: boolean().optional(),
57671
58392
  /** Ms epoch when the slice was last updated. */
57672
58393
  lastFetchedAt: number()
57673
58394
  });
@@ -57686,13 +58407,13 @@ var EventFireSchema = object({
57686
58407
  deviceId: number(),
57687
58408
  eventType: string(),
57688
58409
  data: record(string(), unknown()).nullable(),
57689
- timestamp: number(),
57690
- seq: number()
58410
+ timestamp: number().meta(readingMeta("none")),
58411
+ seq: number().meta(readingMeta("count"))
57691
58412
  });
57692
58413
  object({
57693
58414
  eventTypes: array(string()),
57694
58415
  lastEvent: EventFireSchema.nullable(),
57695
- eventCountSinceStart: number()
58416
+ eventCountSinceStart: number().meta(readingMeta("count"))
57696
58417
  });
57697
58418
  DeviceType.EventEmitter;
57698
58419
  var IdentitySchema = object({
@@ -58081,10 +58802,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
58081
58802
  object({
58082
58803
  /** Active speed as 0..100 inclusive. Null when the device has no
58083
58804
  * speed surface (single-speed fan). */
58084
- percentage: number().min(0).max(100).nullable(),
58805
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58085
58806
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
58086
58807
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
58087
- percentageStep: number().positive().optional(),
58808
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
58088
58809
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
58089
58810
  * — empty string when no preset is active or supported. */
58090
58811
  preset: string(),
@@ -58166,7 +58887,7 @@ var FeatureProbeStatusSchema = object({
58166
58887
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
58167
58888
  model: string().nullable(),
58168
58889
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
58169
- channelCount: number().nullable(),
58890
+ channelCount: number().nullable().meta(readingMeta("count")),
58170
58891
  /**
58171
58892
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
58172
58893
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -58198,9 +58919,45 @@ object({
58198
58919
  lastChangedAt: number()
58199
58920
  });
58200
58921
  DeviceType.Sensor;
58922
+ object({
58923
+ /** One row per member that has been commanded. A later command replaces that member's row. */
58924
+ results: array(object({
58925
+ deviceId: number().int().nonnegative(),
58926
+ cap: string().min(1),
58927
+ method: string().min(1),
58928
+ ok: boolean(),
58929
+ error: string().optional(),
58930
+ /** Ms epoch of the attempt. */
58931
+ at: number()
58932
+ })) });
58933
+ DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
58934
+ object({
58935
+ active: boolean().meta({ label: "Active" }),
58936
+ /** Ms epoch when the slice was last updated. */
58937
+ lastChangedAt: number()
58938
+ });
58939
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Climate, DeviceType.Thermostat;
58940
+ object({
58941
+ /** Members whose current value entered the aggregate. */
58942
+ included: number().int().nonnegative().meta(readingMeta("count", {
58943
+ precision: 0,
58944
+ label: "Reporting"
58945
+ })),
58946
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
58947
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
58948
+ precision: 0,
58949
+ label: "Excluded"
58950
+ })),
58951
+ /** `included` + `excluded`. */
58952
+ total: number().int().nonnegative().meta(readingMeta("count", {
58953
+ precision: 0,
58954
+ label: "Members"
58955
+ }))
58956
+ });
58957
+ DeviceType.Sensor, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
58201
58958
  object({
58202
58959
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
58203
- position: number().min(0).max(100).nullable(),
58960
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58204
58961
  /** Ms epoch when the slice was last updated (push or poll). */
58205
58962
  lastFetchedAt: number()
58206
58963
  });
@@ -58209,9 +58966,9 @@ object({
58209
58966
  /** Whether the humidifier is currently on. */
58210
58967
  on: boolean(),
58211
58968
  /** Current measured relative humidity (0..100). Null when not reported. */
58212
- currentHumidity: number().min(0).max(100).nullable(),
58969
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58213
58970
  /** Target relative humidity (0..100). Null when no setpoint surface. */
58214
- targetHumidity: number().min(0).max(100).nullable(),
58971
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58215
58972
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
58216
58973
  * device has no mode surface. */
58217
58974
  mode: string().nullable(),
@@ -58221,9 +58978,9 @@ object({
58221
58978
  * `idle` / `off`). Null when the device doesn't report it. */
58222
58979
  action: string().nullable(),
58223
58980
  /** HA `min_humidity` attribute. Null → UI uses 0. */
58224
- minHumidity: number().nullable(),
58981
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
58225
58982
  /** HA `max_humidity` attribute. Null → UI uses 100. */
58226
- maxHumidity: number().nullable(),
58983
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
58227
58984
  /** Ms epoch when the slice was last updated. */
58228
58985
  lastChangedAt: number()
58229
58986
  });
@@ -58247,22 +59004,13 @@ DeviceType.Humidifier, method(object({
58247
59004
  auth: "protected"
58248
59005
  });
58249
59006
  object({
58250
- /** Current relative humidity, 0..100. */
58251
- percent: number().min(0).max(100).meta({
58252
- unit: "%",
59007
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
59008
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
59009
+ precision: 0,
58253
59010
  label: "Humidity"
58254
- }),
59011
+ })),
58255
59012
  /** Ms epoch when the slice was last updated. */
58256
- lastFetchedAt: number(),
58257
- /** Live display unit from the upstream source (e.g. HA
58258
- * `attributes.unit_of_measurement`). The UI prefers this over the
58259
- * role's canonical unit. Absent → fall back to the canonical unit. */
58260
- unit: string().optional(),
58261
- /** Suggested decimal places for numeric display.
58262
- * Populated live from the upstream source when provided (e.g. HA
58263
- * `attributes.suggested_display_precision`). Falls back to
58264
- * auto-formatting when absent. */
58265
- precision: number().int().min(0).max(10).optional()
59013
+ lastFetchedAt: number()
58266
59014
  });
58267
59015
  DeviceType.Sensor, DeviceType.Thermostat;
58268
59016
  object({
@@ -58270,7 +59018,7 @@ object({
58270
59018
  * entity exposes no `entity_picture` (yet). */
58271
59019
  url: string().nullable(),
58272
59020
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
58273
- lastUpdated: number().nullable()
59021
+ lastUpdated: number().nullable().meta(readingMeta("none"))
58274
59022
  });
58275
59023
  DeviceType.Image;
58276
59024
  /**
@@ -58326,19 +59074,19 @@ var NormalizedRangeSchema = object({
58326
59074
  });
58327
59075
  object({
58328
59076
  /** Normalized 0–100. */
58329
- brightness: number().optional(),
59077
+ brightness: number().optional().meta(readingMeta("none")),
58330
59078
  /** Normalized 0–100. */
58331
- contrast: number().optional(),
59079
+ contrast: number().optional().meta(readingMeta("none")),
58332
59080
  /** Normalized 0–100. */
58333
- saturation: number().optional(),
59081
+ saturation: number().optional().meta(readingMeta("none")),
58334
59082
  /** Normalized 0–100. */
58335
- sharpness: number().optional(),
59083
+ sharpness: number().optional().meta(readingMeta("none")),
58336
59084
  mirror: boolean().optional(),
58337
59085
  flip: boolean().optional(),
58338
59086
  rotate: ImageRotateSchema.optional(),
58339
59087
  whiteBalance: WhiteBalanceModeSchema.optional(),
58340
59088
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
58341
- warmth: number().optional(),
59089
+ warmth: number().optional().meta(readingMeta("none")),
58342
59090
  exposureMode: ExposureModeSchema.optional(),
58343
59091
  backlightMode: BacklightModeSchema.optional(),
58344
59092
  lastFetchedAt: number()
@@ -58583,10 +59331,10 @@ var IntercomAbilitySchema = object({
58583
59331
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
58584
59332
  codecs: array(string()),
58585
59333
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
58586
- sampleRate: number(),
59334
+ sampleRate: number().meta(readingMeta("none")),
58587
59335
  duplex: _enum(["half", "full"]),
58588
59336
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
58589
- maxBacklogMs: number()
59337
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
58590
59338
  });
58591
59339
  var IntercomStatusSchema = object({
58592
59340
  /** True while a talk session is open. */
@@ -58697,13 +59445,13 @@ object({
58697
59445
  activity: LawnMowerActivitySchema,
58698
59446
  /** 0..100 battery percentage. Null when the device has no battery
58699
59447
  * reading. */
58700
- batteryLevel: number().min(0).max(100).nullable(),
59448
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58701
59449
  /** 0..100 mowing-completion percentage of the current task, or null when no
58702
59450
  * task is active / progress is unavailable. */
58703
- progressPercent: number().min(0).max(100).nullable(),
59451
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58704
59452
  /** Current device/event code (dynamic — mostly status, sometimes an error),
58705
59453
  * or null when unknown. */
58706
- currentCode: number().nullable(),
59454
+ currentCode: number().nullable().meta(readingMeta("none")),
58707
59455
  /** Human label for {@link currentCode}, or null when undecodable. */
58708
59456
  currentCodeLabel: string().nullable(),
58709
59457
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -59113,7 +59861,14 @@ DeviceType.Lock, method(object({
59113
59861
  auth: "protected"
59114
59862
  }), method(object({
59115
59863
  deviceId: number().int().nonnegative(),
59116
- code: string().min(1).optional()
59864
+ code: string().min(1).optional(),
59865
+ /**
59866
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
59867
+ * with one call is refused unless the caller says it was confirmed. A
59868
+ * single lock ignores it. The refusal lives in the callee, so no
59869
+ * automation or API caller reaches the doors by skipping a dialog.
59870
+ */
59871
+ confirm: boolean().optional()
59117
59872
  }), _void(), {
59118
59873
  kind: "mutation",
59119
59874
  auth: "protected"
@@ -59121,6 +59876,11 @@ DeviceType.Lock, method(object({
59121
59876
  kind: "mutation",
59122
59877
  auth: "protected"
59123
59878
  });
59879
+ object({
59880
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
59881
+ lastFetchedAt: number()
59882
+ });
59883
+ DeviceType.Sensor;
59124
59884
  /**
59125
59885
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
59126
59886
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
@@ -59165,7 +59925,7 @@ object({
59165
59925
  state: MediaPlayerStateSchema,
59166
59926
  /** Volume as 0..100 inclusive. Null when the device has no volume
59167
59927
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
59168
- volumeLevel: number().min(0).max(100).nullable(),
59928
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59169
59929
  /** Mute toggle distinct from volume=0. Null when no mute surface.
59170
59930
  * Pair with `DeviceFeature.MediaPlayerMute`. */
59171
59931
  isMuted: boolean().nullable(),
@@ -59178,10 +59938,10 @@ object({
59178
59938
  currentMedia: MediaInfoSchema.nullable(),
59179
59939
  /** Current playback position in ms. Null when not seekable or
59180
59940
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
59181
- positionMs: number().int().nonnegative().nullable(),
59941
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59182
59942
  /** Total duration of the current media in ms. Null when unknown
59183
59943
  * (live stream) or nothing is playing. */
59184
- durationMs: number().int().nonnegative().nullable(),
59944
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59185
59945
  /** Shuffle toggle. Null when no shuffle surface.
59186
59946
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
59187
59947
  shuffle: boolean().nullable(),
@@ -59481,7 +60241,7 @@ object({
59481
60241
  * it, every provider seeded it at boot, and every session left it
59482
60242
  * `detected: true` after its detach.
59483
60243
  */
59484
- autoClearAfterMs: number().nullable()
60244
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
59485
60245
  });
59486
60246
  /**
59487
60247
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -59531,7 +60291,7 @@ var MotionZoneRegionSchema = object({
59531
60291
  });
59532
60292
  object({
59533
60293
  enabled: boolean(),
59534
- sensitivity: number(),
60294
+ sensitivity: number().meta(readingMeta("none")),
59535
60295
  /** Grid region(s). Today exactly one `grid` shape. */
59536
60296
  regions: array(MotionZoneRegionSchema),
59537
60297
  lastFetchedAt: number()
@@ -59835,7 +60595,7 @@ var NavigationStatusSchema = object({
59835
60595
  following: boolean(),
59836
60596
  flash: boolean(),
59837
60597
  lightMode: NavigationLightModeSchema,
59838
- lightLevel: number().min(40).max(100),
60598
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
59839
60599
  /** Ms epoch when the slice was last updated. */
59840
60600
  lastChangedAt: number()
59841
60601
  });
@@ -59881,13 +60641,13 @@ var NetworkLinkStatusSchema = object({
59881
60641
  * one whose reading has not landed must not be drawn at 0 %. Consumers
59882
60642
  * SKIP a null rather than coerce it.
59883
60643
  */
59884
- signalPercent: number().min(0).max(100).nullable(),
60644
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59885
60645
  /** Raw received signal strength in dBm, when the firmware reports one. */
59886
- rssiDbm: number().optional(),
60646
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
59887
60647
  /** Network name of a wireless link, when the firmware reports it. */
59888
60648
  ssid: string().optional(),
59889
60649
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
59890
- lastUpdated: number()
60650
+ lastUpdated: number().meta(readingMeta("none"))
59891
60651
  });
59892
60652
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
59893
60653
  deviceId: number(),
@@ -60206,7 +60966,7 @@ object({
60206
60966
  lastError: string().nullable(),
60207
60967
  /** Number of deliveries currently buffered server-side (rate-limited
60208
60968
  * notifiers may queue). 0 when the provider sends synchronously. */
60209
- queueDepth: number().int().nonnegative(),
60969
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
60210
60970
  /** Per-feature capability matrix — authoritative source for the
60211
60971
  * compose-form field gating. */
60212
60972
  supports: NotifierSupportsSchema
@@ -60256,15 +61016,8 @@ DeviceType.Notifier, method(NotifierSendInputSchema, NotifierSendResultSchema, {
60256
61016
  acceptedAt: number()
60257
61017
  });
60258
61018
  object({
60259
- value: number(),
60260
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
60261
- * Populated live from the upstream source on each state push.
60262
- * Absent for unitless sensors. */
60263
- unit: string().optional(),
60264
- /** Suggested decimal places for numeric display.
60265
- * Populated live from the upstream source when provided.
60266
- * Falls back to auto-formatting when absent. */
60267
- precision: number().int().min(0).max(10).optional(),
61019
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
61020
+ value: number().nullable().meta(readingMeta("device")),
60268
61021
  /** Ms epoch when the slice was last updated. */
60269
61022
  lastFetchedAt: number()
60270
61023
  });
@@ -60554,23 +61307,26 @@ object({
60554
61307
  /** Food currently in the bowl (grams). Null when the device has not
60555
61308
  * reported a reading yet. On dual-hopper models this is the combined
60556
61309
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
60557
- foodLevel: number().nullable(),
61310
+ foodLevel: number().nullable().meta(readingMeta("none")),
60558
61311
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
60559
61312
  * single-hopper models. */
60560
- food1: number().nullable(),
61313
+ food1: number().nullable().meta(readingMeta("none")),
60561
61314
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
60562
61315
  * single-hopper models. */
60563
- food2: number().nullable(),
61316
+ food2: number().nullable().meta(readingMeta("none")),
60564
61317
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
60565
61318
  * (`device_class: problem`, on = low). True when the bowl is empty /
60566
61319
  * below the feeder's low threshold. */
60567
61320
  lowFood: boolean(),
60568
61321
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
60569
61322
  * device has no battery reading. */
60570
- batteryPower: number().min(0).max(100).nullable(),
60571
- /** Days of desiccant life remaining. Null when the model has no
60572
- * desiccant sensor. */
60573
- desiccantLeftDays: number().nullable(),
61323
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61324
+ /**
61325
+ * Desiccant life remaining, stored in seconds (duration base, D944).
61326
+ * Providers convert from the days the feeder reports. Null when the
61327
+ * model has no desiccant sensor.
61328
+ */
61329
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
60574
61330
  /** True while a feed is in progress. */
60575
61331
  feeding: boolean(),
60576
61332
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -60581,7 +61337,7 @@ object({
60581
61337
  * with `errorCode` for consumers that want the raw integer. */
60582
61338
  error: string().nullable(),
60583
61339
  /** Raw device error code (0 / null = no error). */
60584
- errorCode: number().nullable(),
61340
+ errorCode: number().nullable().meta(readingMeta("none")),
60585
61341
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
60586
61342
  isDualHopper: boolean(),
60587
61343
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -60591,7 +61347,7 @@ object({
60591
61347
  /** Play a chime when dispensing. */
60592
61348
  feedSound: boolean(),
60593
61349
  /** Speaker / prompt volume level (device-scaled integer). */
60594
- volume: number(),
61350
+ volume: number().meta(readingMeta("none")),
60595
61351
  /** Ms epoch when the slice was last refreshed from the cloud. */
60596
61352
  lastFetchedAt: number()
60597
61353
  });
@@ -61028,38 +61784,37 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
61028
61784
  });
61029
61785
  object({
61030
61786
  /** Instantaneous power draw in watts. */
61031
- watts: number().optional().meta({
61032
- unit: "W",
61033
- label: "Power"
61034
- }),
61787
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
61035
61788
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
61036
- kwhTotal: number().optional().meta({
61037
- unit: "kWh",
61038
- label: "Energy"
61039
- }),
61789
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
61040
61790
  /** Voltage in volts. */
61041
- volts: number().optional(),
61791
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
61042
61792
  /** Current in amperes. */
61043
- amps: number().optional(),
61044
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
61045
- va: number().optional(),
61793
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
61794
+ /** Apparent power in volt-amperes. */
61795
+ va: number().optional().meta(readingMeta("apparent-power")),
61046
61796
  /** Power factor (0..1) if reported. */
61047
- powerFactor: number().min(0).max(1).optional(),
61797
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
61048
61798
  /** Ms epoch when the slice was last updated. */
61049
- lastFetchedAt: number(),
61050
- /** Live display unit of the single metric this slice carries (e.g. HA
61051
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
61052
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
61053
- * unit per slice is unambiguous. The UI prefers this over the role's
61054
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
61055
- unit: string().optional(),
61056
- /** Suggested decimal places for numeric display.
61057
- * Populated live from the upstream source when provided (e.g. HA
61058
- * `attributes.suggested_display_precision`). Falls back to
61059
- * auto-formatting when absent. */
61060
- precision: number().int().min(0).max(10).optional()
61799
+ lastFetchedAt: number()
61061
61800
  });
61062
- DeviceType.Sensor;
61801
+ [
61802
+ DeviceType.Sensor,
61803
+ DeviceType.Switch,
61804
+ DeviceType.Light,
61805
+ DeviceType.Fan,
61806
+ DeviceType.Climate,
61807
+ DeviceType.Thermostat,
61808
+ DeviceType.Humidifier,
61809
+ DeviceType.WaterHeater,
61810
+ DeviceType.MediaPlayer,
61811
+ DeviceType.Washer,
61812
+ DeviceType.Dryer,
61813
+ DeviceType.Dishwasher,
61814
+ DeviceType.Vacuum,
61815
+ DeviceType.LawnMower,
61816
+ DeviceType.PetFeeder
61817
+ ].filter((type) => type !== DeviceType.Sensor);
61063
61818
  /**
61064
61819
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
61065
61820
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -61078,11 +61833,11 @@ DeviceType.Sensor;
61078
61833
  */
61079
61834
  var GpsLocationSchema = object({
61080
61835
  /** Latitude in decimal degrees, -90..90. */
61081
- latitude: number().min(-90).max(90),
61836
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
61082
61837
  /** Longitude in decimal degrees, -180..180. */
61083
- longitude: number().min(-180).max(180),
61838
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
61084
61839
  /** Reported accuracy in meters (lower = better). */
61085
- accuracyMeters: number().nonnegative()
61840
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
61086
61841
  });
61087
61842
  object({
61088
61843
  /** `home` / `not_home` / any user-defined zone name. */
@@ -61094,25 +61849,16 @@ object({
61094
61849
  gps: GpsLocationSchema.nullable(),
61095
61850
  /** Optional battery level of the tracking device (0..100). Null
61096
61851
  * when not reported. */
61097
- batteryPercent: number().min(0).max(100).nullable(),
61852
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61098
61853
  /** Ms epoch when the slice was last updated. */
61099
61854
  lastChangedAt: number()
61100
61855
  });
61101
61856
  DeviceType.Presence;
61102
61857
  object({
61103
- /** Current pressure in hPa. */
61104
- hpa: number(),
61858
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
61859
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
61105
61860
  /** Ms epoch when the slice was last updated. */
61106
- lastFetchedAt: number(),
61107
- /** Live display unit from the upstream source (e.g. HA
61108
- * `attributes.unit_of_measurement`). The UI prefers this over the
61109
- * role's canonical unit. Absent → fall back to the canonical unit. */
61110
- unit: string().optional(),
61111
- /** Suggested decimal places for numeric display.
61112
- * Populated live from the upstream source when provided (e.g. HA
61113
- * `attributes.suggested_display_precision`). Falls back to
61114
- * auto-formatting when absent. */
61115
- precision: number().int().min(0).max(10).optional()
61861
+ lastFetchedAt: number()
61116
61862
  });
61117
61863
  DeviceType.Sensor;
61118
61864
  /**
@@ -61445,8 +62191,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
61445
62191
  }), _void(), { kind: "mutation" });
61446
62192
  var PtzAutotrackSettingsSchema = object({
61447
62193
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
61448
- stopDelaySeconds: number().int().min(0).max(300),
61449
- disappearDelaySeconds: number().int().min(0).max(300)
62194
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
62195
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
61450
62196
  });
61451
62197
  /**
61452
62198
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -61503,7 +62249,7 @@ object({
61503
62249
  /** null = unknown — never coerced to false (D393). */
61504
62250
  running: boolean().nullable(),
61505
62251
  /** Epoch ms the current boolean began; null while `running` is null. */
61506
- since: number().nullable(),
62252
+ since: number().nullable().meta(readingMeta("none")),
61507
62253
  source: RunningStateSourceSchema.meta({ label: "Source" }),
61508
62254
  phase: string().nullable(),
61509
62255
  /** null until a reading has been published (D393). */
@@ -61515,7 +62261,7 @@ object({
61515
62261
  phase: string().nullable()
61516
62262
  });
61517
62263
  Object.values(DeviceType);
61518
- DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
62264
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
61519
62265
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
61520
62266
  kind: "mutation",
61521
62267
  auth: "admin"
@@ -62701,9 +63447,9 @@ var RecordTriggerSchema = _enum([
62701
63447
  */
62702
63448
  var RecordWindowSchema = object({
62703
63449
  trigger: RecordTriggerSchema,
62704
- day: number().int().min(0).max(6),
62705
- startMinute: number().int().min(0).max(1439),
62706
- endMinute: number().int().min(1).max(1440)
63450
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
63451
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
63452
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
62707
63453
  });
62708
63454
  /** Status of one physical volume, as the camera itself describes it. */
62709
63455
  var OnboardStorageVolumeSchema = object({
@@ -62772,10 +63518,10 @@ object({
62772
63518
  /** Master "record to the card at all" switch. */
62773
63519
  enabled: boolean().nullable(),
62774
63520
  overwriteWhenFull: boolean().nullable(),
62775
- preRecordSec: number().nullable(),
62776
- postRecordSec: number().nullable(),
63521
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
63522
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
62777
63523
  /** Length of one recorded file, in minutes. */
62778
- segmentMinutes: number().nullable(),
63524
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
62779
63525
  /** The primary track's weekly windows, flattened. */
62780
63526
  windows: array(RecordWindowSchema),
62781
63527
  /**
@@ -62791,7 +63537,7 @@ object({
62791
63537
  * provider reporting a non-zero count also reports the schedule as not
62792
63538
  * writable.
62793
63539
  */
62794
- unreadableWindows: number(),
63540
+ unreadableWindows: number().meta(readingMeta("count")),
62795
63541
  /**
62796
63542
  * The camera is scheduled to record and has NO usable storage.
62797
63543
  *
@@ -63428,7 +64174,7 @@ var CamStreamDescriptorSchema = object({
63428
64174
  url: string().optional(),
63429
64175
  codec: string().optional(),
63430
64176
  resolution: CamStreamResolutionSchema.optional(),
63431
- fps: number().positive().optional(),
64177
+ fps: number().positive().optional().meta(readingMeta("rate")),
63432
64178
  label: string().optional(),
63433
64179
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
63434
64180
  deviceFeatures: array(string()).optional(),
@@ -63458,18 +64204,18 @@ var StreamProfileSchema = _enum([
63458
64204
  ]);
63459
64205
  /** The current encoding config of one stream profile. */
63460
64206
  var StreamProfileConfigSchema = object({
63461
- width: number(),
63462
- height: number(),
64207
+ width: number().meta(readingMeta("none")),
64208
+ height: number().meta(readingMeta("none")),
63463
64209
  codec: _enum(["h264", "h265"]),
63464
- framerate: number(),
63465
- bitrate: number(),
64210
+ framerate: number().meta(readingMeta("rate")),
64211
+ bitrate: number().meta(readingMeta("bitrate")),
63466
64212
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
63467
64213
  encoderProfile: _enum([
63468
64214
  "high",
63469
64215
  "main",
63470
64216
  "baseline"
63471
64217
  ]).optional(),
63472
- gop: number().optional(),
64218
+ gop: number().optional().meta(readingMeta("none")),
63473
64219
  /**
63474
64220
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
63475
64221
  *
@@ -63554,7 +64300,7 @@ object({
63554
64300
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
63555
64301
  lastChangedAt: number()
63556
64302
  });
63557
- DeviceType.Switch, DeviceType.Siren, DeviceType.Light, method(object({
64303
+ DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, method(object({
63558
64304
  deviceId: number(),
63559
64305
  on: boolean()
63560
64306
  }), _void(), {
@@ -64112,23 +64858,13 @@ object({
64112
64858
  });
64113
64859
  DeviceType.Sensor;
64114
64860
  object({
64115
- /** Current temperature in Celsius. */
64116
- celsius: number().meta({
64117
- unit: "°C",
64861
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
64862
+ celsius: number().nullable().meta(readingMeta("temperature", {
64863
+ precision: 1,
64118
64864
  label: "Temperature"
64119
- }),
64865
+ })),
64120
64866
  /** Ms epoch when the slice was last updated (push or poll). */
64121
- lastFetchedAt: number(),
64122
- /** Live display unit from the upstream source (e.g. HA
64123
- * `attributes.unit_of_measurement`). The UI prefers this over the
64124
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
64125
- * Absent → the UI falls back to the role's canonical unit. */
64126
- unit: string().optional(),
64127
- /** Suggested decimal places for numeric display.
64128
- * Populated live from the upstream source when provided (e.g. HA
64129
- * `attributes.suggested_display_precision`). Falls back to
64130
- * auto-formatting when absent. */
64131
- precision: number().int().min(0).max(10).optional()
64867
+ lastFetchedAt: number()
64132
64868
  });
64133
64869
  DeviceType.Sensor, DeviceType.Thermostat;
64134
64870
  /**
@@ -64380,16 +65116,16 @@ object({
64380
65116
  ]),
64381
65117
  /** 0..100 battery percentage. Null when the device has no battery
64382
65118
  * reading. */
64383
- batteryLevel: number().min(0).max(100).nullable(),
65119
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64384
65120
  /** Current fan-speed token (provider-verbatim). Null when unknown or
64385
65121
  * the vacuum has no speed control. */
64386
65122
  fanSpeed: string().nullable(),
64387
65123
  /** Speed tokens the hardware accepts — drives the UI selector. */
64388
65124
  availableFanSpeeds: array(string()),
64389
65125
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64390
- progressPercent: number().min(0).max(100).nullable(),
65126
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64391
65127
  /** Current error code (0 / null = no error). */
64392
- errorCode: number().nullable(),
65128
+ errorCode: number().nullable().meta(readingMeta("none")),
64393
65129
  /** Human label for {@link errorCode}, or null when none / undecodable. */
64394
65130
  errorLabel: string().nullable(),
64395
65131
  /** Ms epoch when the slice was last updated. */
@@ -64428,7 +65164,7 @@ object({
64428
65164
  ]),
64429
65165
  /** 0 = fully closed, 100 = fully open. Null when the device has no
64430
65166
  * intermediate position surface. */
64431
- position: number().min(0).max(100).nullable(),
65167
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64432
65168
  /** Ms epoch when the slice was last updated. */
64433
65169
  lastChangedAt: number()
64434
65170
  });
@@ -64456,9 +65192,9 @@ object({
64456
65192
  DeviceType.Sensor;
64457
65193
  object({
64458
65194
  /** Current measured temperature. Null when not reported. */
64459
- currentTemp: number().nullable(),
65195
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
64460
65196
  /** Target temperature setpoint. Null when no setpoint surface. */
64461
- targetTemp: number().nullable(),
65197
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
64462
65198
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
64463
65199
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
64464
65200
  * device reports an unknown state. */
@@ -64469,9 +65205,9 @@ object({
64469
65205
  * has no away surface. */
64470
65206
  away: boolean().nullable(),
64471
65207
  /** HA `min_temp` attribute. Null when not reported. */
64472
- minTemp: number().nullable(),
65208
+ minTemp: number().nullable().meta(readingMeta("temperature")),
64473
65209
  /** HA `max_temp` attribute. Null when not reported. */
64474
- maxTemp: number().nullable(),
65210
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
64475
65211
  /** Ms epoch when the slice was last updated. */
64476
65212
  lastChangedAt: number()
64477
65213
  });
@@ -64495,25 +65231,18 @@ DeviceType.WaterHeater, method(object({
64495
65231
  auth: "protected"
64496
65232
  });
64497
65233
  object({
64498
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
64499
- * when no condition has been reported yet. */
65234
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
64500
65235
  condition: string().nullable(),
64501
- /** Current temperature in the reported unit. Null when not provided. */
64502
- temperature: number().nullable(),
64503
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
64504
- temperatureUnit: string().nullable(),
65236
+ /** Current temperature in Celsius. Null when not provided or refused. */
65237
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
64505
65238
  /** Relative humidity (0..100). Null when not provided. */
64506
- humidity: number().min(0).max(100).nullable(),
64507
- /** Barometric pressure in the reported unit. Null when not provided. */
64508
- pressure: number().nullable(),
64509
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
64510
- pressureUnit: string().nullable(),
64511
- /** Wind speed in the reported unit. Null when not provided. */
64512
- windSpeed: number().nullable(),
64513
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
64514
- windSpeedUnit: string().nullable(),
65239
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
65240
+ /** Barometric pressure in hPa. Null when not provided or refused. */
65241
+ pressure: number().nullable().meta(readingMeta("pressure")),
65242
+ /** Wind speed in metres per second. Null when not provided or refused. */
65243
+ windSpeed: number().nullable().meta(readingMeta("speed")),
64515
65244
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
64516
- windBearing: number().nullable(),
65245
+ windBearing: number().nullable().meta(readingMeta("angle")),
64517
65246
  /** Ms epoch when the slice was last updated. */
64518
65247
  lastFetchedAt: number()
64519
65248
  });
@@ -65263,8 +65992,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
65263
65992
  kind: "mutation",
65264
65993
  auth: "admin"
65265
65994
  });
65995
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
65266
65996
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
65267
- 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;
65997
+ 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;
65268
65998
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
65269
65999
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
65270
66000
  new Set(CAP_NAMES);
@@ -65317,7 +66047,7 @@ var AccessoryKind = {
65317
66047
  PrivacyMask: DeviceRole.PrivacyMask
65318
66048
  };
65319
66049
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
65320
- 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;
66050
+ 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;
65321
66051
  new Set(Object.values(DeviceType));
65322
66052
  Object.freeze({
65323
66053
  "accessories.getStatus": {
@@ -65425,6 +66155,13 @@ Object.freeze({
65425
66155
  access: "view",
65426
66156
  role: "admin"
65427
66157
  },
66158
+ "addons.getPackageDocs": {
66159
+ capName: "addons",
66160
+ capScope: "system",
66161
+ addonId: null,
66162
+ access: "view",
66163
+ role: "admin"
66164
+ },
65428
66165
  "addons.getVersions": {
65429
66166
  capName: "addons",
65430
66167
  capScope: "system",
@@ -68533,6 +69270,27 @@ Object.freeze({
68533
69270
  access: "view",
68534
69271
  role: "viewer"
68535
69272
  },
69273
+ "groupActive.getStatus": {
69274
+ capName: "group-active",
69275
+ capScope: "device",
69276
+ addonId: null,
69277
+ access: "view",
69278
+ role: "viewer"
69279
+ },
69280
+ "groupCommands.getStatus": {
69281
+ capName: "group-commands",
69282
+ capScope: "device",
69283
+ addonId: null,
69284
+ access: "view",
69285
+ role: "viewer"
69286
+ },
69287
+ "groupStatus.getStatus": {
69288
+ capName: "group-status",
69289
+ capScope: "device",
69290
+ addonId: null,
69291
+ access: "view",
69292
+ role: "viewer"
69293
+ },
68536
69294
  "heatingValve.getStatus": {
68537
69295
  capName: "heating-valve",
68538
69296
  capScope: "device",
@@ -69485,6 +70243,13 @@ Object.freeze({
69485
70243
  access: "create",
69486
70244
  role: "admin"
69487
70245
  },
70246
+ "meter.getStatus": {
70247
+ capName: "meter",
70248
+ capScope: "device",
70249
+ addonId: null,
70250
+ access: "view",
70251
+ role: "viewer"
70252
+ },
69488
70253
  "metricsProvider.collectSnapshot": {
69489
70254
  capName: "metrics-provider",
69490
70255
  capScope: "system",
@@ -77583,7 +78348,39 @@ var APPLIANCE_CAP_VOCABULARY = {
77583
78348
  phrase: t("When {device} finishes", "Quando {device} finisce"),
77584
78349
  icon: "square"
77585
78350
  }
77586
- }
78351
+ },
78352
+ overrides: { byType: {
78353
+ [DeviceType.Washer]: {
78354
+ state: { running: {
78355
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
78356
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
78357
+ } },
78358
+ events: {
78359
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
78360
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
78361
+ }
78362
+ },
78363
+ [DeviceType.Dryer]: {
78364
+ state: { running: {
78365
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
78366
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
78367
+ } },
78368
+ events: {
78369
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
78370
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
78371
+ }
78372
+ },
78373
+ [DeviceType.Dishwasher]: {
78374
+ state: { running: {
78375
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
78376
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
78377
+ } },
78378
+ events: {
78379
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
78380
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
78381
+ }
78382
+ }
78383
+ } }
77587
78384
  },
77588
78385
  update: {
77589
78386
  scope: "device",
@@ -78355,6 +79152,41 @@ var SENSOR_CAP_VOCABULARY = {
78355
79152
  icon: "flame"
78356
79153
  }]
78357
79154
  },
79155
+ "group-active": {
79156
+ scope: "device",
79157
+ noun: t("Group state", "Stato del gruppo")
79158
+ },
79159
+ "group-commands": {
79160
+ scope: "device",
79161
+ noun: t("Group command", "Comando di gruppo")
79162
+ },
79163
+ "group-status": {
79164
+ scope: "device",
79165
+ noun: t("Group", "Gruppo"),
79166
+ state: [
79167
+ {
79168
+ kind: "numeric",
79169
+ field: "included",
79170
+ quantity: t("the members reporting", "i membri che rispondono"),
79171
+ label: t("Reporting", "Rispondono"),
79172
+ icon: "users"
79173
+ },
79174
+ {
79175
+ kind: "numeric",
79176
+ field: "excluded",
79177
+ quantity: t("the members left out", "i membri esclusi"),
79178
+ label: t("Excluded", "Esclusi"),
79179
+ icon: "users"
79180
+ },
79181
+ {
79182
+ kind: "numeric",
79183
+ field: "total",
79184
+ quantity: t("the member count", "il numero di membri"),
79185
+ label: t("Members", "Membri"),
79186
+ icon: "sigma"
79187
+ }
79188
+ ]
79189
+ },
78358
79190
  "humidity-sensor": {
78359
79191
  scope: "device",
78360
79192
  noun: t("Humidity", "Umidità"),
@@ -78380,6 +79212,17 @@ var SENSOR_CAP_VOCABULARY = {
78380
79212
  }],
78381
79213
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
78382
79214
  },
79215
+ meter: {
79216
+ scope: "device",
79217
+ noun: t("Meter", "Contatore"),
79218
+ state: [{
79219
+ kind: "numeric",
79220
+ field: "value",
79221
+ quantity: t("the total", "il totale"),
79222
+ label: t("Total", "Totale"),
79223
+ icon: "gauge"
79224
+ }]
79225
+ },
78383
79226
  "numeric-sensor": {
78384
79227
  scope: "device",
78385
79228
  noun: t("Sensor", "Sensore"),
@@ -78388,7 +79231,6 @@ var SENSOR_CAP_VOCABULARY = {
78388
79231
  field: "value",
78389
79232
  quantity: t("the reading", "il valore"),
78390
79233
  label: t("Reading", "Valore"),
78391
- unitField: "unit",
78392
79234
  icon: "gauge"
78393
79235
  }]
78394
79236
  },
@@ -78524,7 +79366,7 @@ var SENSOR_CAP_VOCABULARY = {
78524
79366
  field: "temperature",
78525
79367
  quantity: t("the outdoor temperature", "la temperatura esterna"),
78526
79368
  label: t("Temperature", "Temperatura"),
78527
- unitField: "temperatureUnit",
79369
+ unit: "°C",
78528
79370
  icon: "thermometer"
78529
79371
  },
78530
79372
  {
@@ -78540,7 +79382,7 @@ var SENSOR_CAP_VOCABULARY = {
78540
79382
  field: "windSpeed",
78541
79383
  quantity: t("the wind speed", "la velocità del vento"),
78542
79384
  label: t("Wind", "Vento"),
78543
- unitField: "windSpeedUnit",
79385
+ unit: "m/s",
78544
79386
  icon: "wind"
78545
79387
  },
78546
79388
  {
@@ -78548,7 +79390,7 @@ var SENSOR_CAP_VOCABULARY = {
78548
79390
  field: "pressure",
78549
79391
  quantity: t("the air pressure", "la pressione atmosferica"),
78550
79392
  label: t("Pressure", "Pressione"),
78551
- unitField: "pressureUnit",
79393
+ unit: "hPa",
78552
79394
  icon: "gauge"
78553
79395
  }
78554
79396
  ]
@@ -78853,8 +79695,47 @@ function projectAction(cap, action) {
78853
79695
  };
78854
79696
  }
78855
79697
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
78856
- 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;
79698
+ 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;
78857
79699
  DeviceFeature.BatteryOperated;
79700
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
79701
+ 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.";
79702
+ 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.";
79703
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
79704
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
79705
+ 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.";
79706
+ function withheld(reason) {
79707
+ return {
79708
+ [DeviceType.Washer]: {
79709
+ exported: false,
79710
+ reason
79711
+ },
79712
+ [DeviceType.Dryer]: {
79713
+ exported: false,
79714
+ reason
79715
+ },
79716
+ [DeviceType.Dishwasher]: {
79717
+ exported: false,
79718
+ reason
79719
+ }
79720
+ };
79721
+ }
79722
+ function offered(received) {
79723
+ return {
79724
+ [DeviceType.Washer]: {
79725
+ exported: true,
79726
+ as: received
79727
+ },
79728
+ [DeviceType.Dryer]: {
79729
+ exported: true,
79730
+ as: received
79731
+ },
79732
+ [DeviceType.Dishwasher]: {
79733
+ exported: true,
79734
+ as: received
79735
+ }
79736
+ };
79737
+ }
79738
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
78858
79739
  Object.freeze({
78859
79740
  "accessories.getStatus": [{
78860
79741
  name: "deviceId",
@@ -79821,6 +80702,21 @@ Object.freeze({
79821
80702
  form: "single",
79822
80703
  optional: false
79823
80704
  }],
80705
+ "groupActive.getStatus": [{
80706
+ name: "deviceId",
80707
+ form: "single",
80708
+ optional: false
80709
+ }],
80710
+ "groupCommands.getStatus": [{
80711
+ name: "deviceId",
80712
+ form: "single",
80713
+ optional: false
80714
+ }],
80715
+ "groupStatus.getStatus": [{
80716
+ name: "deviceId",
80717
+ form: "single",
80718
+ optional: false
80719
+ }],
79824
80720
  "heatingValve.getStatus": [{
79825
80721
  name: "deviceId",
79826
80722
  form: "single",
@@ -80021,6 +80917,11 @@ Object.freeze({
80021
80917
  form: "single",
80022
80918
  optional: false
80023
80919
  }],
80920
+ "meter.getStatus": [{
80921
+ name: "deviceId",
80922
+ form: "single",
80923
+ optional: false
80924
+ }],
80024
80925
  "motion.getStatus": [{
80025
80926
  name: "deviceId",
80026
80927
  form: "single",
@@ -81628,7 +82529,6 @@ Object.freeze({
81628
82529
  "network-access": "ingress",
81629
82530
  "smtp-provider": "email"
81630
82531
  });
81631
- DeviceType.Camera;
81632
82532
  /**
81633
82533
  * A saved Customize assignment (D936). The schema is the type: callers use
81634
82534
  * `z.infer`, they do not restate the shape.
@@ -81805,6 +82705,8 @@ new Map(Object.entries({
81805
82705
  "detection-blind": "device-events",
81806
82706
  "motion-noisy": "device-events",
81807
82707
  "motion-calm": "device-events",
82708
+ "package-llm-silent": "device-events",
82709
+ "package-llm-answered": "device-events",
81808
82710
  "recording-stalled": "device-events",
81809
82711
  "recording-healthy": "device-events",
81810
82712
  "recording-degraded": "device-events",
@@ -81888,6 +82790,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
81888
82790
  label: "Motion without a subject",
81889
82791
  kinds: ["motion-noisy", "motion-calm"]
81890
82792
  },
82793
+ {
82794
+ id: "package-llm",
82795
+ family: "device-events",
82796
+ label: "Package confirm",
82797
+ kinds: ["package-llm-silent", "package-llm-answered"]
82798
+ },
81891
82799
  {
81892
82800
  id: "recording",
81893
82801
  family: "device-events",
@@ -82069,6 +82977,8 @@ var DEVICE_EVENTS = [
82069
82977
  "detection-blind",
82070
82978
  "motion-noisy",
82071
82979
  "motion-calm",
82980
+ "package-llm-silent",
82981
+ "package-llm-answered",
82072
82982
  "recording-stalled",
82073
82983
  "recording-healthy",
82074
82984
  "recording-degraded",
@@ -82337,6 +83247,15 @@ new Map([
82337
83247
  snapshotLabel: "Cameras with constant motion now",
82338
83248
  snapshotSection: null
82339
83249
  },
83250
+ {
83251
+ id: "package-llm-confirm",
83252
+ family: "device-events",
83253
+ label: "Package confirm (not answering / answering again)",
83254
+ bad: ["package-llm-silent"],
83255
+ recovery: ["package-llm-answered"],
83256
+ snapshotLabel: "Cameras whose package confirm is not answering",
83257
+ snapshotSection: null
83258
+ },
82340
83259
  {
82341
83260
  id: "recording-health",
82342
83261
  family: "device-events",