@camstack/addon-mqtt-broker 1.2.145 → 1.2.147

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.
@@ -46,7 +46,7 @@ let node_fs = require("node:fs");
46
46
  node_fs = __toESM(node_fs);
47
47
  let node_path = require("node:path");
48
48
  node_path = __toESM(node_path);
49
- //#region ../types/dist/event-category-DHXxB8t9.mjs
49
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
50
50
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
51
51
  EventCategory["SystemBoot"] = "system.boot";
52
52
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -372,6 +372,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
372
372
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
373
373
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
374
374
  /**
375
+ * A camera's package LLM confirm has failed closed three times in a row
376
+ * (D931, 2026-10-26). One edge per camera until
377
+ * {@link PipelinePackageLlmAnswered}.
378
+ */
379
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
380
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
381
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
382
+ /**
375
383
  * Per-camera pipeline config was mutated by the orchestrator
376
384
  * (a settings change via `setAgentInferenceDevices` /
377
385
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5614,7 +5622,7 @@ var ZodIssueCode = {
5614
5622
  var ZodFirstPartyTypeKind;
5615
5623
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5616
5624
  //#endregion
5617
- //#region ../types/dist/sleep-BErAxhul.mjs
5625
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5618
5626
  /**
5619
5627
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5620
5628
  * window to float samples (D455).
@@ -8356,6 +8364,99 @@ function defineEntryKind(name, spec) {
8356
8364
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8357
8365
  return defined;
8358
8366
  }
8367
+ var READING_QUANTITIES = [
8368
+ "temperature",
8369
+ "pressure",
8370
+ "speed",
8371
+ "precipitation",
8372
+ "precipitation-rate",
8373
+ "length",
8374
+ "area",
8375
+ "illuminance",
8376
+ "irradiance",
8377
+ "power",
8378
+ "energy",
8379
+ "voltage",
8380
+ "current",
8381
+ "apparent-power",
8382
+ "humidity",
8383
+ "percentage",
8384
+ "concentration-volume",
8385
+ "concentration-mass",
8386
+ "concentration-ppb",
8387
+ "volume",
8388
+ "duration",
8389
+ "duration-ms",
8390
+ "information",
8391
+ "sound-level",
8392
+ "signal-level",
8393
+ "colour-temperature",
8394
+ "angle",
8395
+ "bitrate",
8396
+ "rate",
8397
+ "mass",
8398
+ "index",
8399
+ "ratio",
8400
+ "count",
8401
+ "none",
8402
+ "device"
8403
+ ];
8404
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8405
+ new Set(READING_QUANTITIES);
8406
+ /**
8407
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8408
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8409
+ */
8410
+ var QUANTITY_BASE = {
8411
+ temperature: "°C",
8412
+ pressure: "hPa",
8413
+ speed: "m/s",
8414
+ precipitation: "mm",
8415
+ "precipitation-rate": "mm/h",
8416
+ length: "m",
8417
+ area: "m²",
8418
+ illuminance: "lx",
8419
+ irradiance: "W/m²",
8420
+ power: "W",
8421
+ energy: "kWh",
8422
+ voltage: "V",
8423
+ current: "A",
8424
+ "apparent-power": "VA",
8425
+ humidity: "%",
8426
+ percentage: "%",
8427
+ "concentration-volume": "ppm",
8428
+ "concentration-mass": "µg/m³",
8429
+ "concentration-ppb": "ppb",
8430
+ volume: "L",
8431
+ duration: "s",
8432
+ "duration-ms": "ms",
8433
+ information: "B",
8434
+ "sound-level": "dBFS",
8435
+ "signal-level": "dBm",
8436
+ "colour-temperature": "mired",
8437
+ angle: "°",
8438
+ bitrate: "kbps",
8439
+ rate: "fps",
8440
+ mass: "kg",
8441
+ index: "none",
8442
+ ratio: "none",
8443
+ count: "none",
8444
+ none: "none",
8445
+ device: "device"
8446
+ };
8447
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8448
+ function baseUnitOf(quantity) {
8449
+ const symbol = QUANTITY_BASE[quantity];
8450
+ if (symbol === "none" || symbol === "device") return void 0;
8451
+ return symbol;
8452
+ }
8453
+ function readingMeta(quantity, extras) {
8454
+ return {
8455
+ quantity,
8456
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8457
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8458
+ };
8459
+ }
8359
8460
  /** Shared Zod schemas used across streaming capabilities. */
8360
8461
  var CamProfileSchema = _enum([
8361
8462
  "high",
@@ -8383,8 +8484,8 @@ var CamStreamKindSchema = _enum([
8383
8484
  "derived"
8384
8485
  ]);
8385
8486
  var CamStreamResolutionSchema = object({
8386
- width: number().int().positive(),
8387
- height: number().int().positive()
8487
+ width: number().int().positive().meta(readingMeta("none")),
8488
+ height: number().int().positive().meta(readingMeta("none"))
8388
8489
  });
8389
8490
  var CameraStreamSchema = object({
8390
8491
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8898,6 +8999,9 @@ var CAP_NAMES = Object.freeze([
8898
8999
  "filesystem-browse",
8899
9000
  "flood",
8900
9001
  "gas",
9002
+ "group-active",
9003
+ "group-commands",
9004
+ "group-status",
8901
9005
  "heating-valve",
8902
9006
  "humidifier",
8903
9007
  "humidity-sensor",
@@ -8918,6 +9022,7 @@ var CAP_NAMES = Object.freeze([
8918
9022
  "login-method",
8919
9023
  "media-player",
8920
9024
  "mesh-network",
9025
+ "meter",
8921
9026
  "metrics-provider",
8922
9027
  "model-convert",
8923
9028
  "model-distributor",
@@ -9027,14 +9132,6 @@ Object.freeze({
9027
9132
  {
9028
9133
  "path": "aqi",
9029
9134
  "valueType": "number"
9030
- },
9031
- {
9032
- "path": "unit",
9033
- "valueType": "string"
9034
- },
9035
- {
9036
- "path": "precision",
9037
- "valueType": "number"
9038
9135
  }
9039
9136
  ],
9040
9137
  "alarm-panel": [
@@ -9070,20 +9167,10 @@ Object.freeze({
9070
9167
  "valueType": "boolean"
9071
9168
  }
9072
9169
  ],
9073
- "ambient-light-sensor": [
9074
- {
9075
- "path": "lux",
9076
- "valueType": "number"
9077
- },
9078
- {
9079
- "path": "unit",
9080
- "valueType": "string"
9081
- },
9082
- {
9083
- "path": "precision",
9084
- "valueType": "number"
9085
- }
9086
- ],
9170
+ "ambient-light-sensor": [{
9171
+ "path": "lux",
9172
+ "valueType": "number"
9173
+ }],
9087
9174
  "audio-metrics": [
9088
9175
  {
9089
9176
  "path": "ts",
@@ -9163,6 +9250,10 @@ Object.freeze({
9163
9250
  "path": "lastUpdated",
9164
9251
  "valueType": "number"
9165
9252
  },
9253
+ {
9254
+ "path": "volts",
9255
+ "valueType": "number"
9256
+ },
9166
9257
  {
9167
9258
  "path": "lastContactAt",
9168
9259
  "valueType": "number"
@@ -9405,10 +9496,6 @@ Object.freeze({
9405
9496
  "datetime"
9406
9497
  ]
9407
9498
  },
9408
- {
9409
- "path": "unit",
9410
- "valueType": "string"
9411
- },
9412
9499
  {
9413
9500
  "path": "min",
9414
9501
  "valueType": "number"
@@ -9420,19 +9507,6 @@ Object.freeze({
9420
9507
  {
9421
9508
  "path": "step",
9422
9509
  "valueType": "number"
9423
- },
9424
- {
9425
- "path": "precision",
9426
- "valueType": "number"
9427
- },
9428
- {
9429
- "path": "format",
9430
- "valueType": "enum",
9431
- "enumValues": [
9432
- "date",
9433
- "time",
9434
- "datetime"
9435
- ]
9436
9510
  }
9437
9511
  ],
9438
9512
  "cover": [
@@ -9673,13 +9747,8 @@ Object.freeze({
9673
9747
  "path": "value",
9674
9748
  "valueType": "string"
9675
9749
  }, {
9676
- "path": "format",
9677
- "valueType": "enum",
9678
- "enumValues": [
9679
- "date",
9680
- "time",
9681
- "datetime"
9682
- ]
9750
+ "path": "timestamp",
9751
+ "valueType": "boolean"
9683
9752
  }],
9684
9753
  "event-emitter": [
9685
9754
  {
@@ -9752,6 +9821,24 @@ Object.freeze({
9752
9821
  "path": "detected",
9753
9822
  "valueType": "boolean"
9754
9823
  }],
9824
+ "group-active": [{
9825
+ "path": "active",
9826
+ "valueType": "boolean"
9827
+ }],
9828
+ "group-status": [
9829
+ {
9830
+ "path": "included",
9831
+ "valueType": "number"
9832
+ },
9833
+ {
9834
+ "path": "excluded",
9835
+ "valueType": "number"
9836
+ },
9837
+ {
9838
+ "path": "total",
9839
+ "valueType": "number"
9840
+ }
9841
+ ],
9755
9842
  "heating-valve": [{
9756
9843
  "path": "position",
9757
9844
  "valueType": "number"
@@ -9786,20 +9873,10 @@ Object.freeze({
9786
9873
  "valueType": "number"
9787
9874
  }
9788
9875
  ],
9789
- "humidity-sensor": [
9790
- {
9791
- "path": "percent",
9792
- "valueType": "number"
9793
- },
9794
- {
9795
- "path": "unit",
9796
- "valueType": "string"
9797
- },
9798
- {
9799
- "path": "precision",
9800
- "valueType": "number"
9801
- }
9802
- ],
9876
+ "humidity-sensor": [{
9877
+ "path": "percent",
9878
+ "valueType": "number"
9879
+ }],
9803
9880
  "image": [{
9804
9881
  "path": "url",
9805
9882
  "valueType": "string"
@@ -10007,6 +10084,10 @@ Object.freeze({
10007
10084
  ]
10008
10085
  }
10009
10086
  ],
10087
+ "meter": [{
10088
+ "path": "value",
10089
+ "valueType": "number"
10090
+ }],
10010
10091
  "motion": [
10011
10092
  {
10012
10093
  "path": "detected",
@@ -10138,20 +10219,10 @@ Object.freeze({
10138
10219
  "valueType": "boolean"
10139
10220
  }
10140
10221
  ],
10141
- "numeric-sensor": [
10142
- {
10143
- "path": "value",
10144
- "valueType": "number"
10145
- },
10146
- {
10147
- "path": "unit",
10148
- "valueType": "string"
10149
- },
10150
- {
10151
- "path": "precision",
10152
- "valueType": "number"
10153
- }
10154
- ],
10222
+ "numeric-sensor": [{
10223
+ "path": "value",
10224
+ "valueType": "number"
10225
+ }],
10155
10226
  "pet-feeder": [
10156
10227
  {
10157
10228
  "path": "foodLevel",
@@ -10243,14 +10314,6 @@ Object.freeze({
10243
10314
  {
10244
10315
  "path": "powerFactor",
10245
10316
  "valueType": "number"
10246
- },
10247
- {
10248
- "path": "unit",
10249
- "valueType": "string"
10250
- },
10251
- {
10252
- "path": "precision",
10253
- "valueType": "number"
10254
10317
  }
10255
10318
  ],
10256
10319
  "presence": [
@@ -10279,20 +10342,10 @@ Object.freeze({
10279
10342
  "valueType": "number"
10280
10343
  }
10281
10344
  ],
10282
- "pressure-sensor": [
10283
- {
10284
- "path": "hpa",
10285
- "valueType": "number"
10286
- },
10287
- {
10288
- "path": "unit",
10289
- "valueType": "string"
10290
- },
10291
- {
10292
- "path": "precision",
10293
- "valueType": "number"
10294
- }
10295
- ],
10345
+ "pressure-sensor": [{
10346
+ "path": "hpa",
10347
+ "valueType": "number"
10348
+ }],
10296
10349
  "privacy-mask": [{
10297
10350
  "path": "enabled",
10298
10351
  "valueType": "boolean"
@@ -10603,20 +10656,10 @@ Object.freeze({
10603
10656
  "path": "tampered",
10604
10657
  "valueType": "boolean"
10605
10658
  }],
10606
- "temperature-sensor": [
10607
- {
10608
- "path": "celsius",
10609
- "valueType": "number"
10610
- },
10611
- {
10612
- "path": "unit",
10613
- "valueType": "string"
10614
- },
10615
- {
10616
- "path": "precision",
10617
- "valueType": "number"
10618
- }
10619
- ],
10659
+ "temperature-sensor": [{
10660
+ "path": "celsius",
10661
+ "valueType": "number"
10662
+ }],
10620
10663
  "update": [
10621
10664
  {
10622
10665
  "path": "currentVersion",
@@ -10727,10 +10770,6 @@ Object.freeze({
10727
10770
  "path": "temperature",
10728
10771
  "valueType": "number"
10729
10772
  },
10730
- {
10731
- "path": "temperatureUnit",
10732
- "valueType": "string"
10733
- },
10734
10773
  {
10735
10774
  "path": "humidity",
10736
10775
  "valueType": "number"
@@ -10739,18 +10778,10 @@ Object.freeze({
10739
10778
  "path": "pressure",
10740
10779
  "valueType": "number"
10741
10780
  },
10742
- {
10743
- "path": "pressureUnit",
10744
- "valueType": "string"
10745
- },
10746
10781
  {
10747
10782
  "path": "windSpeed",
10748
10783
  "valueType": "number"
10749
10784
  },
10750
- {
10751
- "path": "windSpeedUnit",
10752
- "valueType": "string"
10753
- },
10754
10785
  {
10755
10786
  "path": "windBearing",
10756
10787
  "valueType": "number"
@@ -10800,7 +10831,7 @@ object({
10800
10831
  params: record(string(), union([string(), number()])).optional()
10801
10832
  });
10802
10833
  //#endregion
10803
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10834
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10804
10835
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10805
10836
  DeviceType["Camera"] = "camera";
10806
10837
  DeviceType["Hub"] = "hub";
@@ -10894,8 +10925,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10894
10925
  DeviceType["PetFeeder"] = "pet-feeder";
10895
10926
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10896
10927
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10897
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10928
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10929
+ * A plug-built dishwasher carries running-state and power instead. */
10898
10930
  DeviceType["Dishwasher"] = "dishwasher";
10931
+ /** Washing machine. A plug-built one is running while its power stays
10932
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10933
+ DeviceType["Washer"] = "washer";
10934
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10935
+ DeviceType["Dryer"] = "dryer";
10899
10936
  return DeviceType;
10900
10937
  }({});
10901
10938
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -11143,7 +11180,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11143
11180
  return DeviceRole;
11144
11181
  }({});
11145
11182
  //#endregion
11146
- //#region ../types/dist/composition-kEl9mevu.mjs
11183
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
11147
11184
  /**
11148
11185
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11149
11186
  *
@@ -11458,6 +11495,13 @@ var table = {
11458
11495
  apply: () => {
11459
11496
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11460
11497
  }
11498
+ },
11499
+ schmitt: {
11500
+ minArgs: 3,
11501
+ maxArgs: 5,
11502
+ apply: () => {
11503
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11504
+ }
11461
11505
  }
11462
11506
  };
11463
11507
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11898,15 +11942,232 @@ function validateExpressionSource(src) {
11898
11942
  }
11899
11943
  return null;
11900
11944
  }
11901
- /** A composed device's stableId is this prefix plus the block id. */
11902
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11903
- /** Longest snippet a `code` source may carry (slice 4). */
11904
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11945
+ /**
11946
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11947
+ *
11948
+ * Standby of the control board is under 5 W; a motor or a heater is far
11949
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11950
+ * a cycle, so running starts only after 30 s above the line.
11951
+ *
11952
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11953
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11954
+ * report "finished" in the middle of the program, so the stop dwell is
11955
+ * 10 minutes on every type. The number is stored per type so one machine
11956
+ * can move later without moving the others.
11957
+ */
11958
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11959
+ var APPLIANCE_KINDS = [
11960
+ "washer",
11961
+ "dryer",
11962
+ "dishwasher"
11963
+ ];
11964
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11965
+ /**
11966
+ * Statistics and integrals over a history read (D944).
11967
+ *
11968
+ * Pure: the same points always give the same answer, so a restart recomputes
11969
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11970
+ * never the mean of the samples. A hole with no held value is not integrated
11971
+ * as zero. A truncated read is not published.
11972
+ */
11973
+ var STATISTIC_STATS = [
11974
+ "min",
11975
+ "max",
11976
+ "mean",
11977
+ "sum",
11978
+ "count",
11979
+ "change"
11980
+ ];
11981
+ /**
11982
+ * The range a statistics or integral helper asks history for (D944).
11983
+ *
11984
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11985
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11986
+ * is strictly longer than the longest that range can be. Forever qualifies.
11987
+ * Equal to a tier's retention is a refusal, not a short answer.
11988
+ */
11989
+ var MINUTE_MS = 6e4;
11990
+ var HOUR_MS = 60 * MINUTE_MS;
11991
+ var DAY_MS = 24 * HOUR_MS;
11992
+ var ROLLING_SPANS = [
11993
+ "1h",
11994
+ "24h",
11995
+ "2d",
11996
+ "7d",
11997
+ "30d",
11998
+ "custom"
11999
+ ];
12000
+ var CALENDAR_PERIODS = [
12001
+ "today",
12002
+ "yesterday",
12003
+ "this-week",
12004
+ "this-month"
12005
+ ];
12006
+ var RESET_PERIODS = [
12007
+ "never",
12008
+ "daily",
12009
+ "weekly",
12010
+ "monthly"
12011
+ ];
12012
+ 5 * MINUTE_MS;
12013
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
12014
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
12015
+ var rollingWindowObject = object({
12016
+ kind: literal("rolling"),
12017
+ span: _enum(ROLLING_SPANS),
12018
+ customMinutes: number().int().min(5).optional()
12019
+ }).check((payload) => {
12020
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
12021
+ code: "custom",
12022
+ message: "a custom range needs a duration in minutes",
12023
+ path: ["customMinutes"],
12024
+ input: payload.value
12025
+ });
12026
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
12027
+ code: "custom",
12028
+ message: "only a custom range carries a duration",
12029
+ path: ["customMinutes"],
12030
+ input: payload.value
12031
+ });
12032
+ });
12033
+ var RollingWindowSchema = rollingWindowObject;
12034
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
12035
+ kind: literal("calendar"),
12036
+ period: _enum(CALENDAR_PERIODS)
12037
+ })]);
12038
+ /**
12039
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
12040
+ * id. Split out so helper schemas can name it without a cycle through
12041
+ * `composition.ts` (D659).
12042
+ */
11905
12043
  var CompositionSourceRefSchema = object({
11906
12044
  /** The owning addon; `stableId` is unique only within it. */
11907
12045
  addonId: string().min(1),
11908
12046
  stableId: string().min(1)
11909
12047
  });
12048
+ var GROUP_TEMPLATE_IDS = [
12049
+ "temperature",
12050
+ "humidity",
12051
+ "pressure",
12052
+ "illuminance",
12053
+ "power",
12054
+ "energy",
12055
+ "battery",
12056
+ "numeric",
12057
+ "motion",
12058
+ "contact",
12059
+ "leak",
12060
+ "smoke",
12061
+ "occupancy",
12062
+ "binary",
12063
+ "switch",
12064
+ "light",
12065
+ "fan",
12066
+ "cover",
12067
+ "valve",
12068
+ "lock",
12069
+ "media-player",
12070
+ "climate"
12071
+ ];
12072
+ var NUMERIC_AGGREGATES = [
12073
+ "mean",
12074
+ "median",
12075
+ "min",
12076
+ "max",
12077
+ "sum"
12078
+ ];
12079
+ var BINARY_AGGREGATES = ["any", "all"];
12080
+ /** How an actuator group decides it is on. Unknown members never count as off. */
12081
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
12082
+ var AGGREGATE_REDUCERS = [
12083
+ "mean",
12084
+ "median",
12085
+ "min",
12086
+ "max",
12087
+ "sum",
12088
+ "any",
12089
+ "all"
12090
+ ];
12091
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
12092
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
12093
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
12094
+ /** One member. The field is stored so occupancy can name motion or binary. */
12095
+ var HelperMemberSchema = object({
12096
+ source: CompositionSourceRefSchema,
12097
+ cap: string().min(1),
12098
+ fieldPath: string().min(1)
12099
+ });
12100
+ var HelperSpecSchema = discriminatedUnion("kind", [
12101
+ object({
12102
+ kind: literal("group"),
12103
+ templateId: GroupTemplateIdSchema,
12104
+ aggregate: AggregateReducerSchema,
12105
+ members: array(HelperMemberSchema).min(1).max(64),
12106
+ /** Actuator groups only. Readings leave this unset. */
12107
+ active: ActiveCriterionSchema.optional()
12108
+ }),
12109
+ object({
12110
+ kind: literal("appliance"),
12111
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
12112
+ appliance: _enum(APPLIANCE_KINDS),
12113
+ thresholdWatts: number().positive(),
12114
+ startDwellMs: number().int().nonnegative(),
12115
+ stopDwellMs: number().int().nonnegative(),
12116
+ member: HelperMemberSchema
12117
+ }),
12118
+ object({
12119
+ kind: literal("statistic"),
12120
+ stat: _enum(STATISTIC_STATS),
12121
+ window: StatisticWindowSchema,
12122
+ source: HelperMemberSchema
12123
+ }),
12124
+ object({
12125
+ kind: literal("integral"),
12126
+ reset: _enum(RESET_PERIODS),
12127
+ output: _enum(["energy", "meter"]),
12128
+ source: HelperMemberSchema
12129
+ }),
12130
+ object({
12131
+ kind: literal("threshold"),
12132
+ source: HelperMemberSchema,
12133
+ high: number(),
12134
+ low: number(),
12135
+ onForMs: number().int().nonnegative(),
12136
+ offForMs: number().int().nonnegative()
12137
+ }),
12138
+ object({
12139
+ kind: literal("arithmetic"),
12140
+ op: _enum([
12141
+ "sum",
12142
+ "difference",
12143
+ "ratio"
12144
+ ]),
12145
+ inputs: array(HelperMemberSchema).min(2).max(64)
12146
+ }),
12147
+ object({
12148
+ kind: literal("expression"),
12149
+ expr: string().min(1),
12150
+ inputs: array(object({
12151
+ name: string().min(1),
12152
+ member: HelperMemberSchema
12153
+ })).min(1).max(64),
12154
+ output: _enum(["number", "boolean"])
12155
+ }),
12156
+ object({
12157
+ kind: literal("derivative"),
12158
+ window: RollingWindowSchema,
12159
+ per: _enum([
12160
+ "second",
12161
+ "minute",
12162
+ "hour"
12163
+ ]),
12164
+ source: HelperMemberSchema
12165
+ })
12166
+ ]);
12167
+ /** A composed device's stableId is this prefix plus the block id. */
12168
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12169
+ /** Longest snippet a `code` source may carry (slice 4). */
12170
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11910
12171
  /** One field of one capability of one source device. */
11911
12172
  var CompositionFieldReadSchema = object({
11912
12173
  source: CompositionSourceRefSchema,
@@ -11950,7 +12211,59 @@ var CompositionCodeSourceSchema = object({
11950
12211
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11951
12212
  CompositionFromSourceSchema,
11952
12213
  CompositionExpressionSourceSchema,
11953
- CompositionCodeSourceSchema
12214
+ CompositionCodeSourceSchema,
12215
+ object({
12216
+ kind: literal("aggregate"),
12217
+ reducer: _enum([
12218
+ "mean",
12219
+ "median",
12220
+ "min",
12221
+ "max",
12222
+ "sum",
12223
+ "any",
12224
+ "all"
12225
+ ]),
12226
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12227
+ /**
12228
+ * Set when the reads are an enum state, not a yes/no: the listed states
12229
+ * count as active (a cover is `open`). A state outside the list is inactive;
12230
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
12231
+ */
12232
+ activeWhen: array(string().min(1)).min(1).optional(),
12233
+ /** `value` is the reading; the others are the member counts. */
12234
+ publish: _enum([
12235
+ "value",
12236
+ "included",
12237
+ "excluded",
12238
+ "total"
12239
+ ]),
12240
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12241
+ empty: _enum([
12242
+ "null",
12243
+ "omit",
12244
+ "offline"
12245
+ ])
12246
+ }),
12247
+ object({
12248
+ kind: literal("series"),
12249
+ read: CompositionFieldReadSchema,
12250
+ /** `value` is the statistic or the integral. The others are the gap counts. */
12251
+ publish: _enum([
12252
+ "value",
12253
+ "included",
12254
+ "excluded",
12255
+ "total"
12256
+ ]),
12257
+ /**
12258
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
12259
+ * device unseeded until history answers, so a restart cannot publish 0.
12260
+ */
12261
+ empty: _enum([
12262
+ "null",
12263
+ "omit",
12264
+ "offline"
12265
+ ])
12266
+ })
11954
12267
  ]);
11955
12268
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11956
12269
  kind: literal("forward"),
@@ -11996,7 +12309,13 @@ var CompositionSchema = object({
11996
12309
  kind: literal("existing"),
11997
12310
  device: CompositionSourceRefSchema
11998
12311
  })]),
11999
- features: array(CompositionFeatureSchema).min(1).max(16)
12312
+ features: array(CompositionFeatureSchema).min(1).max(16),
12313
+ /**
12314
+ * Present when the block is a helper template. Absent on a free
12315
+ * composition (the room example, device 4603). The editor reads this,
12316
+ * never the compiled features.
12317
+ */
12318
+ helper: HelperSpecSchema.optional()
12000
12319
  }).superRefine((composition, ctx) => {
12001
12320
  const seen = /* @__PURE__ */ new Set();
12002
12321
  composition.features.forEach((feature, index) => {
@@ -17103,7 +17422,22 @@ var BrokerProviderInfoSchema = object({
17103
17422
  */
17104
17423
  interactiveConnectPath: string().min(1).optional(),
17105
17424
  /** Relative hub path to disconnect the linked account (same popup pattern). */
17106
- interactiveDisconnectPath: string().min(1).optional()
17425
+ interactiveDisconnectPath: string().min(1).optional(),
17426
+ /**
17427
+ * The kind signs in through a server-held interactive session instead of
17428
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17429
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17430
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17431
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17432
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17433
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17434
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17435
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17436
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17437
+ * offers it as the default path; the fields the session fills are
17438
+ * marked `filledBySignIn` in the kind's settings schema.
17439
+ */
17440
+ signIn: object({ basePath: string().min(1) }).optional()
17107
17441
  }))
17108
17442
  });
17109
17443
  var ListInputSchema = object({
@@ -18147,7 +18481,12 @@ var CompositionProblemSchema = object({
18147
18481
  "item-field-reserved",
18148
18482
  "target-missing",
18149
18483
  "target-unreadable",
18150
- "not-replaceable"
18484
+ "not-replaceable",
18485
+ "native-capability-replace",
18486
+ "mixed-quantity",
18487
+ "helper-mismatch",
18488
+ "member-constraint",
18489
+ "retention"
18151
18490
  ]),
18152
18491
  path: string(),
18153
18492
  message: string()
@@ -18168,7 +18507,8 @@ var CompositionUnavailableModeSchema = _enum([
18168
18507
  "null",
18169
18508
  "offline",
18170
18509
  "unknown",
18171
- "release"
18510
+ "release",
18511
+ "omit"
18172
18512
  ]);
18173
18513
  /**
18174
18514
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18205,9 +18545,11 @@ var CompositionFieldPlanSchema = object({
18205
18545
  reads: array(CompositionFieldReadSchema)
18206
18546
  });
18207
18547
  /**
18208
- * What a feature does to its target (D663): `new` builds a capability on a new
18209
- * device; on an existing device it `add`s a capability the device lacks, or
18210
- * `replace`s named fields of one it has natively.
18548
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18549
+ * new device; on an existing device it `add`s a capability the device lacks.
18550
+ * Replacing a capability the device already serves is refused
18551
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18552
+ * wire for a claim the hub already holds.
18211
18553
  */
18212
18554
  var CompositionFeatureModeSchema = _enum([
18213
18555
  "new",
@@ -18378,8 +18720,392 @@ Object.freeze({
18378
18720
  latchedAt: {
18379
18721
  kind: "fixed",
18380
18722
  result: ["number", "null"]
18723
+ },
18724
+ schmitt: {
18725
+ kind: "fixed",
18726
+ result: ["boolean", "null"]
18381
18727
  }
18382
18728
  });
18729
+ /**
18730
+ * One catalog row per base reading a group helper can build (D944 §3).
18731
+ * Data, not a form default written into the block.
18732
+ */
18733
+ var NOW$2 = {
18734
+ fieldPath: "lastFetchedAt",
18735
+ expr: "now"
18736
+ };
18737
+ var CHANGED = {
18738
+ fieldPath: "lastChangedAt",
18739
+ expr: "now"
18740
+ };
18741
+ function numeric(input) {
18742
+ return {
18743
+ ...input,
18744
+ family: "numeric",
18745
+ deviceType: DeviceType.Sensor,
18746
+ memberDeviceType: DeviceType.Sensor,
18747
+ memberDeviceTypes: [DeviceType.Sensor],
18748
+ activeWhen: void 0,
18749
+ commandCaps: [],
18750
+ baseUnit: baseUnitOf(input.quantity),
18751
+ aggregates: NUMERIC_AGGREGATES,
18752
+ accept: [{
18753
+ cap: input.outputCap,
18754
+ fieldPath: input.outputField
18755
+ }]
18756
+ };
18757
+ }
18758
+ function binary(input) {
18759
+ return {
18760
+ ...input,
18761
+ family: "binary",
18762
+ deviceType: DeviceType.Sensor,
18763
+ memberDeviceType: DeviceType.Sensor,
18764
+ memberDeviceTypes: [DeviceType.Sensor],
18765
+ activeWhen: void 0,
18766
+ commandCaps: [],
18767
+ quantity: "none",
18768
+ baseUnit: void 0,
18769
+ aggregates: BINARY_AGGREGATES,
18770
+ empty: "offline"
18771
+ };
18772
+ }
18773
+ var GROUP_TEMPLATES = [
18774
+ numeric({
18775
+ id: "temperature",
18776
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18777
+ role: DeviceRole.TemperatureSensor,
18778
+ outputCap: "temperature-sensor",
18779
+ outputField: "celsius",
18780
+ quantity: "temperature",
18781
+ defaultAggregate: "mean",
18782
+ empty: "null",
18783
+ companions: [NOW$2]
18784
+ }),
18785
+ numeric({
18786
+ id: "humidity",
18787
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18788
+ role: DeviceRole.HumiditySensor,
18789
+ outputCap: "humidity-sensor",
18790
+ outputField: "percent",
18791
+ quantity: "humidity",
18792
+ defaultAggregate: "mean",
18793
+ empty: "null",
18794
+ companions: [NOW$2]
18795
+ }),
18796
+ numeric({
18797
+ id: "pressure",
18798
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18799
+ role: DeviceRole.PressureSensor,
18800
+ outputCap: "pressure-sensor",
18801
+ outputField: "hpa",
18802
+ quantity: "pressure",
18803
+ defaultAggregate: "mean",
18804
+ empty: "null",
18805
+ companions: [NOW$2]
18806
+ }),
18807
+ numeric({
18808
+ id: "illuminance",
18809
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18810
+ role: DeviceRole.AmbientLightSensor,
18811
+ outputCap: "ambient-light-sensor",
18812
+ outputField: "lux",
18813
+ quantity: "illuminance",
18814
+ defaultAggregate: "mean",
18815
+ empty: "null",
18816
+ companions: [NOW$2]
18817
+ }),
18818
+ numeric({
18819
+ id: "power",
18820
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18821
+ role: DeviceRole.PowerSensor,
18822
+ outputCap: "power-meter",
18823
+ outputField: "watts",
18824
+ quantity: "power",
18825
+ defaultAggregate: "sum",
18826
+ empty: "omit",
18827
+ companions: [NOW$2]
18828
+ }),
18829
+ numeric({
18830
+ id: "energy",
18831
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18832
+ role: DeviceRole.EnergySensor,
18833
+ outputCap: "power-meter",
18834
+ outputField: "kwhTotal",
18835
+ quantity: "energy",
18836
+ defaultAggregate: "sum",
18837
+ empty: "omit",
18838
+ companions: [NOW$2]
18839
+ }),
18840
+ numeric({
18841
+ id: "battery",
18842
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18843
+ role: DeviceRole.BatterySensor,
18844
+ outputCap: "battery",
18845
+ outputField: "percentage",
18846
+ quantity: "percentage",
18847
+ defaultAggregate: "mean",
18848
+ empty: "null",
18849
+ companions: [
18850
+ {
18851
+ fieldPath: "charging",
18852
+ expr: "\"unknown\""
18853
+ },
18854
+ {
18855
+ fieldPath: "sleeping",
18856
+ expr: "false"
18857
+ },
18858
+ {
18859
+ fieldPath: "lastUpdated",
18860
+ expr: "now"
18861
+ }
18862
+ ]
18863
+ }),
18864
+ numeric({
18865
+ id: "numeric",
18866
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18867
+ role: DeviceRole.NumericSensor,
18868
+ outputCap: "numeric-sensor",
18869
+ outputField: "value",
18870
+ quantity: "device",
18871
+ defaultAggregate: "mean",
18872
+ empty: "null",
18873
+ companions: [NOW$2]
18874
+ }),
18875
+ binary({
18876
+ id: "motion",
18877
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18878
+ role: DeviceRole.MotionSensor,
18879
+ outputCap: "motion",
18880
+ outputField: "detected",
18881
+ defaultAggregate: "any",
18882
+ accept: [{
18883
+ cap: "motion",
18884
+ fieldPath: "detected"
18885
+ }],
18886
+ companions: [{
18887
+ fieldPath: "lastDetectedAt",
18888
+ expr: "null"
18889
+ }, {
18890
+ fieldPath: "autoClearAfterMs",
18891
+ expr: "null"
18892
+ }]
18893
+ }),
18894
+ binary({
18895
+ id: "contact",
18896
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18897
+ role: DeviceRole.ContactSensor,
18898
+ outputCap: "contact",
18899
+ outputField: "entryOpen",
18900
+ defaultAggregate: "any",
18901
+ accept: [{
18902
+ cap: "contact",
18903
+ fieldPath: "entryOpen"
18904
+ }],
18905
+ companions: [CHANGED]
18906
+ }),
18907
+ binary({
18908
+ id: "leak",
18909
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18910
+ role: DeviceRole.LeakSensor,
18911
+ outputCap: "flood",
18912
+ outputField: "flooded",
18913
+ defaultAggregate: "any",
18914
+ accept: [{
18915
+ cap: "flood",
18916
+ fieldPath: "flooded"
18917
+ }],
18918
+ companions: [CHANGED]
18919
+ }),
18920
+ binary({
18921
+ id: "smoke",
18922
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18923
+ role: DeviceRole.SmokeSensor,
18924
+ outputCap: "smoke",
18925
+ outputField: "detected",
18926
+ defaultAggregate: "any",
18927
+ accept: [{
18928
+ cap: "smoke",
18929
+ fieldPath: "detected"
18930
+ }],
18931
+ companions: [CHANGED]
18932
+ }),
18933
+ binary({
18934
+ id: "occupancy",
18935
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18936
+ role: DeviceRole.BinarySensor,
18937
+ outputCap: "binary",
18938
+ outputField: "on",
18939
+ defaultAggregate: "any",
18940
+ accept: [{
18941
+ cap: "motion",
18942
+ fieldPath: "detected"
18943
+ }, {
18944
+ cap: "binary",
18945
+ fieldPath: "on"
18946
+ }],
18947
+ companions: [CHANGED]
18948
+ }),
18949
+ binary({
18950
+ id: "binary",
18951
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18952
+ role: DeviceRole.BinarySensor,
18953
+ outputCap: "binary",
18954
+ outputField: "on",
18955
+ defaultAggregate: "any",
18956
+ accept: [{
18957
+ cap: "binary",
18958
+ fieldPath: "on"
18959
+ }],
18960
+ companions: [CHANGED]
18961
+ }),
18962
+ actuator({
18963
+ id: "switch",
18964
+ 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.",
18965
+ deviceType: DeviceType.Switch,
18966
+ outputCap: "switch",
18967
+ outputField: "on",
18968
+ member: {
18969
+ cap: "switch",
18970
+ fieldPath: "on"
18971
+ },
18972
+ commandCaps: ["switch"]
18973
+ }),
18974
+ actuator({
18975
+ id: "light",
18976
+ 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.",
18977
+ deviceType: DeviceType.Light,
18978
+ outputCap: "switch",
18979
+ outputField: "on",
18980
+ member: {
18981
+ cap: "switch",
18982
+ fieldPath: "on"
18983
+ },
18984
+ commandCaps: [
18985
+ "switch",
18986
+ "brightness",
18987
+ "color"
18988
+ ]
18989
+ }),
18990
+ actuator({
18991
+ id: "fan",
18992
+ 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.",
18993
+ deviceType: DeviceType.Fan,
18994
+ outputCap: "switch",
18995
+ outputField: "on",
18996
+ member: {
18997
+ cap: "switch",
18998
+ fieldPath: "on"
18999
+ },
19000
+ commandCaps: ["switch", "fan-control"]
19001
+ }),
19002
+ actuator({
19003
+ id: "cover",
19004
+ 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.",
19005
+ deviceType: DeviceType.Cover,
19006
+ outputCap: "group-active",
19007
+ outputField: "active",
19008
+ member: {
19009
+ cap: "cover",
19010
+ fieldPath: "state"
19011
+ },
19012
+ activeWhen: [
19013
+ "open",
19014
+ "opening",
19015
+ "stopped"
19016
+ ],
19017
+ commandCaps: ["cover"]
19018
+ }),
19019
+ actuator({
19020
+ id: "valve",
19021
+ 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.",
19022
+ deviceType: DeviceType.Valve,
19023
+ outputCap: "group-active",
19024
+ outputField: "active",
19025
+ member: {
19026
+ cap: "valve",
19027
+ fieldPath: "state"
19028
+ },
19029
+ activeWhen: [
19030
+ "open",
19031
+ "opening",
19032
+ "stopped"
19033
+ ],
19034
+ commandCaps: ["valve"]
19035
+ }),
19036
+ actuator({
19037
+ id: "lock",
19038
+ 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.",
19039
+ deviceType: DeviceType.Lock,
19040
+ outputCap: "group-active",
19041
+ outputField: "active",
19042
+ member: {
19043
+ cap: "lock-control",
19044
+ fieldPath: "state"
19045
+ },
19046
+ activeWhen: ["locked"],
19047
+ commandCaps: ["lock-control"]
19048
+ }),
19049
+ actuator({
19050
+ id: "media-player",
19051
+ 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.",
19052
+ deviceType: DeviceType.MediaPlayer,
19053
+ outputCap: "group-active",
19054
+ outputField: "active",
19055
+ member: {
19056
+ cap: "media-player",
19057
+ fieldPath: "state"
19058
+ },
19059
+ activeWhen: ["playing", "buffering"],
19060
+ commandCaps: ["media-player"]
19061
+ }),
19062
+ actuator({
19063
+ id: "climate",
19064
+ 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.",
19065
+ deviceType: DeviceType.Thermostat,
19066
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
19067
+ outputCap: "group-active",
19068
+ outputField: "active",
19069
+ member: {
19070
+ cap: "climate-control",
19071
+ fieldPath: "mode"
19072
+ },
19073
+ activeWhen: [
19074
+ "heat",
19075
+ "cool",
19076
+ "auto",
19077
+ "heat_cool",
19078
+ "fan_only",
19079
+ "dry"
19080
+ ],
19081
+ commandCaps: ["climate-control"]
19082
+ })
19083
+ ];
19084
+ function actuator(input) {
19085
+ return {
19086
+ id: input.id,
19087
+ summary: input.summary,
19088
+ deviceType: input.deviceType,
19089
+ outputCap: input.outputCap,
19090
+ outputField: input.outputField,
19091
+ commandCaps: input.commandCaps,
19092
+ family: "actuator",
19093
+ role: void 0,
19094
+ memberDeviceType: input.deviceType,
19095
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
19096
+ activeWhen: input.activeWhen,
19097
+ quantity: "none",
19098
+ baseUnit: void 0,
19099
+ aggregates: BINARY_AGGREGATES,
19100
+ defaultAggregate: "any",
19101
+ empty: "offline",
19102
+ accept: [input.member],
19103
+ companions: [CHANGED]
19104
+ };
19105
+ }
19106
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19107
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19108
+ DeviceType.Camera;
18383
19109
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18384
19110
  /**
18385
19111
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20093,12 +20819,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20093
20819
  * providers detect renames by matching `uniqueId` and patch `id`
20094
20820
  * in-place so dispatch keeps working.
20095
20821
  *
20096
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20097
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20098
- * state push and are the single source of truth. The `device_class` tag is
20099
- * an HA-specific input that is consumed at the discovery boundary
20100
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20101
- * stored after that point.
20822
+ * **Display unit and precision are NOT stored here.** They are a read-time
20823
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20824
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20825
+ * neither is repeated on each sample. The `device_class` tag is an
20826
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20827
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20102
20828
  *
20103
20829
  * The `raw` escape hatch carries provider-specific attributes that don't
20104
20830
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20112,6 +20838,14 @@ var SourceInfoSchema = object({
20112
20838
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20113
20839
  * Used to detect rename when `id` changes. */
20114
20840
  uniqueId: string().optional(),
20841
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20842
+ quantity: ReadingQuantitySchema.optional(),
20843
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20844
+ dateTimeShape: _enum([
20845
+ "date",
20846
+ "time",
20847
+ "datetime"
20848
+ ]).optional(),
20115
20849
  /** Provider-specific extras that don't fit the structured slots.
20116
20850
  * Whitelist what goes here per provider — do NOT dump entire HA
20117
20851
  * attribute blobs (would explode the meta JSON column). */
@@ -41024,7 +41758,8 @@ method(object({
41024
41758
  "enum"
41025
41759
  ]),
41026
41760
  enumValues: array(string()).optional(),
41027
- item: boolean().optional()
41761
+ item: boolean().optional(),
41762
+ quantity: ReadingQuantitySchema.optional()
41028
41763
  })).readonly(),
41029
41764
  itemArray: object({
41030
41765
  path: string(),
@@ -43404,7 +44139,7 @@ var NetworkAccessStatusSchema = object({
43404
44139
  });
43405
44140
  /**
43406
44141
  * Optional, richer endpoint shape returned by providers that expose
43407
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44142
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43408
44143
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43409
44144
  * the originating provider config (mode + sourcePort) so the
43410
44145
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44843,8 +45578,8 @@ var MaskPolygonShapeSchema = object({
44843
45578
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44844
45579
  var MaskGridShapeSchema = object({
44845
45580
  kind: literal("grid"),
44846
- gridWidth: number(),
44847
- gridHeight: number(),
45581
+ gridWidth: number().meta(readingMeta("none")),
45582
+ gridHeight: number().meta(readingMeta("none")),
44848
45583
  cells: array(boolean())
44849
45584
  });
44850
45585
  discriminatedUnion("kind", [
@@ -44956,6 +45691,8 @@ var NcSystemEventKindSchema = _enum([
44956
45691
  "detection-blind",
44957
45692
  "motion-noisy",
44958
45693
  "motion-calm",
45694
+ "package-llm-silent",
45695
+ "package-llm-answered",
44959
45696
  "recording-stalled",
44960
45697
  "recording-healthy",
44961
45698
  "recording-degraded",
@@ -46045,7 +46782,20 @@ var NcRuleInputSchema = object({
46045
46782
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46046
46783
  * path does not run Zod defaults.
46047
46784
  */
46048
- groupingSec: number().int().min(5).max(60).optional()
46785
+ groupingSec: number().int().min(5).max(60).optional(),
46786
+ /**
46787
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46788
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46789
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46790
+ * same mark the Library writes, and records whose notification it was and
46791
+ * when. A channel with no buttons gets the same two choices as links.
46792
+ *
46793
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46794
+ * cap path): absent and `false` both mean off, which is what every rule
46795
+ * authored before this field meant. An editor turns it off by writing
46796
+ * `false`, so it needs no `clear` key.
46797
+ */
46798
+ askVerdict: boolean().optional()
46049
46799
  });
46050
46800
  /**
46051
46801
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -55970,6 +56720,28 @@ var PackageVersionInfoSchema = object({
55970
56720
  deprecated: string().optional(),
55971
56721
  distTags: array(string()).readonly()
55972
56722
  });
56723
+ /**
56724
+ * One published document. `ok` is markdown from the registry. `missing` means
56725
+ * the registry answered and the file was not there. `error` is a registry or
56726
+ * tarball failure — never an empty string standing in for either.
56727
+ */
56728
+ var PackageDocBodySchema = discriminatedUnion("status", [
56729
+ object({
56730
+ status: literal("ok"),
56731
+ markdown: string()
56732
+ }),
56733
+ object({ status: literal("missing") }),
56734
+ object({
56735
+ status: literal("error"),
56736
+ message: string()
56737
+ })
56738
+ ]);
56739
+ var PackageDocsSchema = object({
56740
+ name: string(),
56741
+ version: string(),
56742
+ readme: PackageDocBodySchema,
56743
+ changelog: PackageDocBodySchema
56744
+ });
55973
56745
  var SearchResultSchema = object({
55974
56746
  name: string(),
55975
56747
  version: string(),
@@ -56188,7 +56960,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56188
56960
  }), object({ success: literal(true) }), {
56189
56961
  kind: "mutation",
56190
56962
  auth: "admin"
56191
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
56963
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
56964
+ name: string().min(1),
56965
+ version: string().min(1).optional()
56966
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56192
56967
  kind: "mutation",
56193
56968
  auth: "admin"
56194
56969
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56238,46 +57013,27 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56238
57013
  }), LogStreamEntrySchema, { kind: "subscription" });
56239
57014
  object({
56240
57015
  /** Carbon dioxide concentration in ppm. */
56241
- co2Ppm: number().min(0).optional(),
57016
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56242
57017
  /** Total volatile organic compounds in ppb. */
56243
- vocPpb: number().min(0).optional(),
57018
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56244
57019
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56245
- pm25: number().min(0).optional(),
57020
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56246
57021
  /** Particulate matter ≤ 10 μm in µg/m³. */
56247
- pm10: number().min(0).optional(),
57022
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56248
57023
  /** Composite AQI value (typically 0..500). */
56249
- aqi: number().optional(),
57024
+ aqi: number().optional().meta(readingMeta("index")),
56250
57025
  /** Ms epoch when the slice was last updated. */
56251
- lastFetchedAt: number(),
56252
- /** Live display unit of the single metric this slice carries (e.g. HA
56253
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56254
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56255
- * per slice is unambiguous. */
56256
- unit: string().optional(),
56257
- /** Suggested decimal places for numeric display.
56258
- * Populated live from the upstream source when provided (e.g. HA
56259
- * `attributes.suggested_display_precision`). Falls back to
56260
- * auto-formatting when absent. */
56261
- precision: number().int().min(0).max(10).optional()
57026
+ lastFetchedAt: number()
56262
57027
  });
56263
57028
  DeviceType.Sensor;
56264
57029
  object({
56265
- /** Current illuminance in lux (lx). */
56266
- lux: number().min(0).meta({
56267
- unit: "lx",
57030
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57031
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57032
+ precision: 0,
56268
57033
  label: "Illuminance"
56269
- }),
57034
+ })),
56270
57035
  /** Ms epoch when the slice was last updated. */
56271
- lastFetchedAt: number(),
56272
- /** Live display unit from the upstream source (e.g. HA
56273
- * `attributes.unit_of_measurement`). The UI prefers this over the
56274
- * role's canonical unit. Absent → fall back to the canonical unit. */
56275
- unit: string().optional(),
56276
- /** Suggested decimal places for numeric display.
56277
- * Populated live from the upstream source when provided (e.g. HA
56278
- * `attributes.suggested_display_precision`). Falls back to
56279
- * auto-formatting when absent. */
56280
- precision: number().int().min(0).max(10).optional()
57036
+ lastFetchedAt: number()
56281
57037
  });
56282
57038
  DeviceType.Sensor;
56283
57039
  /**
@@ -56422,10 +57178,7 @@ var BatteryStatusSchema = object({
56422
57178
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56423
57179
  * declines to band a non-finite reading, which is the correct shape.
56424
57180
  */
56425
- percentage: number().min(0).max(100).nullable().meta({
56426
- unit: "%",
56427
- label: "Charge"
56428
- }),
57181
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56429
57182
  /**
56430
57183
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56431
57184
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56452,7 +57205,14 @@ var BatteryStatusSchema = object({
56452
57205
  */
56453
57206
  sleeping: boolean().meta({ label: "Sleeping" }),
56454
57207
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56455
- lastUpdated: number(),
57208
+ lastUpdated: number().meta(readingMeta("none")),
57209
+ /**
57210
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57211
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57212
+ * that only report charge. A second reading on this cap, not a second
57213
+ * `numeric-sensor` on the same device.
57214
+ */
57215
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56456
57216
  /**
56457
57217
  * Ms epoch of the last time the device PROVED it was reachable — a
56458
57218
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56492,7 +57252,7 @@ var BatteryStatusSchema = object({
56492
57252
  */
56493
57253
  binary: boolean().optional().meta({ label: "Binary indicator" })
56494
57254
  });
56495
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat;
57255
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter;
56496
57256
  method(object({
56497
57257
  deviceId: number(),
56498
57258
  /** Bound on the wait. Sensible range 3000–10000ms. */
@@ -56512,7 +57272,7 @@ object({
56512
57272
  DeviceType.Sensor;
56513
57273
  object({
56514
57274
  /** Current level as 0..100 inclusive. Firmware-reported. */
56515
- percentage: number().min(0).max(100),
57275
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56516
57276
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56517
57277
  lastChangedAt: number()
56518
57278
  });
@@ -56897,21 +57657,21 @@ object({
56897
57657
  /** Single setpoint in Celsius. Used by single-target modes
56898
57658
  * (heat / cool / dry). Null when the device is in a range mode
56899
57659
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
56900
- target: number().nullable(),
57660
+ target: number().nullable().meta(readingMeta("temperature")),
56901
57661
  /** Upper bound of the dual setpoint in Celsius. Populated only
56902
57662
  * in `heat_cool` mode. */
56903
- targetHigh: number().nullable(),
57663
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
56904
57664
  /** Lower bound of the dual setpoint in Celsius. Populated only
56905
57665
  * in `heat_cool` mode. */
56906
- targetLow: number().nullable(),
57666
+ targetLow: number().nullable().meta(readingMeta("temperature")),
56907
57667
  /** Target relative humidity (0..100). Null when the device has no
56908
57668
  * humidity control. */
56909
- targetHumidity: number().min(0).max(100).nullable(),
57669
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56910
57670
  /** Current measured temperature in Celsius. Read-only — pushed by
56911
57671
  * the device's internal sensor. */
56912
- currentTemp: number().nullable(),
57672
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
56913
57673
  /** Current measured relative humidity (0..100). Read-only. */
56914
- currentHumidity: number().min(0).max(100).nullable(),
57674
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56915
57675
  /** Vertical louver swing on/off. Null when the device has no vertical
56916
57676
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
56917
57677
  swingVertical: boolean().nullable(),
@@ -56919,11 +57679,11 @@ object({
56919
57679
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
56920
57680
  swingHorizontal: boolean().nullable(),
56921
57681
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
56922
- minTemp: number().nullable().optional(),
57682
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
56923
57683
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
56924
- maxTemp: number().nullable().optional(),
57684
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
56925
57685
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
56926
- tempStep: number().positive().nullable().optional(),
57686
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
56927
57687
  /** Ms epoch when the slice was last updated (push or command). */
56928
57688
  lastFetchedAt: number()
56929
57689
  });
@@ -57002,17 +57762,17 @@ DeviceType.Thermostat, DeviceType.Climate, method(object({
57002
57762
  * modes to expose in the picker without a slice fetch.
57003
57763
  */
57004
57764
  var RgbTripletSchema = object({
57005
- r: number().int().min(0).max(255),
57006
- g: number().int().min(0).max(255),
57007
- b: number().int().min(0).max(255)
57765
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57766
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57767
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57008
57768
  });
57009
57769
  var HsvTripletSchema = object({
57010
57770
  /** Hue in degrees, 0..360. */
57011
- h: number().min(0).max(360),
57771
+ h: number().min(0).max(360).meta(readingMeta("none")),
57012
57772
  /** Saturation as 0..100 inclusive. */
57013
- s: number().min(0).max(100),
57773
+ s: number().min(0).max(100).meta(readingMeta("none")),
57014
57774
  /** Value/brightness as 0..100 inclusive. */
57015
- v: number().min(0).max(100)
57775
+ v: number().min(0).max(100).meta(readingMeta("none"))
57016
57776
  });
57017
57777
  var ColorInputSchema = discriminatedUnion("mode", [
57018
57778
  object({
@@ -57041,7 +57801,7 @@ object({
57041
57801
  /** Populated when `mode === 'hsv'`. */
57042
57802
  hsv: HsvTripletSchema.optional(),
57043
57803
  /** Populated when `mode === 'mired'`. */
57044
- mireds: number().int().optional(),
57804
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57045
57805
  /** Ms epoch of the last operator-driven change. */
57046
57806
  lastChangedAt: number()
57047
57807
  });
@@ -57165,18 +57925,12 @@ _enum([
57165
57925
  "empty",
57166
57926
  "full"
57167
57927
  ]);
57168
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57169
- * each. */
57170
- var ConsumableRemainingUnitSchema = _enum([
57171
- "days",
57172
- "hours",
57173
- "cycles"
57174
- ]);
57175
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57176
- * days). `value` null = unknown (D393). */
57928
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57929
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57930
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57177
57931
  var ConsumableRemainingSchema = object({
57178
- value: number().min(0).nullable(),
57179
- unit: ConsumableRemainingUnitSchema
57932
+ value: number().min(0).nullable().meta(readingMeta("device")),
57933
+ kind: ConsumableRemainingKindSchema
57180
57934
  });
57181
57935
  var ConsumablesStatusSchema = object({
57182
57936
  items: array(object({
@@ -57185,7 +57939,7 @@ var ConsumablesStatusSchema = object({
57185
57939
  /** Display name. */
57186
57940
  label: string().min(1),
57187
57941
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57188
- level: number().min(0).max(100).nullable(),
57942
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57189
57943
  /** The provider's discrete verdict when known (binary mode). */
57190
57944
  status: ConsumableStatusSchema.nullable(),
57191
57945
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57202,7 +57956,7 @@ var ConsumablesStatusSchema = object({
57202
57956
  })),
57203
57957
  lastChangedAt: number()
57204
57958
  });
57205
- Object.values(DeviceType), [...ConsumableRemainingUnitSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57959
+ Object.values(DeviceType), [...ConsumableRemainingKindSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57206
57960
  Object.values(DeviceType), method(object({
57207
57961
  deviceId: number().int().nonnegative(),
57208
57962
  key: string().min(1)
@@ -57227,39 +57981,17 @@ object({
57227
57981
  /** Current value. Numeric kind → number; select/text/datetime → string.
57228
57982
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
57229
57983
  * or time-only — HA accepts all three forms). */
57230
- value: union([number(), string()]),
57984
+ value: union([number().meta(readingMeta("device")), string()]),
57231
57985
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
57232
57986
  options: array(string()),
57233
57987
  /** Ms epoch when the slice was last updated. */
57234
57988
  lastChangedAt: number(),
57235
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
57236
- * Populated live from HA `attributes.unit_of_measurement` on each push.
57237
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57238
- unit: string().optional(),
57239
- /** Minimum allowed value. Populated live from HA `attributes.min`.
57240
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57241
- min: number().optional(),
57242
- /** Maximum allowed value. Populated live from HA `attributes.max`.
57243
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57244
- max: number().optional(),
57245
- /** Step increment. Populated live from HA `attributes.step`.
57246
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57247
- step: number().optional(),
57248
- /** Suggested decimal places for numeric display. Populated live from HA
57249
- * `attributes.suggested_display_precision`, or derived from `step` when the
57250
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
57251
- * other kinds. Falls back to auto-formatting when absent. */
57252
- precision: number().int().min(0).max(10).optional(),
57253
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
57254
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
57255
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
57256
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
57257
- * `has_date`/`has_time` attributes on each push. */
57258
- format: _enum([
57259
- "date",
57260
- "time",
57261
- "datetime"
57262
- ]).optional()
57989
+ /** Minimum allowed value, in the device quantity's base unit. */
57990
+ min: number().optional().meta(readingMeta("device")),
57991
+ /** Maximum allowed value, in the device quantity's base unit. */
57992
+ max: number().optional().meta(readingMeta("device")),
57993
+ /** Step increment, in the device quantity's base unit. */
57994
+ step: number().optional().meta(readingMeta("device"))
57263
57995
  });
57264
57996
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
57265
57997
  object({
@@ -57297,9 +58029,9 @@ object({
57297
58029
  ]),
57298
58030
  /** 0 = fully closed, 100 = fully open. Null when the device has
57299
58031
  * no intermediate position surface. */
57300
- position: number().min(0).max(100).nullable(),
58032
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57301
58033
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
57302
- tiltPosition: number().min(0).max(100).nullable(),
58034
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57303
58035
  /** Ms epoch when the slice was last updated. */
57304
58036
  lastChangedAt: number()
57305
58037
  });
@@ -57357,9 +58089,9 @@ var NormalizedRangeSchema$1 = object({
57357
58089
  object({
57358
58090
  mode: DayNightModeSchema,
57359
58091
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
57360
- sensitivity: number().optional(),
58092
+ sensitivity: number().optional().meta(readingMeta("none")),
57361
58093
  /** Delay before the IR-cut filter flips, in seconds. */
57362
- switchDelaySec: number().optional(),
58094
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
57363
58095
  lastFetchedAt: number()
57364
58096
  });
57365
58097
  /**
@@ -57514,11 +58246,11 @@ object({
57514
58246
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
57515
58247
  availablePrograms: array(string()),
57516
58248
  /** Seconds remaining on the active cycle, or null when not running / not known. */
57517
- remainingSeconds: number().int().nonnegative().nullable(),
58249
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57518
58250
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
57519
- delayedStartSeconds: number().int().nonnegative().nullable(),
58251
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57520
58252
  /** Cycle completion 0..100, or null when not applicable. */
57521
- progressPercent: number().min(0).max(100).nullable(),
58253
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57522
58254
  /** True when the door is reported open; null when the door state is not known. */
57523
58255
  doorOpen: boolean().nullable(),
57524
58256
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -57542,7 +58274,7 @@ object({
57542
58274
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57543
58275
  machineCareDue: boolean().nullable(),
57544
58276
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57545
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
58277
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
57546
58278
  /** Option states: null when the appliance does not expose the option or has not said. */
57547
58279
  halfLoad: boolean().nullable(),
57548
58280
  hygienePlus: boolean().nullable(),
@@ -57557,7 +58289,7 @@ object({
57557
58289
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
57558
58290
  activeEvents: array(string()),
57559
58291
  /** Numeric error code when the appliance reports one; null otherwise. */
57560
- errorCode: number().nullable(),
58292
+ errorCode: number().nullable().meta(readingMeta("none")),
57561
58293
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
57562
58294
  errorLabel: string().nullable(),
57563
58295
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -57703,24 +58435,14 @@ object({
57703
58435
  /** Ms epoch of the last press. null = never observed since this provider started. */
57704
58436
  lastPressedAt: number().nullable(),
57705
58437
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
57706
- pressCountSinceStart: number()
58438
+ pressCountSinceStart: number().meta(readingMeta("count"))
57707
58439
  });
57708
58440
  object({
57709
58441
  deviceId: number(),
57710
58442
  timestamp: number()
57711
58443
  });
57712
58444
  DeviceType.Button, DeviceType.Camera;
57713
- /**
57714
- * Enum-state sensor — a string value picked from a finite option set.
57715
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
57716
- * states, weather conditions, contact-source channels, mode strings).
57717
- * The option set is stable across observations; it is persisted in the
57718
- * device config blob (via the control slice at adoption) and is NOT
57719
- * stored in `sourceInfo`.
57720
- */
57721
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
57722
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
57723
- var EnumSensorDateTimeFormatSchema = _enum([
58445
+ _enum([
57724
58446
  "date",
57725
58447
  "time",
57726
58448
  "datetime"
@@ -57728,12 +58450,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
57728
58450
  object({
57729
58451
  value: string(),
57730
58452
  /**
57731
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
57732
- * as a locale date/time/datetime; absent for plain enum sensors. HA
57733
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
57734
- * a time-only sensor → `'time'`.
58453
+ * Set when `value` is a timestamp rather than an enum state, so history
58454
+ * does not learn ISO strings as states. The display shape (date / time /
58455
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
57735
58456
  */
57736
- format: EnumSensorDateTimeFormatSchema.optional(),
58457
+ timestamp: boolean().optional(),
57737
58458
  /** Ms epoch when the slice was last updated. */
57738
58459
  lastFetchedAt: number()
57739
58460
  });
@@ -57752,13 +58473,13 @@ var EventFireSchema = object({
57752
58473
  deviceId: number(),
57753
58474
  eventType: string(),
57754
58475
  data: record(string(), unknown()).nullable(),
57755
- timestamp: number(),
57756
- seq: number()
58476
+ timestamp: number().meta(readingMeta("none")),
58477
+ seq: number().meta(readingMeta("count"))
57757
58478
  });
57758
58479
  object({
57759
58480
  eventTypes: array(string()),
57760
58481
  lastEvent: EventFireSchema.nullable(),
57761
- eventCountSinceStart: number()
58482
+ eventCountSinceStart: number().meta(readingMeta("count"))
57762
58483
  });
57763
58484
  DeviceType.EventEmitter;
57764
58485
  var IdentitySchema = object({
@@ -58147,10 +58868,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
58147
58868
  object({
58148
58869
  /** Active speed as 0..100 inclusive. Null when the device has no
58149
58870
  * speed surface (single-speed fan). */
58150
- percentage: number().min(0).max(100).nullable(),
58871
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58151
58872
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
58152
58873
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
58153
- percentageStep: number().positive().optional(),
58874
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
58154
58875
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
58155
58876
  * — empty string when no preset is active or supported. */
58156
58877
  preset: string(),
@@ -58232,7 +58953,7 @@ var FeatureProbeStatusSchema = object({
58232
58953
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
58233
58954
  model: string().nullable(),
58234
58955
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
58235
- channelCount: number().nullable(),
58956
+ channelCount: number().nullable().meta(readingMeta("count")),
58236
58957
  /**
58237
58958
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
58238
58959
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -58264,9 +58985,45 @@ object({
58264
58985
  lastChangedAt: number()
58265
58986
  });
58266
58987
  DeviceType.Sensor;
58988
+ object({
58989
+ /** One row per member that has been commanded. A later command replaces that member's row. */
58990
+ results: array(object({
58991
+ deviceId: number().int().nonnegative(),
58992
+ cap: string().min(1),
58993
+ method: string().min(1),
58994
+ ok: boolean(),
58995
+ error: string().optional(),
58996
+ /** Ms epoch of the attempt. */
58997
+ at: number()
58998
+ })) });
58999
+ DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
59000
+ object({
59001
+ active: boolean().meta({ label: "Active" }),
59002
+ /** Ms epoch when the slice was last updated. */
59003
+ lastChangedAt: number()
59004
+ });
59005
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Climate, DeviceType.Thermostat;
59006
+ object({
59007
+ /** Members whose current value entered the aggregate. */
59008
+ included: number().int().nonnegative().meta(readingMeta("count", {
59009
+ precision: 0,
59010
+ label: "Reporting"
59011
+ })),
59012
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
59013
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
59014
+ precision: 0,
59015
+ label: "Excluded"
59016
+ })),
59017
+ /** `included` + `excluded`. */
59018
+ total: number().int().nonnegative().meta(readingMeta("count", {
59019
+ precision: 0,
59020
+ label: "Members"
59021
+ }))
59022
+ });
59023
+ DeviceType.Sensor, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
58267
59024
  object({
58268
59025
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
58269
- position: number().min(0).max(100).nullable(),
59026
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58270
59027
  /** Ms epoch when the slice was last updated (push or poll). */
58271
59028
  lastFetchedAt: number()
58272
59029
  });
@@ -58275,9 +59032,9 @@ object({
58275
59032
  /** Whether the humidifier is currently on. */
58276
59033
  on: boolean(),
58277
59034
  /** Current measured relative humidity (0..100). Null when not reported. */
58278
- currentHumidity: number().min(0).max(100).nullable(),
59035
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58279
59036
  /** Target relative humidity (0..100). Null when no setpoint surface. */
58280
- targetHumidity: number().min(0).max(100).nullable(),
59037
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58281
59038
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
58282
59039
  * device has no mode surface. */
58283
59040
  mode: string().nullable(),
@@ -58287,9 +59044,9 @@ object({
58287
59044
  * `idle` / `off`). Null when the device doesn't report it. */
58288
59045
  action: string().nullable(),
58289
59046
  /** HA `min_humidity` attribute. Null → UI uses 0. */
58290
- minHumidity: number().nullable(),
59047
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
58291
59048
  /** HA `max_humidity` attribute. Null → UI uses 100. */
58292
- maxHumidity: number().nullable(),
59049
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
58293
59050
  /** Ms epoch when the slice was last updated. */
58294
59051
  lastChangedAt: number()
58295
59052
  });
@@ -58313,22 +59070,13 @@ DeviceType.Humidifier, method(object({
58313
59070
  auth: "protected"
58314
59071
  });
58315
59072
  object({
58316
- /** Current relative humidity, 0..100. */
58317
- percent: number().min(0).max(100).meta({
58318
- unit: "%",
59073
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
59074
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
59075
+ precision: 0,
58319
59076
  label: "Humidity"
58320
- }),
59077
+ })),
58321
59078
  /** Ms epoch when the slice was last updated. */
58322
- lastFetchedAt: number(),
58323
- /** Live display unit from the upstream source (e.g. HA
58324
- * `attributes.unit_of_measurement`). The UI prefers this over the
58325
- * role's canonical unit. Absent → fall back to the canonical unit. */
58326
- unit: string().optional(),
58327
- /** Suggested decimal places for numeric display.
58328
- * Populated live from the upstream source when provided (e.g. HA
58329
- * `attributes.suggested_display_precision`). Falls back to
58330
- * auto-formatting when absent. */
58331
- precision: number().int().min(0).max(10).optional()
59079
+ lastFetchedAt: number()
58332
59080
  });
58333
59081
  DeviceType.Sensor, DeviceType.Thermostat;
58334
59082
  object({
@@ -58336,7 +59084,7 @@ object({
58336
59084
  * entity exposes no `entity_picture` (yet). */
58337
59085
  url: string().nullable(),
58338
59086
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
58339
- lastUpdated: number().nullable()
59087
+ lastUpdated: number().nullable().meta(readingMeta("none"))
58340
59088
  });
58341
59089
  DeviceType.Image;
58342
59090
  /**
@@ -58392,19 +59140,19 @@ var NormalizedRangeSchema = object({
58392
59140
  });
58393
59141
  object({
58394
59142
  /** Normalized 0–100. */
58395
- brightness: number().optional(),
59143
+ brightness: number().optional().meta(readingMeta("none")),
58396
59144
  /** Normalized 0–100. */
58397
- contrast: number().optional(),
59145
+ contrast: number().optional().meta(readingMeta("none")),
58398
59146
  /** Normalized 0–100. */
58399
- saturation: number().optional(),
59147
+ saturation: number().optional().meta(readingMeta("none")),
58400
59148
  /** Normalized 0–100. */
58401
- sharpness: number().optional(),
59149
+ sharpness: number().optional().meta(readingMeta("none")),
58402
59150
  mirror: boolean().optional(),
58403
59151
  flip: boolean().optional(),
58404
59152
  rotate: ImageRotateSchema.optional(),
58405
59153
  whiteBalance: WhiteBalanceModeSchema.optional(),
58406
59154
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
58407
- warmth: number().optional(),
59155
+ warmth: number().optional().meta(readingMeta("none")),
58408
59156
  exposureMode: ExposureModeSchema.optional(),
58409
59157
  backlightMode: BacklightModeSchema.optional(),
58410
59158
  lastFetchedAt: number()
@@ -58649,10 +59397,10 @@ var IntercomAbilitySchema = object({
58649
59397
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
58650
59398
  codecs: array(string()),
58651
59399
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
58652
- sampleRate: number(),
59400
+ sampleRate: number().meta(readingMeta("none")),
58653
59401
  duplex: _enum(["half", "full"]),
58654
59402
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
58655
- maxBacklogMs: number()
59403
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
58656
59404
  });
58657
59405
  var IntercomStatusSchema = object({
58658
59406
  /** True while a talk session is open. */
@@ -58763,13 +59511,13 @@ object({
58763
59511
  activity: LawnMowerActivitySchema,
58764
59512
  /** 0..100 battery percentage. Null when the device has no battery
58765
59513
  * reading. */
58766
- batteryLevel: number().min(0).max(100).nullable(),
59514
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58767
59515
  /** 0..100 mowing-completion percentage of the current task, or null when no
58768
59516
  * task is active / progress is unavailable. */
58769
- progressPercent: number().min(0).max(100).nullable(),
59517
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58770
59518
  /** Current device/event code (dynamic — mostly status, sometimes an error),
58771
59519
  * or null when unknown. */
58772
- currentCode: number().nullable(),
59520
+ currentCode: number().nullable().meta(readingMeta("none")),
58773
59521
  /** Human label for {@link currentCode}, or null when undecodable. */
58774
59522
  currentCodeLabel: string().nullable(),
58775
59523
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -59179,7 +59927,14 @@ DeviceType.Lock, method(object({
59179
59927
  auth: "protected"
59180
59928
  }), method(object({
59181
59929
  deviceId: number().int().nonnegative(),
59182
- code: string().min(1).optional()
59930
+ code: string().min(1).optional(),
59931
+ /**
59932
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
59933
+ * with one call is refused unless the caller says it was confirmed. A
59934
+ * single lock ignores it. The refusal lives in the callee, so no
59935
+ * automation or API caller reaches the doors by skipping a dialog.
59936
+ */
59937
+ confirm: boolean().optional()
59183
59938
  }), _void(), {
59184
59939
  kind: "mutation",
59185
59940
  auth: "protected"
@@ -59187,6 +59942,11 @@ DeviceType.Lock, method(object({
59187
59942
  kind: "mutation",
59188
59943
  auth: "protected"
59189
59944
  });
59945
+ object({
59946
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
59947
+ lastFetchedAt: number()
59948
+ });
59949
+ DeviceType.Sensor;
59190
59950
  /**
59191
59951
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
59192
59952
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
@@ -59231,7 +59991,7 @@ object({
59231
59991
  state: MediaPlayerStateSchema,
59232
59992
  /** Volume as 0..100 inclusive. Null when the device has no volume
59233
59993
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
59234
- volumeLevel: number().min(0).max(100).nullable(),
59994
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59235
59995
  /** Mute toggle distinct from volume=0. Null when no mute surface.
59236
59996
  * Pair with `DeviceFeature.MediaPlayerMute`. */
59237
59997
  isMuted: boolean().nullable(),
@@ -59244,10 +60004,10 @@ object({
59244
60004
  currentMedia: MediaInfoSchema.nullable(),
59245
60005
  /** Current playback position in ms. Null when not seekable or
59246
60006
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
59247
- positionMs: number().int().nonnegative().nullable(),
60007
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59248
60008
  /** Total duration of the current media in ms. Null when unknown
59249
60009
  * (live stream) or nothing is playing. */
59250
- durationMs: number().int().nonnegative().nullable(),
60010
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59251
60011
  /** Shuffle toggle. Null when no shuffle surface.
59252
60012
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
59253
60013
  shuffle: boolean().nullable(),
@@ -59547,7 +60307,7 @@ object({
59547
60307
  * it, every provider seeded it at boot, and every session left it
59548
60308
  * `detected: true` after its detach.
59549
60309
  */
59550
- autoClearAfterMs: number().nullable()
60310
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
59551
60311
  });
59552
60312
  /**
59553
60313
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -59597,7 +60357,7 @@ var MotionZoneRegionSchema = object({
59597
60357
  });
59598
60358
  object({
59599
60359
  enabled: boolean(),
59600
- sensitivity: number(),
60360
+ sensitivity: number().meta(readingMeta("none")),
59601
60361
  /** Grid region(s). Today exactly one `grid` shape. */
59602
60362
  regions: array(MotionZoneRegionSchema),
59603
60363
  lastFetchedAt: number()
@@ -59901,7 +60661,7 @@ var NavigationStatusSchema = object({
59901
60661
  following: boolean(),
59902
60662
  flash: boolean(),
59903
60663
  lightMode: NavigationLightModeSchema,
59904
- lightLevel: number().min(40).max(100),
60664
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
59905
60665
  /** Ms epoch when the slice was last updated. */
59906
60666
  lastChangedAt: number()
59907
60667
  });
@@ -59947,13 +60707,13 @@ var NetworkLinkStatusSchema = object({
59947
60707
  * one whose reading has not landed must not be drawn at 0 %. Consumers
59948
60708
  * SKIP a null rather than coerce it.
59949
60709
  */
59950
- signalPercent: number().min(0).max(100).nullable(),
60710
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59951
60711
  /** Raw received signal strength in dBm, when the firmware reports one. */
59952
- rssiDbm: number().optional(),
60712
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
59953
60713
  /** Network name of a wireless link, when the firmware reports it. */
59954
60714
  ssid: string().optional(),
59955
60715
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
59956
- lastUpdated: number()
60716
+ lastUpdated: number().meta(readingMeta("none"))
59957
60717
  });
59958
60718
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
59959
60719
  deviceId: number(),
@@ -60272,7 +61032,7 @@ object({
60272
61032
  lastError: string().nullable(),
60273
61033
  /** Number of deliveries currently buffered server-side (rate-limited
60274
61034
  * notifiers may queue). 0 when the provider sends synchronously. */
60275
- queueDepth: number().int().nonnegative(),
61035
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
60276
61036
  /** Per-feature capability matrix — authoritative source for the
60277
61037
  * compose-form field gating. */
60278
61038
  supports: NotifierSupportsSchema
@@ -60322,15 +61082,8 @@ DeviceType.Notifier, method(NotifierSendInputSchema, NotifierSendResultSchema, {
60322
61082
  acceptedAt: number()
60323
61083
  });
60324
61084
  object({
60325
- value: number(),
60326
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
60327
- * Populated live from the upstream source on each state push.
60328
- * Absent for unitless sensors. */
60329
- unit: string().optional(),
60330
- /** Suggested decimal places for numeric display.
60331
- * Populated live from the upstream source when provided.
60332
- * Falls back to auto-formatting when absent. */
60333
- precision: number().int().min(0).max(10).optional(),
61085
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
61086
+ value: number().nullable().meta(readingMeta("device")),
60334
61087
  /** Ms epoch when the slice was last updated. */
60335
61088
  lastFetchedAt: number()
60336
61089
  });
@@ -60620,23 +61373,26 @@ object({
60620
61373
  /** Food currently in the bowl (grams). Null when the device has not
60621
61374
  * reported a reading yet. On dual-hopper models this is the combined
60622
61375
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
60623
- foodLevel: number().nullable(),
61376
+ foodLevel: number().nullable().meta(readingMeta("none")),
60624
61377
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
60625
61378
  * single-hopper models. */
60626
- food1: number().nullable(),
61379
+ food1: number().nullable().meta(readingMeta("none")),
60627
61380
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
60628
61381
  * single-hopper models. */
60629
- food2: number().nullable(),
61382
+ food2: number().nullable().meta(readingMeta("none")),
60630
61383
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
60631
61384
  * (`device_class: problem`, on = low). True when the bowl is empty /
60632
61385
  * below the feeder's low threshold. */
60633
61386
  lowFood: boolean(),
60634
61387
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
60635
61388
  * device has no battery reading. */
60636
- batteryPower: number().min(0).max(100).nullable(),
60637
- /** Days of desiccant life remaining. Null when the model has no
60638
- * desiccant sensor. */
60639
- desiccantLeftDays: number().nullable(),
61389
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61390
+ /**
61391
+ * Desiccant life remaining, stored in seconds (duration base, D944).
61392
+ * Providers convert from the days the feeder reports. Null when the
61393
+ * model has no desiccant sensor.
61394
+ */
61395
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
60640
61396
  /** True while a feed is in progress. */
60641
61397
  feeding: boolean(),
60642
61398
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -60647,7 +61403,7 @@ object({
60647
61403
  * with `errorCode` for consumers that want the raw integer. */
60648
61404
  error: string().nullable(),
60649
61405
  /** Raw device error code (0 / null = no error). */
60650
- errorCode: number().nullable(),
61406
+ errorCode: number().nullable().meta(readingMeta("none")),
60651
61407
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
60652
61408
  isDualHopper: boolean(),
60653
61409
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -60657,7 +61413,7 @@ object({
60657
61413
  /** Play a chime when dispensing. */
60658
61414
  feedSound: boolean(),
60659
61415
  /** Speaker / prompt volume level (device-scaled integer). */
60660
- volume: number(),
61416
+ volume: number().meta(readingMeta("none")),
60661
61417
  /** Ms epoch when the slice was last refreshed from the cloud. */
60662
61418
  lastFetchedAt: number()
60663
61419
  });
@@ -61094,38 +61850,37 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
61094
61850
  });
61095
61851
  object({
61096
61852
  /** Instantaneous power draw in watts. */
61097
- watts: number().optional().meta({
61098
- unit: "W",
61099
- label: "Power"
61100
- }),
61853
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
61101
61854
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
61102
- kwhTotal: number().optional().meta({
61103
- unit: "kWh",
61104
- label: "Energy"
61105
- }),
61855
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
61106
61856
  /** Voltage in volts. */
61107
- volts: number().optional(),
61857
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
61108
61858
  /** Current in amperes. */
61109
- amps: number().optional(),
61110
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
61111
- va: number().optional(),
61859
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
61860
+ /** Apparent power in volt-amperes. */
61861
+ va: number().optional().meta(readingMeta("apparent-power")),
61112
61862
  /** Power factor (0..1) if reported. */
61113
- powerFactor: number().min(0).max(1).optional(),
61863
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
61114
61864
  /** Ms epoch when the slice was last updated. */
61115
- lastFetchedAt: number(),
61116
- /** Live display unit of the single metric this slice carries (e.g. HA
61117
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
61118
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
61119
- * unit per slice is unambiguous. The UI prefers this over the role's
61120
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
61121
- unit: string().optional(),
61122
- /** Suggested decimal places for numeric display.
61123
- * Populated live from the upstream source when provided (e.g. HA
61124
- * `attributes.suggested_display_precision`). Falls back to
61125
- * auto-formatting when absent. */
61126
- precision: number().int().min(0).max(10).optional()
61865
+ lastFetchedAt: number()
61127
61866
  });
61128
- DeviceType.Sensor;
61867
+ [
61868
+ DeviceType.Sensor,
61869
+ DeviceType.Switch,
61870
+ DeviceType.Light,
61871
+ DeviceType.Fan,
61872
+ DeviceType.Climate,
61873
+ DeviceType.Thermostat,
61874
+ DeviceType.Humidifier,
61875
+ DeviceType.WaterHeater,
61876
+ DeviceType.MediaPlayer,
61877
+ DeviceType.Washer,
61878
+ DeviceType.Dryer,
61879
+ DeviceType.Dishwasher,
61880
+ DeviceType.Vacuum,
61881
+ DeviceType.LawnMower,
61882
+ DeviceType.PetFeeder
61883
+ ].filter((type) => type !== DeviceType.Sensor);
61129
61884
  /**
61130
61885
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
61131
61886
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -61144,11 +61899,11 @@ DeviceType.Sensor;
61144
61899
  */
61145
61900
  var GpsLocationSchema = object({
61146
61901
  /** Latitude in decimal degrees, -90..90. */
61147
- latitude: number().min(-90).max(90),
61902
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
61148
61903
  /** Longitude in decimal degrees, -180..180. */
61149
- longitude: number().min(-180).max(180),
61904
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
61150
61905
  /** Reported accuracy in meters (lower = better). */
61151
- accuracyMeters: number().nonnegative()
61906
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
61152
61907
  });
61153
61908
  object({
61154
61909
  /** `home` / `not_home` / any user-defined zone name. */
@@ -61160,25 +61915,16 @@ object({
61160
61915
  gps: GpsLocationSchema.nullable(),
61161
61916
  /** Optional battery level of the tracking device (0..100). Null
61162
61917
  * when not reported. */
61163
- batteryPercent: number().min(0).max(100).nullable(),
61918
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61164
61919
  /** Ms epoch when the slice was last updated. */
61165
61920
  lastChangedAt: number()
61166
61921
  });
61167
61922
  DeviceType.Presence;
61168
61923
  object({
61169
- /** Current pressure in hPa. */
61170
- hpa: number(),
61924
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
61925
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
61171
61926
  /** Ms epoch when the slice was last updated. */
61172
- lastFetchedAt: number(),
61173
- /** Live display unit from the upstream source (e.g. HA
61174
- * `attributes.unit_of_measurement`). The UI prefers this over the
61175
- * role's canonical unit. Absent → fall back to the canonical unit. */
61176
- unit: string().optional(),
61177
- /** Suggested decimal places for numeric display.
61178
- * Populated live from the upstream source when provided (e.g. HA
61179
- * `attributes.suggested_display_precision`). Falls back to
61180
- * auto-formatting when absent. */
61181
- precision: number().int().min(0).max(10).optional()
61927
+ lastFetchedAt: number()
61182
61928
  });
61183
61929
  DeviceType.Sensor;
61184
61930
  /**
@@ -61511,8 +62257,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
61511
62257
  }), _void(), { kind: "mutation" });
61512
62258
  var PtzAutotrackSettingsSchema = object({
61513
62259
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
61514
- stopDelaySeconds: number().int().min(0).max(300),
61515
- disappearDelaySeconds: number().int().min(0).max(300)
62260
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
62261
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
61516
62262
  });
61517
62263
  /**
61518
62264
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -61569,7 +62315,7 @@ object({
61569
62315
  /** null = unknown — never coerced to false (D393). */
61570
62316
  running: boolean().nullable(),
61571
62317
  /** Epoch ms the current boolean began; null while `running` is null. */
61572
- since: number().nullable(),
62318
+ since: number().nullable().meta(readingMeta("none")),
61573
62319
  source: RunningStateSourceSchema.meta({ label: "Source" }),
61574
62320
  phase: string().nullable(),
61575
62321
  /** null until a reading has been published (D393). */
@@ -61581,7 +62327,7 @@ object({
61581
62327
  phase: string().nullable()
61582
62328
  });
61583
62329
  Object.values(DeviceType);
61584
- DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
62330
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
61585
62331
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
61586
62332
  kind: "mutation",
61587
62333
  auth: "admin"
@@ -62767,9 +63513,9 @@ var RecordTriggerSchema = _enum([
62767
63513
  */
62768
63514
  var RecordWindowSchema = object({
62769
63515
  trigger: RecordTriggerSchema,
62770
- day: number().int().min(0).max(6),
62771
- startMinute: number().int().min(0).max(1439),
62772
- endMinute: number().int().min(1).max(1440)
63516
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
63517
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
63518
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
62773
63519
  });
62774
63520
  /** Status of one physical volume, as the camera itself describes it. */
62775
63521
  var OnboardStorageVolumeSchema = object({
@@ -62838,10 +63584,10 @@ object({
62838
63584
  /** Master "record to the card at all" switch. */
62839
63585
  enabled: boolean().nullable(),
62840
63586
  overwriteWhenFull: boolean().nullable(),
62841
- preRecordSec: number().nullable(),
62842
- postRecordSec: number().nullable(),
63587
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
63588
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
62843
63589
  /** Length of one recorded file, in minutes. */
62844
- segmentMinutes: number().nullable(),
63590
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
62845
63591
  /** The primary track's weekly windows, flattened. */
62846
63592
  windows: array(RecordWindowSchema),
62847
63593
  /**
@@ -62857,7 +63603,7 @@ object({
62857
63603
  * provider reporting a non-zero count also reports the schedule as not
62858
63604
  * writable.
62859
63605
  */
62860
- unreadableWindows: number(),
63606
+ unreadableWindows: number().meta(readingMeta("count")),
62861
63607
  /**
62862
63608
  * The camera is scheduled to record and has NO usable storage.
62863
63609
  *
@@ -63494,7 +64240,7 @@ var CamStreamDescriptorSchema = object({
63494
64240
  url: string().optional(),
63495
64241
  codec: string().optional(),
63496
64242
  resolution: CamStreamResolutionSchema.optional(),
63497
- fps: number().positive().optional(),
64243
+ fps: number().positive().optional().meta(readingMeta("rate")),
63498
64244
  label: string().optional(),
63499
64245
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
63500
64246
  deviceFeatures: array(string()).optional(),
@@ -63524,18 +64270,18 @@ var StreamProfileSchema = _enum([
63524
64270
  ]);
63525
64271
  /** The current encoding config of one stream profile. */
63526
64272
  var StreamProfileConfigSchema = object({
63527
- width: number(),
63528
- height: number(),
64273
+ width: number().meta(readingMeta("none")),
64274
+ height: number().meta(readingMeta("none")),
63529
64275
  codec: _enum(["h264", "h265"]),
63530
- framerate: number(),
63531
- bitrate: number(),
64276
+ framerate: number().meta(readingMeta("rate")),
64277
+ bitrate: number().meta(readingMeta("bitrate")),
63532
64278
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
63533
64279
  encoderProfile: _enum([
63534
64280
  "high",
63535
64281
  "main",
63536
64282
  "baseline"
63537
64283
  ]).optional(),
63538
- gop: number().optional(),
64284
+ gop: number().optional().meta(readingMeta("none")),
63539
64285
  /**
63540
64286
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
63541
64287
  *
@@ -63620,7 +64366,7 @@ object({
63620
64366
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
63621
64367
  lastChangedAt: number()
63622
64368
  });
63623
- DeviceType.Switch, DeviceType.Siren, DeviceType.Light, method(object({
64369
+ DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, method(object({
63624
64370
  deviceId: number(),
63625
64371
  on: boolean()
63626
64372
  }), _void(), {
@@ -64178,23 +64924,13 @@ object({
64178
64924
  });
64179
64925
  DeviceType.Sensor;
64180
64926
  object({
64181
- /** Current temperature in Celsius. */
64182
- celsius: number().meta({
64183
- unit: "°C",
64927
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
64928
+ celsius: number().nullable().meta(readingMeta("temperature", {
64929
+ precision: 1,
64184
64930
  label: "Temperature"
64185
- }),
64931
+ })),
64186
64932
  /** Ms epoch when the slice was last updated (push or poll). */
64187
- lastFetchedAt: number(),
64188
- /** Live display unit from the upstream source (e.g. HA
64189
- * `attributes.unit_of_measurement`). The UI prefers this over the
64190
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
64191
- * Absent → the UI falls back to the role's canonical unit. */
64192
- unit: string().optional(),
64193
- /** Suggested decimal places for numeric display.
64194
- * Populated live from the upstream source when provided (e.g. HA
64195
- * `attributes.suggested_display_precision`). Falls back to
64196
- * auto-formatting when absent. */
64197
- precision: number().int().min(0).max(10).optional()
64933
+ lastFetchedAt: number()
64198
64934
  });
64199
64935
  DeviceType.Sensor, DeviceType.Thermostat;
64200
64936
  /**
@@ -64446,16 +65182,16 @@ object({
64446
65182
  ]),
64447
65183
  /** 0..100 battery percentage. Null when the device has no battery
64448
65184
  * reading. */
64449
- batteryLevel: number().min(0).max(100).nullable(),
65185
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64450
65186
  /** Current fan-speed token (provider-verbatim). Null when unknown or
64451
65187
  * the vacuum has no speed control. */
64452
65188
  fanSpeed: string().nullable(),
64453
65189
  /** Speed tokens the hardware accepts — drives the UI selector. */
64454
65190
  availableFanSpeeds: array(string()),
64455
65191
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64456
- progressPercent: number().min(0).max(100).nullable(),
65192
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64457
65193
  /** Current error code (0 / null = no error). */
64458
- errorCode: number().nullable(),
65194
+ errorCode: number().nullable().meta(readingMeta("none")),
64459
65195
  /** Human label for {@link errorCode}, or null when none / undecodable. */
64460
65196
  errorLabel: string().nullable(),
64461
65197
  /** Ms epoch when the slice was last updated. */
@@ -64494,7 +65230,7 @@ object({
64494
65230
  ]),
64495
65231
  /** 0 = fully closed, 100 = fully open. Null when the device has no
64496
65232
  * intermediate position surface. */
64497
- position: number().min(0).max(100).nullable(),
65233
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64498
65234
  /** Ms epoch when the slice was last updated. */
64499
65235
  lastChangedAt: number()
64500
65236
  });
@@ -64522,9 +65258,9 @@ object({
64522
65258
  DeviceType.Sensor;
64523
65259
  object({
64524
65260
  /** Current measured temperature. Null when not reported. */
64525
- currentTemp: number().nullable(),
65261
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
64526
65262
  /** Target temperature setpoint. Null when no setpoint surface. */
64527
- targetTemp: number().nullable(),
65263
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
64528
65264
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
64529
65265
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
64530
65266
  * device reports an unknown state. */
@@ -64535,9 +65271,9 @@ object({
64535
65271
  * has no away surface. */
64536
65272
  away: boolean().nullable(),
64537
65273
  /** HA `min_temp` attribute. Null when not reported. */
64538
- minTemp: number().nullable(),
65274
+ minTemp: number().nullable().meta(readingMeta("temperature")),
64539
65275
  /** HA `max_temp` attribute. Null when not reported. */
64540
- maxTemp: number().nullable(),
65276
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
64541
65277
  /** Ms epoch when the slice was last updated. */
64542
65278
  lastChangedAt: number()
64543
65279
  });
@@ -64561,25 +65297,18 @@ DeviceType.WaterHeater, method(object({
64561
65297
  auth: "protected"
64562
65298
  });
64563
65299
  object({
64564
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
64565
- * when no condition has been reported yet. */
65300
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
64566
65301
  condition: string().nullable(),
64567
- /** Current temperature in the reported unit. Null when not provided. */
64568
- temperature: number().nullable(),
64569
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
64570
- temperatureUnit: string().nullable(),
65302
+ /** Current temperature in Celsius. Null when not provided or refused. */
65303
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
64571
65304
  /** Relative humidity (0..100). Null when not provided. */
64572
- humidity: number().min(0).max(100).nullable(),
64573
- /** Barometric pressure in the reported unit. Null when not provided. */
64574
- pressure: number().nullable(),
64575
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
64576
- pressureUnit: string().nullable(),
64577
- /** Wind speed in the reported unit. Null when not provided. */
64578
- windSpeed: number().nullable(),
64579
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
64580
- windSpeedUnit: string().nullable(),
65305
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
65306
+ /** Barometric pressure in hPa. Null when not provided or refused. */
65307
+ pressure: number().nullable().meta(readingMeta("pressure")),
65308
+ /** Wind speed in metres per second. Null when not provided or refused. */
65309
+ windSpeed: number().nullable().meta(readingMeta("speed")),
64581
65310
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
64582
- windBearing: number().nullable(),
65311
+ windBearing: number().nullable().meta(readingMeta("angle")),
64583
65312
  /** Ms epoch when the slice was last updated. */
64584
65313
  lastFetchedAt: number()
64585
65314
  });
@@ -65329,8 +66058,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
65329
66058
  kind: "mutation",
65330
66059
  auth: "admin"
65331
66060
  });
66061
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
65332
66062
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
65333
- 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;
66063
+ 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;
65334
66064
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
65335
66065
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
65336
66066
  new Set(CAP_NAMES);
@@ -65383,7 +66113,7 @@ var AccessoryKind = {
65383
66113
  PrivacyMask: DeviceRole.PrivacyMask
65384
66114
  };
65385
66115
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
65386
- 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;
66116
+ 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;
65387
66117
  new Set(Object.values(DeviceType));
65388
66118
  Object.freeze({
65389
66119
  "accessories.getStatus": {
@@ -65491,6 +66221,13 @@ Object.freeze({
65491
66221
  access: "view",
65492
66222
  role: "admin"
65493
66223
  },
66224
+ "addons.getPackageDocs": {
66225
+ capName: "addons",
66226
+ capScope: "system",
66227
+ addonId: null,
66228
+ access: "view",
66229
+ role: "admin"
66230
+ },
65494
66231
  "addons.getVersions": {
65495
66232
  capName: "addons",
65496
66233
  capScope: "system",
@@ -68599,6 +69336,27 @@ Object.freeze({
68599
69336
  access: "view",
68600
69337
  role: "viewer"
68601
69338
  },
69339
+ "groupActive.getStatus": {
69340
+ capName: "group-active",
69341
+ capScope: "device",
69342
+ addonId: null,
69343
+ access: "view",
69344
+ role: "viewer"
69345
+ },
69346
+ "groupCommands.getStatus": {
69347
+ capName: "group-commands",
69348
+ capScope: "device",
69349
+ addonId: null,
69350
+ access: "view",
69351
+ role: "viewer"
69352
+ },
69353
+ "groupStatus.getStatus": {
69354
+ capName: "group-status",
69355
+ capScope: "device",
69356
+ addonId: null,
69357
+ access: "view",
69358
+ role: "viewer"
69359
+ },
68602
69360
  "heatingValve.getStatus": {
68603
69361
  capName: "heating-valve",
68604
69362
  capScope: "device",
@@ -69551,6 +70309,13 @@ Object.freeze({
69551
70309
  access: "create",
69552
70310
  role: "admin"
69553
70311
  },
70312
+ "meter.getStatus": {
70313
+ capName: "meter",
70314
+ capScope: "device",
70315
+ addonId: null,
70316
+ access: "view",
70317
+ role: "viewer"
70318
+ },
69554
70319
  "metricsProvider.collectSnapshot": {
69555
70320
  capName: "metrics-provider",
69556
70321
  capScope: "system",
@@ -77649,7 +78414,39 @@ var APPLIANCE_CAP_VOCABULARY = {
77649
78414
  phrase: t("When {device} finishes", "Quando {device} finisce"),
77650
78415
  icon: "square"
77651
78416
  }
77652
- }
78417
+ },
78418
+ overrides: { byType: {
78419
+ [DeviceType.Washer]: {
78420
+ state: { running: {
78421
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
78422
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
78423
+ } },
78424
+ events: {
78425
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
78426
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
78427
+ }
78428
+ },
78429
+ [DeviceType.Dryer]: {
78430
+ state: { running: {
78431
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
78432
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
78433
+ } },
78434
+ events: {
78435
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
78436
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
78437
+ }
78438
+ },
78439
+ [DeviceType.Dishwasher]: {
78440
+ state: { running: {
78441
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
78442
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
78443
+ } },
78444
+ events: {
78445
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
78446
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
78447
+ }
78448
+ }
78449
+ } }
77653
78450
  },
77654
78451
  update: {
77655
78452
  scope: "device",
@@ -78421,6 +79218,41 @@ var SENSOR_CAP_VOCABULARY = {
78421
79218
  icon: "flame"
78422
79219
  }]
78423
79220
  },
79221
+ "group-active": {
79222
+ scope: "device",
79223
+ noun: t("Group state", "Stato del gruppo")
79224
+ },
79225
+ "group-commands": {
79226
+ scope: "device",
79227
+ noun: t("Group command", "Comando di gruppo")
79228
+ },
79229
+ "group-status": {
79230
+ scope: "device",
79231
+ noun: t("Group", "Gruppo"),
79232
+ state: [
79233
+ {
79234
+ kind: "numeric",
79235
+ field: "included",
79236
+ quantity: t("the members reporting", "i membri che rispondono"),
79237
+ label: t("Reporting", "Rispondono"),
79238
+ icon: "users"
79239
+ },
79240
+ {
79241
+ kind: "numeric",
79242
+ field: "excluded",
79243
+ quantity: t("the members left out", "i membri esclusi"),
79244
+ label: t("Excluded", "Esclusi"),
79245
+ icon: "users"
79246
+ },
79247
+ {
79248
+ kind: "numeric",
79249
+ field: "total",
79250
+ quantity: t("the member count", "il numero di membri"),
79251
+ label: t("Members", "Membri"),
79252
+ icon: "sigma"
79253
+ }
79254
+ ]
79255
+ },
78424
79256
  "humidity-sensor": {
78425
79257
  scope: "device",
78426
79258
  noun: t("Humidity", "Umidità"),
@@ -78446,6 +79278,17 @@ var SENSOR_CAP_VOCABULARY = {
78446
79278
  }],
78447
79279
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
78448
79280
  },
79281
+ meter: {
79282
+ scope: "device",
79283
+ noun: t("Meter", "Contatore"),
79284
+ state: [{
79285
+ kind: "numeric",
79286
+ field: "value",
79287
+ quantity: t("the total", "il totale"),
79288
+ label: t("Total", "Totale"),
79289
+ icon: "gauge"
79290
+ }]
79291
+ },
78449
79292
  "numeric-sensor": {
78450
79293
  scope: "device",
78451
79294
  noun: t("Sensor", "Sensore"),
@@ -78454,7 +79297,6 @@ var SENSOR_CAP_VOCABULARY = {
78454
79297
  field: "value",
78455
79298
  quantity: t("the reading", "il valore"),
78456
79299
  label: t("Reading", "Valore"),
78457
- unitField: "unit",
78458
79300
  icon: "gauge"
78459
79301
  }]
78460
79302
  },
@@ -78590,7 +79432,7 @@ var SENSOR_CAP_VOCABULARY = {
78590
79432
  field: "temperature",
78591
79433
  quantity: t("the outdoor temperature", "la temperatura esterna"),
78592
79434
  label: t("Temperature", "Temperatura"),
78593
- unitField: "temperatureUnit",
79435
+ unit: "°C",
78594
79436
  icon: "thermometer"
78595
79437
  },
78596
79438
  {
@@ -78606,7 +79448,7 @@ var SENSOR_CAP_VOCABULARY = {
78606
79448
  field: "windSpeed",
78607
79449
  quantity: t("the wind speed", "la velocità del vento"),
78608
79450
  label: t("Wind", "Vento"),
78609
- unitField: "windSpeedUnit",
79451
+ unit: "m/s",
78610
79452
  icon: "wind"
78611
79453
  },
78612
79454
  {
@@ -78614,7 +79456,7 @@ var SENSOR_CAP_VOCABULARY = {
78614
79456
  field: "pressure",
78615
79457
  quantity: t("the air pressure", "la pressione atmosferica"),
78616
79458
  label: t("Pressure", "Pressione"),
78617
- unitField: "pressureUnit",
79459
+ unit: "hPa",
78618
79460
  icon: "gauge"
78619
79461
  }
78620
79462
  ]
@@ -78919,8 +79761,47 @@ function projectAction(cap, action) {
78919
79761
  };
78920
79762
  }
78921
79763
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
78922
- 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;
79764
+ 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;
78923
79765
  DeviceFeature.BatteryOperated;
79766
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
79767
+ 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.";
79768
+ 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.";
79769
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
79770
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
79771
+ 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.";
79772
+ function withheld(reason) {
79773
+ return {
79774
+ [DeviceType.Washer]: {
79775
+ exported: false,
79776
+ reason
79777
+ },
79778
+ [DeviceType.Dryer]: {
79779
+ exported: false,
79780
+ reason
79781
+ },
79782
+ [DeviceType.Dishwasher]: {
79783
+ exported: false,
79784
+ reason
79785
+ }
79786
+ };
79787
+ }
79788
+ function offered(received) {
79789
+ return {
79790
+ [DeviceType.Washer]: {
79791
+ exported: true,
79792
+ as: received
79793
+ },
79794
+ [DeviceType.Dryer]: {
79795
+ exported: true,
79796
+ as: received
79797
+ },
79798
+ [DeviceType.Dishwasher]: {
79799
+ exported: true,
79800
+ as: received
79801
+ }
79802
+ };
79803
+ }
79804
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
78924
79805
  Object.freeze({
78925
79806
  "accessories.getStatus": [{
78926
79807
  name: "deviceId",
@@ -79887,6 +80768,21 @@ Object.freeze({
79887
80768
  form: "single",
79888
80769
  optional: false
79889
80770
  }],
80771
+ "groupActive.getStatus": [{
80772
+ name: "deviceId",
80773
+ form: "single",
80774
+ optional: false
80775
+ }],
80776
+ "groupCommands.getStatus": [{
80777
+ name: "deviceId",
80778
+ form: "single",
80779
+ optional: false
80780
+ }],
80781
+ "groupStatus.getStatus": [{
80782
+ name: "deviceId",
80783
+ form: "single",
80784
+ optional: false
80785
+ }],
79890
80786
  "heatingValve.getStatus": [{
79891
80787
  name: "deviceId",
79892
80788
  form: "single",
@@ -80087,6 +80983,11 @@ Object.freeze({
80087
80983
  form: "single",
80088
80984
  optional: false
80089
80985
  }],
80986
+ "meter.getStatus": [{
80987
+ name: "deviceId",
80988
+ form: "single",
80989
+ optional: false
80990
+ }],
80090
80991
  "motion.getStatus": [{
80091
80992
  name: "deviceId",
80092
80993
  form: "single",
@@ -81694,7 +82595,6 @@ Object.freeze({
81694
82595
  "network-access": "ingress",
81695
82596
  "smtp-provider": "email"
81696
82597
  });
81697
- DeviceType.Camera;
81698
82598
  /**
81699
82599
  * A saved Customize assignment (D936). The schema is the type: callers use
81700
82600
  * `z.infer`, they do not restate the shape.
@@ -81871,6 +82771,8 @@ new Map(Object.entries({
81871
82771
  "detection-blind": "device-events",
81872
82772
  "motion-noisy": "device-events",
81873
82773
  "motion-calm": "device-events",
82774
+ "package-llm-silent": "device-events",
82775
+ "package-llm-answered": "device-events",
81874
82776
  "recording-stalled": "device-events",
81875
82777
  "recording-healthy": "device-events",
81876
82778
  "recording-degraded": "device-events",
@@ -81954,6 +82856,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
81954
82856
  label: "Motion without a subject",
81955
82857
  kinds: ["motion-noisy", "motion-calm"]
81956
82858
  },
82859
+ {
82860
+ id: "package-llm",
82861
+ family: "device-events",
82862
+ label: "Package confirm",
82863
+ kinds: ["package-llm-silent", "package-llm-answered"]
82864
+ },
81957
82865
  {
81958
82866
  id: "recording",
81959
82867
  family: "device-events",
@@ -82135,6 +83043,8 @@ var DEVICE_EVENTS = [
82135
83043
  "detection-blind",
82136
83044
  "motion-noisy",
82137
83045
  "motion-calm",
83046
+ "package-llm-silent",
83047
+ "package-llm-answered",
82138
83048
  "recording-stalled",
82139
83049
  "recording-healthy",
82140
83050
  "recording-degraded",
@@ -82403,6 +83313,15 @@ new Map([
82403
83313
  snapshotLabel: "Cameras with constant motion now",
82404
83314
  snapshotSection: null
82405
83315
  },
83316
+ {
83317
+ id: "package-llm-confirm",
83318
+ family: "device-events",
83319
+ label: "Package confirm (not answering / answering again)",
83320
+ bad: ["package-llm-silent"],
83321
+ recovery: ["package-llm-answered"],
83322
+ snapshotLabel: "Cameras whose package confirm is not answering",
83323
+ snapshotSection: null
83324
+ },
82406
83325
  {
82407
83326
  id: "recording-health",
82408
83327
  family: "device-events",