@camstack/addon-auth 1.2.168 → 1.2.170

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,7 +32,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
32
32
  }) : target, mod));
33
33
  var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
34
34
  require("node:crypto");
35
- //#region ../types/dist/event-category-DHXxB8t9.mjs
35
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
36
36
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
37
37
  EventCategory["SystemBoot"] = "system.boot";
38
38
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -358,6 +358,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
358
358
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
359
359
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
360
360
  /**
361
+ * A camera's package LLM confirm has failed closed three times in a row
362
+ * (D931, 2026-10-26). One edge per camera until
363
+ * {@link PipelinePackageLlmAnswered}.
364
+ */
365
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
366
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
367
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
368
+ /**
361
369
  * Per-camera pipeline config was mutated by the orchestrator
362
370
  * (a settings change via `setAgentInferenceDevices` /
363
371
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5600,7 +5608,7 @@ var ZodIssueCode = {
5600
5608
  var ZodFirstPartyTypeKind;
5601
5609
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5602
5610
  //#endregion
5603
- //#region ../types/dist/sleep-BErAxhul.mjs
5611
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5604
5612
  /**
5605
5613
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5606
5614
  * window to float samples (D455).
@@ -8328,6 +8336,99 @@ function defineEntryKind(name, spec) {
8328
8336
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8329
8337
  return defined;
8330
8338
  }
8339
+ var READING_QUANTITIES = [
8340
+ "temperature",
8341
+ "pressure",
8342
+ "speed",
8343
+ "precipitation",
8344
+ "precipitation-rate",
8345
+ "length",
8346
+ "area",
8347
+ "illuminance",
8348
+ "irradiance",
8349
+ "power",
8350
+ "energy",
8351
+ "voltage",
8352
+ "current",
8353
+ "apparent-power",
8354
+ "humidity",
8355
+ "percentage",
8356
+ "concentration-volume",
8357
+ "concentration-mass",
8358
+ "concentration-ppb",
8359
+ "volume",
8360
+ "duration",
8361
+ "duration-ms",
8362
+ "information",
8363
+ "sound-level",
8364
+ "signal-level",
8365
+ "colour-temperature",
8366
+ "angle",
8367
+ "bitrate",
8368
+ "rate",
8369
+ "mass",
8370
+ "index",
8371
+ "ratio",
8372
+ "count",
8373
+ "none",
8374
+ "device"
8375
+ ];
8376
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8377
+ new Set(READING_QUANTITIES);
8378
+ /**
8379
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8380
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8381
+ */
8382
+ var QUANTITY_BASE = {
8383
+ temperature: "°C",
8384
+ pressure: "hPa",
8385
+ speed: "m/s",
8386
+ precipitation: "mm",
8387
+ "precipitation-rate": "mm/h",
8388
+ length: "m",
8389
+ area: "m²",
8390
+ illuminance: "lx",
8391
+ irradiance: "W/m²",
8392
+ power: "W",
8393
+ energy: "kWh",
8394
+ voltage: "V",
8395
+ current: "A",
8396
+ "apparent-power": "VA",
8397
+ humidity: "%",
8398
+ percentage: "%",
8399
+ "concentration-volume": "ppm",
8400
+ "concentration-mass": "µg/m³",
8401
+ "concentration-ppb": "ppb",
8402
+ volume: "L",
8403
+ duration: "s",
8404
+ "duration-ms": "ms",
8405
+ information: "B",
8406
+ "sound-level": "dBFS",
8407
+ "signal-level": "dBm",
8408
+ "colour-temperature": "mired",
8409
+ angle: "°",
8410
+ bitrate: "kbps",
8411
+ rate: "fps",
8412
+ mass: "kg",
8413
+ index: "none",
8414
+ ratio: "none",
8415
+ count: "none",
8416
+ none: "none",
8417
+ device: "device"
8418
+ };
8419
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8420
+ function baseUnitOf(quantity) {
8421
+ const symbol = QUANTITY_BASE[quantity];
8422
+ if (symbol === "none" || symbol === "device") return void 0;
8423
+ return symbol;
8424
+ }
8425
+ function readingMeta(quantity, extras) {
8426
+ return {
8427
+ quantity,
8428
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8429
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8430
+ };
8431
+ }
8331
8432
  /** Shared Zod schemas used across streaming capabilities. */
8332
8433
  var CamProfileSchema = _enum([
8333
8434
  "high",
@@ -8355,8 +8456,8 @@ var CamStreamKindSchema = _enum([
8355
8456
  "derived"
8356
8457
  ]);
8357
8458
  var CamStreamResolutionSchema = object({
8358
- width: number().int().positive(),
8359
- height: number().int().positive()
8459
+ width: number().int().positive().meta(readingMeta("none")),
8460
+ height: number().int().positive().meta(readingMeta("none"))
8360
8461
  });
8361
8462
  var CameraStreamSchema = object({
8362
8463
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8870,6 +8971,9 @@ var CAP_NAMES = Object.freeze([
8870
8971
  "filesystem-browse",
8871
8972
  "flood",
8872
8973
  "gas",
8974
+ "group-active",
8975
+ "group-commands",
8976
+ "group-status",
8873
8977
  "heating-valve",
8874
8978
  "humidifier",
8875
8979
  "humidity-sensor",
@@ -8890,6 +8994,7 @@ var CAP_NAMES = Object.freeze([
8890
8994
  "login-method",
8891
8995
  "media-player",
8892
8996
  "mesh-network",
8997
+ "meter",
8893
8998
  "metrics-provider",
8894
8999
  "model-convert",
8895
9000
  "model-distributor",
@@ -8999,14 +9104,6 @@ Object.freeze({
8999
9104
  {
9000
9105
  "path": "aqi",
9001
9106
  "valueType": "number"
9002
- },
9003
- {
9004
- "path": "unit",
9005
- "valueType": "string"
9006
- },
9007
- {
9008
- "path": "precision",
9009
- "valueType": "number"
9010
9107
  }
9011
9108
  ],
9012
9109
  "alarm-panel": [
@@ -9042,20 +9139,10 @@ Object.freeze({
9042
9139
  "valueType": "boolean"
9043
9140
  }
9044
9141
  ],
9045
- "ambient-light-sensor": [
9046
- {
9047
- "path": "lux",
9048
- "valueType": "number"
9049
- },
9050
- {
9051
- "path": "unit",
9052
- "valueType": "string"
9053
- },
9054
- {
9055
- "path": "precision",
9056
- "valueType": "number"
9057
- }
9058
- ],
9142
+ "ambient-light-sensor": [{
9143
+ "path": "lux",
9144
+ "valueType": "number"
9145
+ }],
9059
9146
  "audio-metrics": [
9060
9147
  {
9061
9148
  "path": "ts",
@@ -9135,6 +9222,10 @@ Object.freeze({
9135
9222
  "path": "lastUpdated",
9136
9223
  "valueType": "number"
9137
9224
  },
9225
+ {
9226
+ "path": "volts",
9227
+ "valueType": "number"
9228
+ },
9138
9229
  {
9139
9230
  "path": "lastContactAt",
9140
9231
  "valueType": "number"
@@ -9377,10 +9468,6 @@ Object.freeze({
9377
9468
  "datetime"
9378
9469
  ]
9379
9470
  },
9380
- {
9381
- "path": "unit",
9382
- "valueType": "string"
9383
- },
9384
9471
  {
9385
9472
  "path": "min",
9386
9473
  "valueType": "number"
@@ -9392,19 +9479,6 @@ Object.freeze({
9392
9479
  {
9393
9480
  "path": "step",
9394
9481
  "valueType": "number"
9395
- },
9396
- {
9397
- "path": "precision",
9398
- "valueType": "number"
9399
- },
9400
- {
9401
- "path": "format",
9402
- "valueType": "enum",
9403
- "enumValues": [
9404
- "date",
9405
- "time",
9406
- "datetime"
9407
- ]
9408
9482
  }
9409
9483
  ],
9410
9484
  "cover": [
@@ -9645,13 +9719,8 @@ Object.freeze({
9645
9719
  "path": "value",
9646
9720
  "valueType": "string"
9647
9721
  }, {
9648
- "path": "format",
9649
- "valueType": "enum",
9650
- "enumValues": [
9651
- "date",
9652
- "time",
9653
- "datetime"
9654
- ]
9722
+ "path": "timestamp",
9723
+ "valueType": "boolean"
9655
9724
  }],
9656
9725
  "event-emitter": [
9657
9726
  {
@@ -9724,6 +9793,24 @@ Object.freeze({
9724
9793
  "path": "detected",
9725
9794
  "valueType": "boolean"
9726
9795
  }],
9796
+ "group-active": [{
9797
+ "path": "active",
9798
+ "valueType": "boolean"
9799
+ }],
9800
+ "group-status": [
9801
+ {
9802
+ "path": "included",
9803
+ "valueType": "number"
9804
+ },
9805
+ {
9806
+ "path": "excluded",
9807
+ "valueType": "number"
9808
+ },
9809
+ {
9810
+ "path": "total",
9811
+ "valueType": "number"
9812
+ }
9813
+ ],
9727
9814
  "heating-valve": [{
9728
9815
  "path": "position",
9729
9816
  "valueType": "number"
@@ -9758,20 +9845,10 @@ Object.freeze({
9758
9845
  "valueType": "number"
9759
9846
  }
9760
9847
  ],
9761
- "humidity-sensor": [
9762
- {
9763
- "path": "percent",
9764
- "valueType": "number"
9765
- },
9766
- {
9767
- "path": "unit",
9768
- "valueType": "string"
9769
- },
9770
- {
9771
- "path": "precision",
9772
- "valueType": "number"
9773
- }
9774
- ],
9848
+ "humidity-sensor": [{
9849
+ "path": "percent",
9850
+ "valueType": "number"
9851
+ }],
9775
9852
  "image": [{
9776
9853
  "path": "url",
9777
9854
  "valueType": "string"
@@ -9979,6 +10056,10 @@ Object.freeze({
9979
10056
  ]
9980
10057
  }
9981
10058
  ],
10059
+ "meter": [{
10060
+ "path": "value",
10061
+ "valueType": "number"
10062
+ }],
9982
10063
  "motion": [
9983
10064
  {
9984
10065
  "path": "detected",
@@ -10110,20 +10191,10 @@ Object.freeze({
10110
10191
  "valueType": "boolean"
10111
10192
  }
10112
10193
  ],
10113
- "numeric-sensor": [
10114
- {
10115
- "path": "value",
10116
- "valueType": "number"
10117
- },
10118
- {
10119
- "path": "unit",
10120
- "valueType": "string"
10121
- },
10122
- {
10123
- "path": "precision",
10124
- "valueType": "number"
10125
- }
10126
- ],
10194
+ "numeric-sensor": [{
10195
+ "path": "value",
10196
+ "valueType": "number"
10197
+ }],
10127
10198
  "pet-feeder": [
10128
10199
  {
10129
10200
  "path": "foodLevel",
@@ -10215,14 +10286,6 @@ Object.freeze({
10215
10286
  {
10216
10287
  "path": "powerFactor",
10217
10288
  "valueType": "number"
10218
- },
10219
- {
10220
- "path": "unit",
10221
- "valueType": "string"
10222
- },
10223
- {
10224
- "path": "precision",
10225
- "valueType": "number"
10226
10289
  }
10227
10290
  ],
10228
10291
  "presence": [
@@ -10251,20 +10314,10 @@ Object.freeze({
10251
10314
  "valueType": "number"
10252
10315
  }
10253
10316
  ],
10254
- "pressure-sensor": [
10255
- {
10256
- "path": "hpa",
10257
- "valueType": "number"
10258
- },
10259
- {
10260
- "path": "unit",
10261
- "valueType": "string"
10262
- },
10263
- {
10264
- "path": "precision",
10265
- "valueType": "number"
10266
- }
10267
- ],
10317
+ "pressure-sensor": [{
10318
+ "path": "hpa",
10319
+ "valueType": "number"
10320
+ }],
10268
10321
  "privacy-mask": [{
10269
10322
  "path": "enabled",
10270
10323
  "valueType": "boolean"
@@ -10575,20 +10628,10 @@ Object.freeze({
10575
10628
  "path": "tampered",
10576
10629
  "valueType": "boolean"
10577
10630
  }],
10578
- "temperature-sensor": [
10579
- {
10580
- "path": "celsius",
10581
- "valueType": "number"
10582
- },
10583
- {
10584
- "path": "unit",
10585
- "valueType": "string"
10586
- },
10587
- {
10588
- "path": "precision",
10589
- "valueType": "number"
10590
- }
10591
- ],
10631
+ "temperature-sensor": [{
10632
+ "path": "celsius",
10633
+ "valueType": "number"
10634
+ }],
10592
10635
  "update": [
10593
10636
  {
10594
10637
  "path": "currentVersion",
@@ -10699,10 +10742,6 @@ Object.freeze({
10699
10742
  "path": "temperature",
10700
10743
  "valueType": "number"
10701
10744
  },
10702
- {
10703
- "path": "temperatureUnit",
10704
- "valueType": "string"
10705
- },
10706
10745
  {
10707
10746
  "path": "humidity",
10708
10747
  "valueType": "number"
@@ -10711,18 +10750,10 @@ Object.freeze({
10711
10750
  "path": "pressure",
10712
10751
  "valueType": "number"
10713
10752
  },
10714
- {
10715
- "path": "pressureUnit",
10716
- "valueType": "string"
10717
- },
10718
10753
  {
10719
10754
  "path": "windSpeed",
10720
10755
  "valueType": "number"
10721
10756
  },
10722
- {
10723
- "path": "windSpeedUnit",
10724
- "valueType": "string"
10725
- },
10726
10757
  {
10727
10758
  "path": "windBearing",
10728
10759
  "valueType": "number"
@@ -10772,7 +10803,7 @@ object({
10772
10803
  params: record(string(), union([string(), number()])).optional()
10773
10804
  });
10774
10805
  //#endregion
10775
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10806
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10776
10807
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10777
10808
  DeviceType["Camera"] = "camera";
10778
10809
  DeviceType["Hub"] = "hub";
@@ -10866,8 +10897,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10866
10897
  DeviceType["PetFeeder"] = "pet-feeder";
10867
10898
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10868
10899
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10869
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10900
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10901
+ * A plug-built dishwasher carries running-state and power instead. */
10870
10902
  DeviceType["Dishwasher"] = "dishwasher";
10903
+ /** Washing machine. A plug-built one is running while its power stays
10904
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10905
+ DeviceType["Washer"] = "washer";
10906
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10907
+ DeviceType["Dryer"] = "dryer";
10871
10908
  return DeviceType;
10872
10909
  }({});
10873
10910
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -11125,7 +11162,7 @@ function errMsg(err) {
11125
11162
  return String(err);
11126
11163
  }
11127
11164
  //#endregion
11128
- //#region ../types/dist/composition-kEl9mevu.mjs
11165
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
11129
11166
  /**
11130
11167
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11131
11168
  *
@@ -11440,6 +11477,13 @@ var table = {
11440
11477
  apply: () => {
11441
11478
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11442
11479
  }
11480
+ },
11481
+ schmitt: {
11482
+ minArgs: 3,
11483
+ maxArgs: 5,
11484
+ apply: () => {
11485
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11486
+ }
11443
11487
  }
11444
11488
  };
11445
11489
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11880,15 +11924,232 @@ function validateExpressionSource(src) {
11880
11924
  }
11881
11925
  return null;
11882
11926
  }
11883
- /** A composed device's stableId is this prefix plus the block id. */
11884
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11885
- /** Longest snippet a `code` source may carry (slice 4). */
11886
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11927
+ /**
11928
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11929
+ *
11930
+ * Standby of the control board is under 5 W; a motor or a heater is far
11931
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11932
+ * a cycle, so running starts only after 30 s above the line.
11933
+ *
11934
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11935
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11936
+ * report "finished" in the middle of the program, so the stop dwell is
11937
+ * 10 minutes on every type. The number is stored per type so one machine
11938
+ * can move later without moving the others.
11939
+ */
11940
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11941
+ var APPLIANCE_KINDS = [
11942
+ "washer",
11943
+ "dryer",
11944
+ "dishwasher"
11945
+ ];
11946
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11947
+ /**
11948
+ * Statistics and integrals over a history read (D944).
11949
+ *
11950
+ * Pure: the same points always give the same answer, so a restart recomputes
11951
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11952
+ * never the mean of the samples. A hole with no held value is not integrated
11953
+ * as zero. A truncated read is not published.
11954
+ */
11955
+ var STATISTIC_STATS = [
11956
+ "min",
11957
+ "max",
11958
+ "mean",
11959
+ "sum",
11960
+ "count",
11961
+ "change"
11962
+ ];
11963
+ /**
11964
+ * The range a statistics or integral helper asks history for (D944).
11965
+ *
11966
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11967
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11968
+ * is strictly longer than the longest that range can be. Forever qualifies.
11969
+ * Equal to a tier's retention is a refusal, not a short answer.
11970
+ */
11971
+ var MINUTE_MS = 6e4;
11972
+ var HOUR_MS = 60 * MINUTE_MS;
11973
+ var DAY_MS = 24 * HOUR_MS;
11974
+ var ROLLING_SPANS = [
11975
+ "1h",
11976
+ "24h",
11977
+ "2d",
11978
+ "7d",
11979
+ "30d",
11980
+ "custom"
11981
+ ];
11982
+ var CALENDAR_PERIODS = [
11983
+ "today",
11984
+ "yesterday",
11985
+ "this-week",
11986
+ "this-month"
11987
+ ];
11988
+ var RESET_PERIODS = [
11989
+ "never",
11990
+ "daily",
11991
+ "weekly",
11992
+ "monthly"
11993
+ ];
11994
+ 5 * MINUTE_MS;
11995
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11996
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11997
+ var rollingWindowObject = object({
11998
+ kind: literal("rolling"),
11999
+ span: _enum(ROLLING_SPANS),
12000
+ customMinutes: number().int().min(5).optional()
12001
+ }).check((payload) => {
12002
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
12003
+ code: "custom",
12004
+ message: "a custom range needs a duration in minutes",
12005
+ path: ["customMinutes"],
12006
+ input: payload.value
12007
+ });
12008
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
12009
+ code: "custom",
12010
+ message: "only a custom range carries a duration",
12011
+ path: ["customMinutes"],
12012
+ input: payload.value
12013
+ });
12014
+ });
12015
+ var RollingWindowSchema = rollingWindowObject;
12016
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
12017
+ kind: literal("calendar"),
12018
+ period: _enum(CALENDAR_PERIODS)
12019
+ })]);
12020
+ /**
12021
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
12022
+ * id. Split out so helper schemas can name it without a cycle through
12023
+ * `composition.ts` (D659).
12024
+ */
11887
12025
  var CompositionSourceRefSchema = object({
11888
12026
  /** The owning addon; `stableId` is unique only within it. */
11889
12027
  addonId: string().min(1),
11890
12028
  stableId: string().min(1)
11891
12029
  });
12030
+ var GROUP_TEMPLATE_IDS = [
12031
+ "temperature",
12032
+ "humidity",
12033
+ "pressure",
12034
+ "illuminance",
12035
+ "power",
12036
+ "energy",
12037
+ "battery",
12038
+ "numeric",
12039
+ "motion",
12040
+ "contact",
12041
+ "leak",
12042
+ "smoke",
12043
+ "occupancy",
12044
+ "binary",
12045
+ "switch",
12046
+ "light",
12047
+ "fan",
12048
+ "cover",
12049
+ "valve",
12050
+ "lock",
12051
+ "media-player",
12052
+ "climate"
12053
+ ];
12054
+ var NUMERIC_AGGREGATES = [
12055
+ "mean",
12056
+ "median",
12057
+ "min",
12058
+ "max",
12059
+ "sum"
12060
+ ];
12061
+ var BINARY_AGGREGATES = ["any", "all"];
12062
+ /** How an actuator group decides it is on. Unknown members never count as off. */
12063
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
12064
+ var AGGREGATE_REDUCERS = [
12065
+ "mean",
12066
+ "median",
12067
+ "min",
12068
+ "max",
12069
+ "sum",
12070
+ "any",
12071
+ "all"
12072
+ ];
12073
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
12074
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
12075
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
12076
+ /** One member. The field is stored so occupancy can name motion or binary. */
12077
+ var HelperMemberSchema = object({
12078
+ source: CompositionSourceRefSchema,
12079
+ cap: string().min(1),
12080
+ fieldPath: string().min(1)
12081
+ });
12082
+ var HelperSpecSchema = discriminatedUnion("kind", [
12083
+ object({
12084
+ kind: literal("group"),
12085
+ templateId: GroupTemplateIdSchema,
12086
+ aggregate: AggregateReducerSchema,
12087
+ members: array(HelperMemberSchema).min(1).max(64),
12088
+ /** Actuator groups only. Readings leave this unset. */
12089
+ active: ActiveCriterionSchema.optional()
12090
+ }),
12091
+ object({
12092
+ kind: literal("appliance"),
12093
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
12094
+ appliance: _enum(APPLIANCE_KINDS),
12095
+ thresholdWatts: number().positive(),
12096
+ startDwellMs: number().int().nonnegative(),
12097
+ stopDwellMs: number().int().nonnegative(),
12098
+ member: HelperMemberSchema
12099
+ }),
12100
+ object({
12101
+ kind: literal("statistic"),
12102
+ stat: _enum(STATISTIC_STATS),
12103
+ window: StatisticWindowSchema,
12104
+ source: HelperMemberSchema
12105
+ }),
12106
+ object({
12107
+ kind: literal("integral"),
12108
+ reset: _enum(RESET_PERIODS),
12109
+ output: _enum(["energy", "meter"]),
12110
+ source: HelperMemberSchema
12111
+ }),
12112
+ object({
12113
+ kind: literal("threshold"),
12114
+ source: HelperMemberSchema,
12115
+ high: number(),
12116
+ low: number(),
12117
+ onForMs: number().int().nonnegative(),
12118
+ offForMs: number().int().nonnegative()
12119
+ }),
12120
+ object({
12121
+ kind: literal("arithmetic"),
12122
+ op: _enum([
12123
+ "sum",
12124
+ "difference",
12125
+ "ratio"
12126
+ ]),
12127
+ inputs: array(HelperMemberSchema).min(2).max(64)
12128
+ }),
12129
+ object({
12130
+ kind: literal("expression"),
12131
+ expr: string().min(1),
12132
+ inputs: array(object({
12133
+ name: string().min(1),
12134
+ member: HelperMemberSchema
12135
+ })).min(1).max(64),
12136
+ output: _enum(["number", "boolean"])
12137
+ }),
12138
+ object({
12139
+ kind: literal("derivative"),
12140
+ window: RollingWindowSchema,
12141
+ per: _enum([
12142
+ "second",
12143
+ "minute",
12144
+ "hour"
12145
+ ]),
12146
+ source: HelperMemberSchema
12147
+ })
12148
+ ]);
12149
+ /** A composed device's stableId is this prefix plus the block id. */
12150
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12151
+ /** Longest snippet a `code` source may carry (slice 4). */
12152
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11892
12153
  /** One field of one capability of one source device. */
11893
12154
  var CompositionFieldReadSchema = object({
11894
12155
  source: CompositionSourceRefSchema,
@@ -11932,7 +12193,59 @@ var CompositionCodeSourceSchema = object({
11932
12193
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11933
12194
  CompositionFromSourceSchema,
11934
12195
  CompositionExpressionSourceSchema,
11935
- CompositionCodeSourceSchema
12196
+ CompositionCodeSourceSchema,
12197
+ object({
12198
+ kind: literal("aggregate"),
12199
+ reducer: _enum([
12200
+ "mean",
12201
+ "median",
12202
+ "min",
12203
+ "max",
12204
+ "sum",
12205
+ "any",
12206
+ "all"
12207
+ ]),
12208
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12209
+ /**
12210
+ * Set when the reads are an enum state, not a yes/no: the listed states
12211
+ * count as active (a cover is `open`). A state outside the list is inactive;
12212
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
12213
+ */
12214
+ activeWhen: array(string().min(1)).min(1).optional(),
12215
+ /** `value` is the reading; the others are the member counts. */
12216
+ publish: _enum([
12217
+ "value",
12218
+ "included",
12219
+ "excluded",
12220
+ "total"
12221
+ ]),
12222
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12223
+ empty: _enum([
12224
+ "null",
12225
+ "omit",
12226
+ "offline"
12227
+ ])
12228
+ }),
12229
+ object({
12230
+ kind: literal("series"),
12231
+ read: CompositionFieldReadSchema,
12232
+ /** `value` is the statistic or the integral. The others are the gap counts. */
12233
+ publish: _enum([
12234
+ "value",
12235
+ "included",
12236
+ "excluded",
12237
+ "total"
12238
+ ]),
12239
+ /**
12240
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
12241
+ * device unseeded until history answers, so a restart cannot publish 0.
12242
+ */
12243
+ empty: _enum([
12244
+ "null",
12245
+ "omit",
12246
+ "offline"
12247
+ ])
12248
+ })
11936
12249
  ]);
11937
12250
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11938
12251
  kind: literal("forward"),
@@ -11978,7 +12291,13 @@ var CompositionSchema = object({
11978
12291
  kind: literal("existing"),
11979
12292
  device: CompositionSourceRefSchema
11980
12293
  })]),
11981
- features: array(CompositionFeatureSchema).min(1).max(16)
12294
+ features: array(CompositionFeatureSchema).min(1).max(16),
12295
+ /**
12296
+ * Present when the block is a helper template. Absent on a free
12297
+ * composition (the room example, device 4603). The editor reads this,
12298
+ * never the compiled features.
12299
+ */
12300
+ helper: HelperSpecSchema.optional()
11982
12301
  }).superRefine((composition, ctx) => {
11983
12302
  const seen = /* @__PURE__ */ new Set();
11984
12303
  composition.features.forEach((feature, index) => {
@@ -17461,7 +17780,22 @@ var BrokerProviderInfoSchema = object({
17461
17780
  */
17462
17781
  interactiveConnectPath: string().min(1).optional(),
17463
17782
  /** Relative hub path to disconnect the linked account (same popup pattern). */
17464
- interactiveDisconnectPath: string().min(1).optional()
17783
+ interactiveDisconnectPath: string().min(1).optional(),
17784
+ /**
17785
+ * The kind signs in through a server-held interactive session instead of
17786
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17787
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17788
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17789
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17790
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17791
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17792
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17793
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17794
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17795
+ * offers it as the default path; the fields the session fills are
17796
+ * marked `filledBySignIn` in the kind's settings schema.
17797
+ */
17798
+ signIn: object({ basePath: string().min(1) }).optional()
17465
17799
  }))
17466
17800
  });
17467
17801
  var ListInputSchema = object({
@@ -18456,7 +18790,12 @@ var CompositionProblemSchema = object({
18456
18790
  "item-field-reserved",
18457
18791
  "target-missing",
18458
18792
  "target-unreadable",
18459
- "not-replaceable"
18793
+ "not-replaceable",
18794
+ "native-capability-replace",
18795
+ "mixed-quantity",
18796
+ "helper-mismatch",
18797
+ "member-constraint",
18798
+ "retention"
18460
18799
  ]),
18461
18800
  path: string(),
18462
18801
  message: string()
@@ -18477,7 +18816,8 @@ var CompositionUnavailableModeSchema = _enum([
18477
18816
  "null",
18478
18817
  "offline",
18479
18818
  "unknown",
18480
- "release"
18819
+ "release",
18820
+ "omit"
18481
18821
  ]);
18482
18822
  /**
18483
18823
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18514,9 +18854,11 @@ var CompositionFieldPlanSchema = object({
18514
18854
  reads: array(CompositionFieldReadSchema)
18515
18855
  });
18516
18856
  /**
18517
- * What a feature does to its target (D663): `new` builds a capability on a new
18518
- * device; on an existing device it `add`s a capability the device lacks, or
18519
- * `replace`s named fields of one it has natively.
18857
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18858
+ * new device; on an existing device it `add`s a capability the device lacks.
18859
+ * Replacing a capability the device already serves is refused
18860
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18861
+ * wire for a claim the hub already holds.
18520
18862
  */
18521
18863
  var CompositionFeatureModeSchema = _enum([
18522
18864
  "new",
@@ -18687,8 +19029,392 @@ Object.freeze({
18687
19029
  latchedAt: {
18688
19030
  kind: "fixed",
18689
19031
  result: ["number", "null"]
19032
+ },
19033
+ schmitt: {
19034
+ kind: "fixed",
19035
+ result: ["boolean", "null"]
18690
19036
  }
18691
19037
  });
19038
+ /**
19039
+ * One catalog row per base reading a group helper can build (D944 §3).
19040
+ * Data, not a form default written into the block.
19041
+ */
19042
+ var NOW$2 = {
19043
+ fieldPath: "lastFetchedAt",
19044
+ expr: "now"
19045
+ };
19046
+ var CHANGED = {
19047
+ fieldPath: "lastChangedAt",
19048
+ expr: "now"
19049
+ };
19050
+ function numeric(input) {
19051
+ return {
19052
+ ...input,
19053
+ family: "numeric",
19054
+ deviceType: DeviceType.Sensor,
19055
+ memberDeviceType: DeviceType.Sensor,
19056
+ memberDeviceTypes: [DeviceType.Sensor],
19057
+ activeWhen: void 0,
19058
+ commandCaps: [],
19059
+ baseUnit: baseUnitOf(input.quantity),
19060
+ aggregates: NUMERIC_AGGREGATES,
19061
+ accept: [{
19062
+ cap: input.outputCap,
19063
+ fieldPath: input.outputField
19064
+ }]
19065
+ };
19066
+ }
19067
+ function binary(input) {
19068
+ return {
19069
+ ...input,
19070
+ family: "binary",
19071
+ deviceType: DeviceType.Sensor,
19072
+ memberDeviceType: DeviceType.Sensor,
19073
+ memberDeviceTypes: [DeviceType.Sensor],
19074
+ activeWhen: void 0,
19075
+ commandCaps: [],
19076
+ quantity: "none",
19077
+ baseUnit: void 0,
19078
+ aggregates: BINARY_AGGREGATES,
19079
+ empty: "offline"
19080
+ };
19081
+ }
19082
+ var GROUP_TEMPLATES = [
19083
+ numeric({
19084
+ id: "temperature",
19085
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
19086
+ role: DeviceRole.TemperatureSensor,
19087
+ outputCap: "temperature-sensor",
19088
+ outputField: "celsius",
19089
+ quantity: "temperature",
19090
+ defaultAggregate: "mean",
19091
+ empty: "null",
19092
+ companions: [NOW$2]
19093
+ }),
19094
+ numeric({
19095
+ id: "humidity",
19096
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
19097
+ role: DeviceRole.HumiditySensor,
19098
+ outputCap: "humidity-sensor",
19099
+ outputField: "percent",
19100
+ quantity: "humidity",
19101
+ defaultAggregate: "mean",
19102
+ empty: "null",
19103
+ companions: [NOW$2]
19104
+ }),
19105
+ numeric({
19106
+ id: "pressure",
19107
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
19108
+ role: DeviceRole.PressureSensor,
19109
+ outputCap: "pressure-sensor",
19110
+ outputField: "hpa",
19111
+ quantity: "pressure",
19112
+ defaultAggregate: "mean",
19113
+ empty: "null",
19114
+ companions: [NOW$2]
19115
+ }),
19116
+ numeric({
19117
+ id: "illuminance",
19118
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
19119
+ role: DeviceRole.AmbientLightSensor,
19120
+ outputCap: "ambient-light-sensor",
19121
+ outputField: "lux",
19122
+ quantity: "illuminance",
19123
+ defaultAggregate: "mean",
19124
+ empty: "null",
19125
+ companions: [NOW$2]
19126
+ }),
19127
+ numeric({
19128
+ id: "power",
19129
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
19130
+ role: DeviceRole.PowerSensor,
19131
+ outputCap: "power-meter",
19132
+ outputField: "watts",
19133
+ quantity: "power",
19134
+ defaultAggregate: "sum",
19135
+ empty: "omit",
19136
+ companions: [NOW$2]
19137
+ }),
19138
+ numeric({
19139
+ id: "energy",
19140
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
19141
+ role: DeviceRole.EnergySensor,
19142
+ outputCap: "power-meter",
19143
+ outputField: "kwhTotal",
19144
+ quantity: "energy",
19145
+ defaultAggregate: "sum",
19146
+ empty: "omit",
19147
+ companions: [NOW$2]
19148
+ }),
19149
+ numeric({
19150
+ id: "battery",
19151
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
19152
+ role: DeviceRole.BatterySensor,
19153
+ outputCap: "battery",
19154
+ outputField: "percentage",
19155
+ quantity: "percentage",
19156
+ defaultAggregate: "mean",
19157
+ empty: "null",
19158
+ companions: [
19159
+ {
19160
+ fieldPath: "charging",
19161
+ expr: "\"unknown\""
19162
+ },
19163
+ {
19164
+ fieldPath: "sleeping",
19165
+ expr: "false"
19166
+ },
19167
+ {
19168
+ fieldPath: "lastUpdated",
19169
+ expr: "now"
19170
+ }
19171
+ ]
19172
+ }),
19173
+ numeric({
19174
+ id: "numeric",
19175
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
19176
+ role: DeviceRole.NumericSensor,
19177
+ outputCap: "numeric-sensor",
19178
+ outputField: "value",
19179
+ quantity: "device",
19180
+ defaultAggregate: "mean",
19181
+ empty: "null",
19182
+ companions: [NOW$2]
19183
+ }),
19184
+ binary({
19185
+ id: "motion",
19186
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
19187
+ role: DeviceRole.MotionSensor,
19188
+ outputCap: "motion",
19189
+ outputField: "detected",
19190
+ defaultAggregate: "any",
19191
+ accept: [{
19192
+ cap: "motion",
19193
+ fieldPath: "detected"
19194
+ }],
19195
+ companions: [{
19196
+ fieldPath: "lastDetectedAt",
19197
+ expr: "null"
19198
+ }, {
19199
+ fieldPath: "autoClearAfterMs",
19200
+ expr: "null"
19201
+ }]
19202
+ }),
19203
+ binary({
19204
+ id: "contact",
19205
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
19206
+ role: DeviceRole.ContactSensor,
19207
+ outputCap: "contact",
19208
+ outputField: "entryOpen",
19209
+ defaultAggregate: "any",
19210
+ accept: [{
19211
+ cap: "contact",
19212
+ fieldPath: "entryOpen"
19213
+ }],
19214
+ companions: [CHANGED]
19215
+ }),
19216
+ binary({
19217
+ id: "leak",
19218
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
19219
+ role: DeviceRole.LeakSensor,
19220
+ outputCap: "flood",
19221
+ outputField: "flooded",
19222
+ defaultAggregate: "any",
19223
+ accept: [{
19224
+ cap: "flood",
19225
+ fieldPath: "flooded"
19226
+ }],
19227
+ companions: [CHANGED]
19228
+ }),
19229
+ binary({
19230
+ id: "smoke",
19231
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
19232
+ role: DeviceRole.SmokeSensor,
19233
+ outputCap: "smoke",
19234
+ outputField: "detected",
19235
+ defaultAggregate: "any",
19236
+ accept: [{
19237
+ cap: "smoke",
19238
+ fieldPath: "detected"
19239
+ }],
19240
+ companions: [CHANGED]
19241
+ }),
19242
+ binary({
19243
+ id: "occupancy",
19244
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
19245
+ role: DeviceRole.BinarySensor,
19246
+ outputCap: "binary",
19247
+ outputField: "on",
19248
+ defaultAggregate: "any",
19249
+ accept: [{
19250
+ cap: "motion",
19251
+ fieldPath: "detected"
19252
+ }, {
19253
+ cap: "binary",
19254
+ fieldPath: "on"
19255
+ }],
19256
+ companions: [CHANGED]
19257
+ }),
19258
+ binary({
19259
+ id: "binary",
19260
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
19261
+ role: DeviceRole.BinarySensor,
19262
+ outputCap: "binary",
19263
+ outputField: "on",
19264
+ defaultAggregate: "any",
19265
+ accept: [{
19266
+ cap: "binary",
19267
+ fieldPath: "on"
19268
+ }],
19269
+ companions: [CHANGED]
19270
+ }),
19271
+ actuator({
19272
+ id: "switch",
19273
+ 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.",
19274
+ deviceType: DeviceType.Switch,
19275
+ outputCap: "switch",
19276
+ outputField: "on",
19277
+ member: {
19278
+ cap: "switch",
19279
+ fieldPath: "on"
19280
+ },
19281
+ commandCaps: ["switch"]
19282
+ }),
19283
+ actuator({
19284
+ id: "light",
19285
+ 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.",
19286
+ deviceType: DeviceType.Light,
19287
+ outputCap: "switch",
19288
+ outputField: "on",
19289
+ member: {
19290
+ cap: "switch",
19291
+ fieldPath: "on"
19292
+ },
19293
+ commandCaps: [
19294
+ "switch",
19295
+ "brightness",
19296
+ "color"
19297
+ ]
19298
+ }),
19299
+ actuator({
19300
+ id: "fan",
19301
+ 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.",
19302
+ deviceType: DeviceType.Fan,
19303
+ outputCap: "switch",
19304
+ outputField: "on",
19305
+ member: {
19306
+ cap: "switch",
19307
+ fieldPath: "on"
19308
+ },
19309
+ commandCaps: ["switch", "fan-control"]
19310
+ }),
19311
+ actuator({
19312
+ id: "cover",
19313
+ 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.",
19314
+ deviceType: DeviceType.Cover,
19315
+ outputCap: "group-active",
19316
+ outputField: "active",
19317
+ member: {
19318
+ cap: "cover",
19319
+ fieldPath: "state"
19320
+ },
19321
+ activeWhen: [
19322
+ "open",
19323
+ "opening",
19324
+ "stopped"
19325
+ ],
19326
+ commandCaps: ["cover"]
19327
+ }),
19328
+ actuator({
19329
+ id: "valve",
19330
+ 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.",
19331
+ deviceType: DeviceType.Valve,
19332
+ outputCap: "group-active",
19333
+ outputField: "active",
19334
+ member: {
19335
+ cap: "valve",
19336
+ fieldPath: "state"
19337
+ },
19338
+ activeWhen: [
19339
+ "open",
19340
+ "opening",
19341
+ "stopped"
19342
+ ],
19343
+ commandCaps: ["valve"]
19344
+ }),
19345
+ actuator({
19346
+ id: "lock",
19347
+ 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.",
19348
+ deviceType: DeviceType.Lock,
19349
+ outputCap: "group-active",
19350
+ outputField: "active",
19351
+ member: {
19352
+ cap: "lock-control",
19353
+ fieldPath: "state"
19354
+ },
19355
+ activeWhen: ["locked"],
19356
+ commandCaps: ["lock-control"]
19357
+ }),
19358
+ actuator({
19359
+ id: "media-player",
19360
+ 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.",
19361
+ deviceType: DeviceType.MediaPlayer,
19362
+ outputCap: "group-active",
19363
+ outputField: "active",
19364
+ member: {
19365
+ cap: "media-player",
19366
+ fieldPath: "state"
19367
+ },
19368
+ activeWhen: ["playing", "buffering"],
19369
+ commandCaps: ["media-player"]
19370
+ }),
19371
+ actuator({
19372
+ id: "climate",
19373
+ 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.",
19374
+ deviceType: DeviceType.Thermostat,
19375
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
19376
+ outputCap: "group-active",
19377
+ outputField: "active",
19378
+ member: {
19379
+ cap: "climate-control",
19380
+ fieldPath: "mode"
19381
+ },
19382
+ activeWhen: [
19383
+ "heat",
19384
+ "cool",
19385
+ "auto",
19386
+ "heat_cool",
19387
+ "fan_only",
19388
+ "dry"
19389
+ ],
19390
+ commandCaps: ["climate-control"]
19391
+ })
19392
+ ];
19393
+ function actuator(input) {
19394
+ return {
19395
+ id: input.id,
19396
+ summary: input.summary,
19397
+ deviceType: input.deviceType,
19398
+ outputCap: input.outputCap,
19399
+ outputField: input.outputField,
19400
+ commandCaps: input.commandCaps,
19401
+ family: "actuator",
19402
+ role: void 0,
19403
+ memberDeviceType: input.deviceType,
19404
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
19405
+ activeWhen: input.activeWhen,
19406
+ quantity: "none",
19407
+ baseUnit: void 0,
19408
+ aggregates: BINARY_AGGREGATES,
19409
+ defaultAggregate: "any",
19410
+ empty: "offline",
19411
+ accept: [input.member],
19412
+ companions: [CHANGED]
19413
+ };
19414
+ }
19415
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19416
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19417
+ DeviceType.Camera;
18692
19418
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18693
19419
  /**
18694
19420
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20402,12 +21128,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20402
21128
  * providers detect renames by matching `uniqueId` and patch `id`
20403
21129
  * in-place so dispatch keeps working.
20404
21130
  *
20405
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20406
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20407
- * state push and are the single source of truth. The `device_class` tag is
20408
- * an HA-specific input that is consumed at the discovery boundary
20409
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20410
- * stored after that point.
21131
+ * **Display unit and precision are NOT stored here.** They are a read-time
21132
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
21133
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
21134
+ * neither is repeated on each sample. The `device_class` tag is an
21135
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
21136
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20411
21137
  *
20412
21138
  * The `raw` escape hatch carries provider-specific attributes that don't
20413
21139
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20421,6 +21147,14 @@ var SourceInfoSchema = object({
20421
21147
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20422
21148
  * Used to detect rename when `id` changes. */
20423
21149
  uniqueId: string().optional(),
21150
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
21151
+ quantity: ReadingQuantitySchema.optional(),
21152
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
21153
+ dateTimeShape: _enum([
21154
+ "date",
21155
+ "time",
21156
+ "datetime"
21157
+ ]).optional(),
20424
21158
  /** Provider-specific extras that don't fit the structured slots.
20425
21159
  * Whitelist what goes here per provider — do NOT dump entire HA
20426
21160
  * attribute blobs (would explode the meta JSON column). */
@@ -41351,7 +42085,8 @@ method(object({
41351
42085
  "enum"
41352
42086
  ]),
41353
42087
  enumValues: array(string()).optional(),
41354
- item: boolean().optional()
42088
+ item: boolean().optional(),
42089
+ quantity: ReadingQuantitySchema.optional()
41355
42090
  })).readonly(),
41356
42091
  itemArray: object({
41357
42092
  path: string(),
@@ -43720,7 +44455,7 @@ var NetworkAccessStatusSchema = object({
43720
44455
  });
43721
44456
  /**
43722
44457
  * Optional, richer endpoint shape returned by providers that expose
43723
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44458
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43724
44459
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43725
44460
  * the originating provider config (mode + sourcePort) so the
43726
44461
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -45159,8 +45894,8 @@ var MaskPolygonShapeSchema = object({
45159
45894
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45160
45895
  var MaskGridShapeSchema = object({
45161
45896
  kind: literal("grid"),
45162
- gridWidth: number(),
45163
- gridHeight: number(),
45897
+ gridWidth: number().meta(readingMeta("none")),
45898
+ gridHeight: number().meta(readingMeta("none")),
45164
45899
  cells: array(boolean())
45165
45900
  });
45166
45901
  discriminatedUnion("kind", [
@@ -45272,6 +46007,8 @@ var NcSystemEventKindSchema = _enum([
45272
46007
  "detection-blind",
45273
46008
  "motion-noisy",
45274
46009
  "motion-calm",
46010
+ "package-llm-silent",
46011
+ "package-llm-answered",
45275
46012
  "recording-stalled",
45276
46013
  "recording-healthy",
45277
46014
  "recording-degraded",
@@ -46361,7 +47098,20 @@ var NcRuleInputSchema = object({
46361
47098
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46362
47099
  * path does not run Zod defaults.
46363
47100
  */
46364
- groupingSec: number().int().min(5).max(60).optional()
47101
+ groupingSec: number().int().min(5).max(60).optional(),
47102
+ /**
47103
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
47104
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
47105
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
47106
+ * same mark the Library writes, and records whose notification it was and
47107
+ * when. A channel with no buttons gets the same two choices as links.
47108
+ *
47109
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
47110
+ * cap path): absent and `false` both mean off, which is what every rule
47111
+ * authored before this field meant. An editor turns it off by writing
47112
+ * `false`, so it needs no `clear` key.
47113
+ */
47114
+ askVerdict: boolean().optional()
46365
47115
  });
46366
47116
  /**
46367
47117
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -56337,6 +57087,28 @@ var PackageVersionInfoSchema = object({
56337
57087
  deprecated: string().optional(),
56338
57088
  distTags: array(string()).readonly()
56339
57089
  });
57090
+ /**
57091
+ * One published document. `ok` is markdown from the registry. `missing` means
57092
+ * the registry answered and the file was not there. `error` is a registry or
57093
+ * tarball failure — never an empty string standing in for either.
57094
+ */
57095
+ var PackageDocBodySchema = discriminatedUnion("status", [
57096
+ object({
57097
+ status: literal("ok"),
57098
+ markdown: string()
57099
+ }),
57100
+ object({ status: literal("missing") }),
57101
+ object({
57102
+ status: literal("error"),
57103
+ message: string()
57104
+ })
57105
+ ]);
57106
+ var PackageDocsSchema = object({
57107
+ name: string(),
57108
+ version: string(),
57109
+ readme: PackageDocBodySchema,
57110
+ changelog: PackageDocBodySchema
57111
+ });
56340
57112
  var SearchResultSchema = object({
56341
57113
  name: string(),
56342
57114
  version: string(),
@@ -56555,7 +57327,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56555
57327
  }), object({ success: literal(true) }), {
56556
57328
  kind: "mutation",
56557
57329
  auth: "admin"
56558
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57330
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57331
+ name: string().min(1),
57332
+ version: string().min(1).optional()
57333
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56559
57334
  kind: "mutation",
56560
57335
  auth: "admin"
56561
57336
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56605,46 +57380,27 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56605
57380
  }), LogStreamEntrySchema, { kind: "subscription" });
56606
57381
  object({
56607
57382
  /** Carbon dioxide concentration in ppm. */
56608
- co2Ppm: number().min(0).optional(),
57383
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56609
57384
  /** Total volatile organic compounds in ppb. */
56610
- vocPpb: number().min(0).optional(),
57385
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56611
57386
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56612
- pm25: number().min(0).optional(),
57387
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56613
57388
  /** Particulate matter ≤ 10 μm in µg/m³. */
56614
- pm10: number().min(0).optional(),
57389
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56615
57390
  /** Composite AQI value (typically 0..500). */
56616
- aqi: number().optional(),
57391
+ aqi: number().optional().meta(readingMeta("index")),
56617
57392
  /** Ms epoch when the slice was last updated. */
56618
- lastFetchedAt: number(),
56619
- /** Live display unit of the single metric this slice carries (e.g. HA
56620
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56621
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56622
- * per slice is unambiguous. */
56623
- unit: string().optional(),
56624
- /** Suggested decimal places for numeric display.
56625
- * Populated live from the upstream source when provided (e.g. HA
56626
- * `attributes.suggested_display_precision`). Falls back to
56627
- * auto-formatting when absent. */
56628
- precision: number().int().min(0).max(10).optional()
57393
+ lastFetchedAt: number()
56629
57394
  });
56630
57395
  DeviceType.Sensor;
56631
57396
  object({
56632
- /** Current illuminance in lux (lx). */
56633
- lux: number().min(0).meta({
56634
- unit: "lx",
57397
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57398
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57399
+ precision: 0,
56635
57400
  label: "Illuminance"
56636
- }),
57401
+ })),
56637
57402
  /** Ms epoch when the slice was last updated. */
56638
- lastFetchedAt: number(),
56639
- /** Live display unit from the upstream source (e.g. HA
56640
- * `attributes.unit_of_measurement`). The UI prefers this over the
56641
- * role's canonical unit. Absent → fall back to the canonical unit. */
56642
- unit: string().optional(),
56643
- /** Suggested decimal places for numeric display.
56644
- * Populated live from the upstream source when provided (e.g. HA
56645
- * `attributes.suggested_display_precision`). Falls back to
56646
- * auto-formatting when absent. */
56647
- precision: number().int().min(0).max(10).optional()
57403
+ lastFetchedAt: number()
56648
57404
  });
56649
57405
  DeviceType.Sensor;
56650
57406
  /**
@@ -56789,10 +57545,7 @@ var BatteryStatusSchema = object({
56789
57545
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56790
57546
  * declines to band a non-finite reading, which is the correct shape.
56791
57547
  */
56792
- percentage: number().min(0).max(100).nullable().meta({
56793
- unit: "%",
56794
- label: "Charge"
56795
- }),
57548
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56796
57549
  /**
56797
57550
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56798
57551
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56819,7 +57572,14 @@ var BatteryStatusSchema = object({
56819
57572
  */
56820
57573
  sleeping: boolean().meta({ label: "Sleeping" }),
56821
57574
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56822
- lastUpdated: number(),
57575
+ lastUpdated: number().meta(readingMeta("none")),
57576
+ /**
57577
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57578
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57579
+ * that only report charge. A second reading on this cap, not a second
57580
+ * `numeric-sensor` on the same device.
57581
+ */
57582
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56823
57583
  /**
56824
57584
  * Ms epoch of the last time the device PROVED it was reachable — a
56825
57585
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56859,7 +57619,7 @@ var BatteryStatusSchema = object({
56859
57619
  */
56860
57620
  binary: boolean().optional().meta({ label: "Binary indicator" })
56861
57621
  });
56862
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat;
57622
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter;
56863
57623
  method(object({
56864
57624
  deviceId: number(),
56865
57625
  /** Bound on the wait. Sensible range 3000–10000ms. */
@@ -56879,7 +57639,7 @@ object({
56879
57639
  DeviceType.Sensor;
56880
57640
  object({
56881
57641
  /** Current level as 0..100 inclusive. Firmware-reported. */
56882
- percentage: number().min(0).max(100),
57642
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56883
57643
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56884
57644
  lastChangedAt: number()
56885
57645
  });
@@ -57264,21 +58024,21 @@ object({
57264
58024
  /** Single setpoint in Celsius. Used by single-target modes
57265
58025
  * (heat / cool / dry). Null when the device is in a range mode
57266
58026
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57267
- target: number().nullable(),
58027
+ target: number().nullable().meta(readingMeta("temperature")),
57268
58028
  /** Upper bound of the dual setpoint in Celsius. Populated only
57269
58029
  * in `heat_cool` mode. */
57270
- targetHigh: number().nullable(),
58030
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57271
58031
  /** Lower bound of the dual setpoint in Celsius. Populated only
57272
58032
  * in `heat_cool` mode. */
57273
- targetLow: number().nullable(),
58033
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57274
58034
  /** Target relative humidity (0..100). Null when the device has no
57275
58035
  * humidity control. */
57276
- targetHumidity: number().min(0).max(100).nullable(),
58036
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57277
58037
  /** Current measured temperature in Celsius. Read-only — pushed by
57278
58038
  * the device's internal sensor. */
57279
- currentTemp: number().nullable(),
58039
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57280
58040
  /** Current measured relative humidity (0..100). Read-only. */
57281
- currentHumidity: number().min(0).max(100).nullable(),
58041
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57282
58042
  /** Vertical louver swing on/off. Null when the device has no vertical
57283
58043
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57284
58044
  swingVertical: boolean().nullable(),
@@ -57286,11 +58046,11 @@ object({
57286
58046
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57287
58047
  swingHorizontal: boolean().nullable(),
57288
58048
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57289
- minTemp: number().nullable().optional(),
58049
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57290
58050
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57291
- maxTemp: number().nullable().optional(),
58051
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57292
58052
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57293
- tempStep: number().positive().nullable().optional(),
58053
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57294
58054
  /** Ms epoch when the slice was last updated (push or command). */
57295
58055
  lastFetchedAt: number()
57296
58056
  });
@@ -57369,17 +58129,17 @@ DeviceType.Thermostat, DeviceType.Climate, method(object({
57369
58129
  * modes to expose in the picker without a slice fetch.
57370
58130
  */
57371
58131
  var RgbTripletSchema = object({
57372
- r: number().int().min(0).max(255),
57373
- g: number().int().min(0).max(255),
57374
- b: number().int().min(0).max(255)
58132
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58133
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58134
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57375
58135
  });
57376
58136
  var HsvTripletSchema = object({
57377
58137
  /** Hue in degrees, 0..360. */
57378
- h: number().min(0).max(360),
58138
+ h: number().min(0).max(360).meta(readingMeta("none")),
57379
58139
  /** Saturation as 0..100 inclusive. */
57380
- s: number().min(0).max(100),
58140
+ s: number().min(0).max(100).meta(readingMeta("none")),
57381
58141
  /** Value/brightness as 0..100 inclusive. */
57382
- v: number().min(0).max(100)
58142
+ v: number().min(0).max(100).meta(readingMeta("none"))
57383
58143
  });
57384
58144
  var ColorInputSchema = discriminatedUnion("mode", [
57385
58145
  object({
@@ -57408,7 +58168,7 @@ object({
57408
58168
  /** Populated when `mode === 'hsv'`. */
57409
58169
  hsv: HsvTripletSchema.optional(),
57410
58170
  /** Populated when `mode === 'mired'`. */
57411
- mireds: number().int().optional(),
58171
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57412
58172
  /** Ms epoch of the last operator-driven change. */
57413
58173
  lastChangedAt: number()
57414
58174
  });
@@ -57532,18 +58292,12 @@ _enum([
57532
58292
  "empty",
57533
58293
  "full"
57534
58294
  ]);
57535
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57536
- * each. */
57537
- var ConsumableRemainingUnitSchema = _enum([
57538
- "days",
57539
- "hours",
57540
- "cycles"
57541
- ]);
57542
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57543
- * days). `value` null = unknown (D393). */
58295
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58296
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58297
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57544
58298
  var ConsumableRemainingSchema = object({
57545
- value: number().min(0).nullable(),
57546
- unit: ConsumableRemainingUnitSchema
58299
+ value: number().min(0).nullable().meta(readingMeta("device")),
58300
+ kind: ConsumableRemainingKindSchema
57547
58301
  });
57548
58302
  var ConsumablesStatusSchema = object({
57549
58303
  items: array(object({
@@ -57552,7 +58306,7 @@ var ConsumablesStatusSchema = object({
57552
58306
  /** Display name. */
57553
58307
  label: string().min(1),
57554
58308
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57555
- level: number().min(0).max(100).nullable(),
58309
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57556
58310
  /** The provider's discrete verdict when known (binary mode). */
57557
58311
  status: ConsumableStatusSchema.nullable(),
57558
58312
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57569,7 +58323,7 @@ var ConsumablesStatusSchema = object({
57569
58323
  })),
57570
58324
  lastChangedAt: number()
57571
58325
  });
57572
- Object.values(DeviceType), [...ConsumableRemainingUnitSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
58326
+ Object.values(DeviceType), [...ConsumableRemainingKindSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57573
58327
  Object.values(DeviceType), method(object({
57574
58328
  deviceId: number().int().nonnegative(),
57575
58329
  key: string().min(1)
@@ -57594,39 +58348,17 @@ object({
57594
58348
  /** Current value. Numeric kind → number; select/text/datetime → string.
57595
58349
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
57596
58350
  * or time-only — HA accepts all three forms). */
57597
- value: union([number(), string()]),
58351
+ value: union([number().meta(readingMeta("device")), string()]),
57598
58352
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
57599
58353
  options: array(string()),
57600
58354
  /** Ms epoch when the slice was last updated. */
57601
58355
  lastChangedAt: number(),
57602
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
57603
- * Populated live from HA `attributes.unit_of_measurement` on each push.
57604
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57605
- unit: string().optional(),
57606
- /** Minimum allowed value. Populated live from HA `attributes.min`.
57607
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57608
- min: number().optional(),
57609
- /** Maximum allowed value. Populated live from HA `attributes.max`.
57610
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57611
- max: number().optional(),
57612
- /** Step increment. Populated live from HA `attributes.step`.
57613
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57614
- step: number().optional(),
57615
- /** Suggested decimal places for numeric display. Populated live from HA
57616
- * `attributes.suggested_display_precision`, or derived from `step` when the
57617
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
57618
- * other kinds. Falls back to auto-formatting when absent. */
57619
- precision: number().int().min(0).max(10).optional(),
57620
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
57621
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
57622
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
57623
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
57624
- * `has_date`/`has_time` attributes on each push. */
57625
- format: _enum([
57626
- "date",
57627
- "time",
57628
- "datetime"
57629
- ]).optional()
58356
+ /** Minimum allowed value, in the device quantity's base unit. */
58357
+ min: number().optional().meta(readingMeta("device")),
58358
+ /** Maximum allowed value, in the device quantity's base unit. */
58359
+ max: number().optional().meta(readingMeta("device")),
58360
+ /** Step increment, in the device quantity's base unit. */
58361
+ step: number().optional().meta(readingMeta("device"))
57630
58362
  });
57631
58363
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
57632
58364
  object({
@@ -57664,9 +58396,9 @@ object({
57664
58396
  ]),
57665
58397
  /** 0 = fully closed, 100 = fully open. Null when the device has
57666
58398
  * no intermediate position surface. */
57667
- position: number().min(0).max(100).nullable(),
58399
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57668
58400
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
57669
- tiltPosition: number().min(0).max(100).nullable(),
58401
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57670
58402
  /** Ms epoch when the slice was last updated. */
57671
58403
  lastChangedAt: number()
57672
58404
  });
@@ -57724,9 +58456,9 @@ var NormalizedRangeSchema$1 = object({
57724
58456
  object({
57725
58457
  mode: DayNightModeSchema,
57726
58458
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
57727
- sensitivity: number().optional(),
58459
+ sensitivity: number().optional().meta(readingMeta("none")),
57728
58460
  /** Delay before the IR-cut filter flips, in seconds. */
57729
- switchDelaySec: number().optional(),
58461
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
57730
58462
  lastFetchedAt: number()
57731
58463
  });
57732
58464
  /**
@@ -57881,11 +58613,11 @@ object({
57881
58613
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
57882
58614
  availablePrograms: array(string()),
57883
58615
  /** Seconds remaining on the active cycle, or null when not running / not known. */
57884
- remainingSeconds: number().int().nonnegative().nullable(),
58616
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57885
58617
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
57886
- delayedStartSeconds: number().int().nonnegative().nullable(),
58618
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57887
58619
  /** Cycle completion 0..100, or null when not applicable. */
57888
- progressPercent: number().min(0).max(100).nullable(),
58620
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57889
58621
  /** True when the door is reported open; null when the door state is not known. */
57890
58622
  doorOpen: boolean().nullable(),
57891
58623
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -57909,7 +58641,7 @@ object({
57909
58641
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57910
58642
  machineCareDue: boolean().nullable(),
57911
58643
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57912
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
58644
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
57913
58645
  /** Option states: null when the appliance does not expose the option or has not said. */
57914
58646
  halfLoad: boolean().nullable(),
57915
58647
  hygienePlus: boolean().nullable(),
@@ -57924,7 +58656,7 @@ object({
57924
58656
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
57925
58657
  activeEvents: array(string()),
57926
58658
  /** Numeric error code when the appliance reports one; null otherwise. */
57927
- errorCode: number().nullable(),
58659
+ errorCode: number().nullable().meta(readingMeta("none")),
57928
58660
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
57929
58661
  errorLabel: string().nullable(),
57930
58662
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58070,24 +58802,14 @@ object({
58070
58802
  /** Ms epoch of the last press. null = never observed since this provider started. */
58071
58803
  lastPressedAt: number().nullable(),
58072
58804
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58073
- pressCountSinceStart: number()
58805
+ pressCountSinceStart: number().meta(readingMeta("count"))
58074
58806
  });
58075
58807
  object({
58076
58808
  deviceId: number(),
58077
58809
  timestamp: number()
58078
58810
  });
58079
58811
  DeviceType.Button, DeviceType.Camera;
58080
- /**
58081
- * Enum-state sensor — a string value picked from a finite option set.
58082
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
58083
- * states, weather conditions, contact-source channels, mode strings).
58084
- * The option set is stable across observations; it is persisted in the
58085
- * device config blob (via the control slice at adoption) and is NOT
58086
- * stored in `sourceInfo`.
58087
- */
58088
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
58089
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
58090
- var EnumSensorDateTimeFormatSchema = _enum([
58812
+ _enum([
58091
58813
  "date",
58092
58814
  "time",
58093
58815
  "datetime"
@@ -58095,12 +58817,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
58095
58817
  object({
58096
58818
  value: string(),
58097
58819
  /**
58098
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
58099
- * as a locale date/time/datetime; absent for plain enum sensors. HA
58100
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
58101
- * a time-only sensor → `'time'`.
58820
+ * Set when `value` is a timestamp rather than an enum state, so history
58821
+ * does not learn ISO strings as states. The display shape (date / time /
58822
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
58102
58823
  */
58103
- format: EnumSensorDateTimeFormatSchema.optional(),
58824
+ timestamp: boolean().optional(),
58104
58825
  /** Ms epoch when the slice was last updated. */
58105
58826
  lastFetchedAt: number()
58106
58827
  });
@@ -58119,13 +58840,13 @@ var EventFireSchema = object({
58119
58840
  deviceId: number(),
58120
58841
  eventType: string(),
58121
58842
  data: record(string(), unknown()).nullable(),
58122
- timestamp: number(),
58123
- seq: number()
58843
+ timestamp: number().meta(readingMeta("none")),
58844
+ seq: number().meta(readingMeta("count"))
58124
58845
  });
58125
58846
  object({
58126
58847
  eventTypes: array(string()),
58127
58848
  lastEvent: EventFireSchema.nullable(),
58128
- eventCountSinceStart: number()
58849
+ eventCountSinceStart: number().meta(readingMeta("count"))
58129
58850
  });
58130
58851
  DeviceType.EventEmitter;
58131
58852
  var IdentitySchema = object({
@@ -58514,10 +59235,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
58514
59235
  object({
58515
59236
  /** Active speed as 0..100 inclusive. Null when the device has no
58516
59237
  * speed surface (single-speed fan). */
58517
- percentage: number().min(0).max(100).nullable(),
59238
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58518
59239
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
58519
59240
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
58520
- percentageStep: number().positive().optional(),
59241
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
58521
59242
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
58522
59243
  * — empty string when no preset is active or supported. */
58523
59244
  preset: string(),
@@ -58599,7 +59320,7 @@ var FeatureProbeStatusSchema = object({
58599
59320
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
58600
59321
  model: string().nullable(),
58601
59322
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
58602
- channelCount: number().nullable(),
59323
+ channelCount: number().nullable().meta(readingMeta("count")),
58603
59324
  /**
58604
59325
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
58605
59326
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -58631,9 +59352,45 @@ object({
58631
59352
  lastChangedAt: number()
58632
59353
  });
58633
59354
  DeviceType.Sensor;
59355
+ object({
59356
+ /** One row per member that has been commanded. A later command replaces that member's row. */
59357
+ results: array(object({
59358
+ deviceId: number().int().nonnegative(),
59359
+ cap: string().min(1),
59360
+ method: string().min(1),
59361
+ ok: boolean(),
59362
+ error: string().optional(),
59363
+ /** Ms epoch of the attempt. */
59364
+ at: number()
59365
+ })) });
59366
+ DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
59367
+ object({
59368
+ active: boolean().meta({ label: "Active" }),
59369
+ /** Ms epoch when the slice was last updated. */
59370
+ lastChangedAt: number()
59371
+ });
59372
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Climate, DeviceType.Thermostat;
59373
+ object({
59374
+ /** Members whose current value entered the aggregate. */
59375
+ included: number().int().nonnegative().meta(readingMeta("count", {
59376
+ precision: 0,
59377
+ label: "Reporting"
59378
+ })),
59379
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
59380
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
59381
+ precision: 0,
59382
+ label: "Excluded"
59383
+ })),
59384
+ /** `included` + `excluded`. */
59385
+ total: number().int().nonnegative().meta(readingMeta("count", {
59386
+ precision: 0,
59387
+ label: "Members"
59388
+ }))
59389
+ });
59390
+ DeviceType.Sensor, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
58634
59391
  object({
58635
59392
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
58636
- position: number().min(0).max(100).nullable(),
59393
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58637
59394
  /** Ms epoch when the slice was last updated (push or poll). */
58638
59395
  lastFetchedAt: number()
58639
59396
  });
@@ -58642,9 +59399,9 @@ object({
58642
59399
  /** Whether the humidifier is currently on. */
58643
59400
  on: boolean(),
58644
59401
  /** Current measured relative humidity (0..100). Null when not reported. */
58645
- currentHumidity: number().min(0).max(100).nullable(),
59402
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58646
59403
  /** Target relative humidity (0..100). Null when no setpoint surface. */
58647
- targetHumidity: number().min(0).max(100).nullable(),
59404
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58648
59405
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
58649
59406
  * device has no mode surface. */
58650
59407
  mode: string().nullable(),
@@ -58654,9 +59411,9 @@ object({
58654
59411
  * `idle` / `off`). Null when the device doesn't report it. */
58655
59412
  action: string().nullable(),
58656
59413
  /** HA `min_humidity` attribute. Null → UI uses 0. */
58657
- minHumidity: number().nullable(),
59414
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
58658
59415
  /** HA `max_humidity` attribute. Null → UI uses 100. */
58659
- maxHumidity: number().nullable(),
59416
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
58660
59417
  /** Ms epoch when the slice was last updated. */
58661
59418
  lastChangedAt: number()
58662
59419
  });
@@ -58680,22 +59437,13 @@ DeviceType.Humidifier, method(object({
58680
59437
  auth: "protected"
58681
59438
  });
58682
59439
  object({
58683
- /** Current relative humidity, 0..100. */
58684
- percent: number().min(0).max(100).meta({
58685
- unit: "%",
59440
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
59441
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
59442
+ precision: 0,
58686
59443
  label: "Humidity"
58687
- }),
59444
+ })),
58688
59445
  /** Ms epoch when the slice was last updated. */
58689
- lastFetchedAt: number(),
58690
- /** Live display unit from the upstream source (e.g. HA
58691
- * `attributes.unit_of_measurement`). The UI prefers this over the
58692
- * role's canonical unit. Absent → fall back to the canonical unit. */
58693
- unit: string().optional(),
58694
- /** Suggested decimal places for numeric display.
58695
- * Populated live from the upstream source when provided (e.g. HA
58696
- * `attributes.suggested_display_precision`). Falls back to
58697
- * auto-formatting when absent. */
58698
- precision: number().int().min(0).max(10).optional()
59446
+ lastFetchedAt: number()
58699
59447
  });
58700
59448
  DeviceType.Sensor, DeviceType.Thermostat;
58701
59449
  object({
@@ -58703,7 +59451,7 @@ object({
58703
59451
  * entity exposes no `entity_picture` (yet). */
58704
59452
  url: string().nullable(),
58705
59453
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
58706
- lastUpdated: number().nullable()
59454
+ lastUpdated: number().nullable().meta(readingMeta("none"))
58707
59455
  });
58708
59456
  DeviceType.Image;
58709
59457
  /**
@@ -58759,19 +59507,19 @@ var NormalizedRangeSchema = object({
58759
59507
  });
58760
59508
  object({
58761
59509
  /** Normalized 0–100. */
58762
- brightness: number().optional(),
59510
+ brightness: number().optional().meta(readingMeta("none")),
58763
59511
  /** Normalized 0–100. */
58764
- contrast: number().optional(),
59512
+ contrast: number().optional().meta(readingMeta("none")),
58765
59513
  /** Normalized 0–100. */
58766
- saturation: number().optional(),
59514
+ saturation: number().optional().meta(readingMeta("none")),
58767
59515
  /** Normalized 0–100. */
58768
- sharpness: number().optional(),
59516
+ sharpness: number().optional().meta(readingMeta("none")),
58769
59517
  mirror: boolean().optional(),
58770
59518
  flip: boolean().optional(),
58771
59519
  rotate: ImageRotateSchema.optional(),
58772
59520
  whiteBalance: WhiteBalanceModeSchema.optional(),
58773
59521
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
58774
- warmth: number().optional(),
59522
+ warmth: number().optional().meta(readingMeta("none")),
58775
59523
  exposureMode: ExposureModeSchema.optional(),
58776
59524
  backlightMode: BacklightModeSchema.optional(),
58777
59525
  lastFetchedAt: number()
@@ -59016,10 +59764,10 @@ var IntercomAbilitySchema = object({
59016
59764
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
59017
59765
  codecs: array(string()),
59018
59766
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
59019
- sampleRate: number(),
59767
+ sampleRate: number().meta(readingMeta("none")),
59020
59768
  duplex: _enum(["half", "full"]),
59021
59769
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
59022
- maxBacklogMs: number()
59770
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
59023
59771
  });
59024
59772
  var IntercomStatusSchema = object({
59025
59773
  /** True while a talk session is open. */
@@ -59130,13 +59878,13 @@ object({
59130
59878
  activity: LawnMowerActivitySchema,
59131
59879
  /** 0..100 battery percentage. Null when the device has no battery
59132
59880
  * reading. */
59133
- batteryLevel: number().min(0).max(100).nullable(),
59881
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59134
59882
  /** 0..100 mowing-completion percentage of the current task, or null when no
59135
59883
  * task is active / progress is unavailable. */
59136
- progressPercent: number().min(0).max(100).nullable(),
59884
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59137
59885
  /** Current device/event code (dynamic — mostly status, sometimes an error),
59138
59886
  * or null when unknown. */
59139
- currentCode: number().nullable(),
59887
+ currentCode: number().nullable().meta(readingMeta("none")),
59140
59888
  /** Human label for {@link currentCode}, or null when undecodable. */
59141
59889
  currentCodeLabel: string().nullable(),
59142
59890
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -59546,7 +60294,14 @@ DeviceType.Lock, method(object({
59546
60294
  auth: "protected"
59547
60295
  }), method(object({
59548
60296
  deviceId: number().int().nonnegative(),
59549
- code: string().min(1).optional()
60297
+ code: string().min(1).optional(),
60298
+ /**
60299
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
60300
+ * with one call is refused unless the caller says it was confirmed. A
60301
+ * single lock ignores it. The refusal lives in the callee, so no
60302
+ * automation or API caller reaches the doors by skipping a dialog.
60303
+ */
60304
+ confirm: boolean().optional()
59550
60305
  }), _void(), {
59551
60306
  kind: "mutation",
59552
60307
  auth: "protected"
@@ -59554,6 +60309,11 @@ DeviceType.Lock, method(object({
59554
60309
  kind: "mutation",
59555
60310
  auth: "protected"
59556
60311
  });
60312
+ object({
60313
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
60314
+ lastFetchedAt: number()
60315
+ });
60316
+ DeviceType.Sensor;
59557
60317
  /**
59558
60318
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
59559
60319
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
@@ -59598,7 +60358,7 @@ object({
59598
60358
  state: MediaPlayerStateSchema,
59599
60359
  /** Volume as 0..100 inclusive. Null when the device has no volume
59600
60360
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
59601
- volumeLevel: number().min(0).max(100).nullable(),
60361
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59602
60362
  /** Mute toggle distinct from volume=0. Null when no mute surface.
59603
60363
  * Pair with `DeviceFeature.MediaPlayerMute`. */
59604
60364
  isMuted: boolean().nullable(),
@@ -59611,10 +60371,10 @@ object({
59611
60371
  currentMedia: MediaInfoSchema.nullable(),
59612
60372
  /** Current playback position in ms. Null when not seekable or
59613
60373
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
59614
- positionMs: number().int().nonnegative().nullable(),
60374
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59615
60375
  /** Total duration of the current media in ms. Null when unknown
59616
60376
  * (live stream) or nothing is playing. */
59617
- durationMs: number().int().nonnegative().nullable(),
60377
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59618
60378
  /** Shuffle toggle. Null when no shuffle surface.
59619
60379
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
59620
60380
  shuffle: boolean().nullable(),
@@ -59914,7 +60674,7 @@ object({
59914
60674
  * it, every provider seeded it at boot, and every session left it
59915
60675
  * `detected: true` after its detach.
59916
60676
  */
59917
- autoClearAfterMs: number().nullable()
60677
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
59918
60678
  });
59919
60679
  /**
59920
60680
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -59964,7 +60724,7 @@ var MotionZoneRegionSchema = object({
59964
60724
  });
59965
60725
  object({
59966
60726
  enabled: boolean(),
59967
- sensitivity: number(),
60727
+ sensitivity: number().meta(readingMeta("none")),
59968
60728
  /** Grid region(s). Today exactly one `grid` shape. */
59969
60729
  regions: array(MotionZoneRegionSchema),
59970
60730
  lastFetchedAt: number()
@@ -60268,7 +61028,7 @@ var NavigationStatusSchema = object({
60268
61028
  following: boolean(),
60269
61029
  flash: boolean(),
60270
61030
  lightMode: NavigationLightModeSchema,
60271
- lightLevel: number().min(40).max(100),
61031
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
60272
61032
  /** Ms epoch when the slice was last updated. */
60273
61033
  lastChangedAt: number()
60274
61034
  });
@@ -60314,13 +61074,13 @@ var NetworkLinkStatusSchema = object({
60314
61074
  * one whose reading has not landed must not be drawn at 0 %. Consumers
60315
61075
  * SKIP a null rather than coerce it.
60316
61076
  */
60317
- signalPercent: number().min(0).max(100).nullable(),
61077
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60318
61078
  /** Raw received signal strength in dBm, when the firmware reports one. */
60319
- rssiDbm: number().optional(),
61079
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
60320
61080
  /** Network name of a wireless link, when the firmware reports it. */
60321
61081
  ssid: string().optional(),
60322
61082
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
60323
- lastUpdated: number()
61083
+ lastUpdated: number().meta(readingMeta("none"))
60324
61084
  });
60325
61085
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
60326
61086
  deviceId: number(),
@@ -60639,7 +61399,7 @@ object({
60639
61399
  lastError: string().nullable(),
60640
61400
  /** Number of deliveries currently buffered server-side (rate-limited
60641
61401
  * notifiers may queue). 0 when the provider sends synchronously. */
60642
- queueDepth: number().int().nonnegative(),
61402
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
60643
61403
  /** Per-feature capability matrix — authoritative source for the
60644
61404
  * compose-form field gating. */
60645
61405
  supports: NotifierSupportsSchema
@@ -60689,15 +61449,8 @@ DeviceType.Notifier, method(NotifierSendInputSchema, NotifierSendResultSchema, {
60689
61449
  acceptedAt: number()
60690
61450
  });
60691
61451
  object({
60692
- value: number(),
60693
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
60694
- * Populated live from the upstream source on each state push.
60695
- * Absent for unitless sensors. */
60696
- unit: string().optional(),
60697
- /** Suggested decimal places for numeric display.
60698
- * Populated live from the upstream source when provided.
60699
- * Falls back to auto-formatting when absent. */
60700
- precision: number().int().min(0).max(10).optional(),
61452
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
61453
+ value: number().nullable().meta(readingMeta("device")),
60701
61454
  /** Ms epoch when the slice was last updated. */
60702
61455
  lastFetchedAt: number()
60703
61456
  });
@@ -60987,23 +61740,26 @@ object({
60987
61740
  /** Food currently in the bowl (grams). Null when the device has not
60988
61741
  * reported a reading yet. On dual-hopper models this is the combined
60989
61742
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
60990
- foodLevel: number().nullable(),
61743
+ foodLevel: number().nullable().meta(readingMeta("none")),
60991
61744
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
60992
61745
  * single-hopper models. */
60993
- food1: number().nullable(),
61746
+ food1: number().nullable().meta(readingMeta("none")),
60994
61747
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
60995
61748
  * single-hopper models. */
60996
- food2: number().nullable(),
61749
+ food2: number().nullable().meta(readingMeta("none")),
60997
61750
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
60998
61751
  * (`device_class: problem`, on = low). True when the bowl is empty /
60999
61752
  * below the feeder's low threshold. */
61000
61753
  lowFood: boolean(),
61001
61754
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
61002
61755
  * device has no battery reading. */
61003
- batteryPower: number().min(0).max(100).nullable(),
61004
- /** Days of desiccant life remaining. Null when the model has no
61005
- * desiccant sensor. */
61006
- desiccantLeftDays: number().nullable(),
61756
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61757
+ /**
61758
+ * Desiccant life remaining, stored in seconds (duration base, D944).
61759
+ * Providers convert from the days the feeder reports. Null when the
61760
+ * model has no desiccant sensor.
61761
+ */
61762
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
61007
61763
  /** True while a feed is in progress. */
61008
61764
  feeding: boolean(),
61009
61765
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -61014,7 +61770,7 @@ object({
61014
61770
  * with `errorCode` for consumers that want the raw integer. */
61015
61771
  error: string().nullable(),
61016
61772
  /** Raw device error code (0 / null = no error). */
61017
- errorCode: number().nullable(),
61773
+ errorCode: number().nullable().meta(readingMeta("none")),
61018
61774
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
61019
61775
  isDualHopper: boolean(),
61020
61776
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -61024,7 +61780,7 @@ object({
61024
61780
  /** Play a chime when dispensing. */
61025
61781
  feedSound: boolean(),
61026
61782
  /** Speaker / prompt volume level (device-scaled integer). */
61027
- volume: number(),
61783
+ volume: number().meta(readingMeta("none")),
61028
61784
  /** Ms epoch when the slice was last refreshed from the cloud. */
61029
61785
  lastFetchedAt: number()
61030
61786
  });
@@ -61461,38 +62217,37 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
61461
62217
  });
61462
62218
  object({
61463
62219
  /** Instantaneous power draw in watts. */
61464
- watts: number().optional().meta({
61465
- unit: "W",
61466
- label: "Power"
61467
- }),
62220
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
61468
62221
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
61469
- kwhTotal: number().optional().meta({
61470
- unit: "kWh",
61471
- label: "Energy"
61472
- }),
62222
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
61473
62223
  /** Voltage in volts. */
61474
- volts: number().optional(),
62224
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
61475
62225
  /** Current in amperes. */
61476
- amps: number().optional(),
61477
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
61478
- va: number().optional(),
62226
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
62227
+ /** Apparent power in volt-amperes. */
62228
+ va: number().optional().meta(readingMeta("apparent-power")),
61479
62229
  /** Power factor (0..1) if reported. */
61480
- powerFactor: number().min(0).max(1).optional(),
62230
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
61481
62231
  /** Ms epoch when the slice was last updated. */
61482
- lastFetchedAt: number(),
61483
- /** Live display unit of the single metric this slice carries (e.g. HA
61484
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
61485
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
61486
- * unit per slice is unambiguous. The UI prefers this over the role's
61487
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
61488
- unit: string().optional(),
61489
- /** Suggested decimal places for numeric display.
61490
- * Populated live from the upstream source when provided (e.g. HA
61491
- * `attributes.suggested_display_precision`). Falls back to
61492
- * auto-formatting when absent. */
61493
- precision: number().int().min(0).max(10).optional()
62232
+ lastFetchedAt: number()
61494
62233
  });
61495
- DeviceType.Sensor;
62234
+ [
62235
+ DeviceType.Sensor,
62236
+ DeviceType.Switch,
62237
+ DeviceType.Light,
62238
+ DeviceType.Fan,
62239
+ DeviceType.Climate,
62240
+ DeviceType.Thermostat,
62241
+ DeviceType.Humidifier,
62242
+ DeviceType.WaterHeater,
62243
+ DeviceType.MediaPlayer,
62244
+ DeviceType.Washer,
62245
+ DeviceType.Dryer,
62246
+ DeviceType.Dishwasher,
62247
+ DeviceType.Vacuum,
62248
+ DeviceType.LawnMower,
62249
+ DeviceType.PetFeeder
62250
+ ].filter((type) => type !== DeviceType.Sensor);
61496
62251
  /**
61497
62252
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
61498
62253
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -61511,11 +62266,11 @@ DeviceType.Sensor;
61511
62266
  */
61512
62267
  var GpsLocationSchema = object({
61513
62268
  /** Latitude in decimal degrees, -90..90. */
61514
- latitude: number().min(-90).max(90),
62269
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
61515
62270
  /** Longitude in decimal degrees, -180..180. */
61516
- longitude: number().min(-180).max(180),
62271
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
61517
62272
  /** Reported accuracy in meters (lower = better). */
61518
- accuracyMeters: number().nonnegative()
62273
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
61519
62274
  });
61520
62275
  object({
61521
62276
  /** `home` / `not_home` / any user-defined zone name. */
@@ -61527,25 +62282,16 @@ object({
61527
62282
  gps: GpsLocationSchema.nullable(),
61528
62283
  /** Optional battery level of the tracking device (0..100). Null
61529
62284
  * when not reported. */
61530
- batteryPercent: number().min(0).max(100).nullable(),
62285
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61531
62286
  /** Ms epoch when the slice was last updated. */
61532
62287
  lastChangedAt: number()
61533
62288
  });
61534
62289
  DeviceType.Presence;
61535
62290
  object({
61536
- /** Current pressure in hPa. */
61537
- hpa: number(),
62291
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
62292
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
61538
62293
  /** Ms epoch when the slice was last updated. */
61539
- lastFetchedAt: number(),
61540
- /** Live display unit from the upstream source (e.g. HA
61541
- * `attributes.unit_of_measurement`). The UI prefers this over the
61542
- * role's canonical unit. Absent → fall back to the canonical unit. */
61543
- unit: string().optional(),
61544
- /** Suggested decimal places for numeric display.
61545
- * Populated live from the upstream source when provided (e.g. HA
61546
- * `attributes.suggested_display_precision`). Falls back to
61547
- * auto-formatting when absent. */
61548
- precision: number().int().min(0).max(10).optional()
62294
+ lastFetchedAt: number()
61549
62295
  });
61550
62296
  DeviceType.Sensor;
61551
62297
  /**
@@ -61878,8 +62624,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
61878
62624
  }), _void(), { kind: "mutation" });
61879
62625
  var PtzAutotrackSettingsSchema = object({
61880
62626
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
61881
- stopDelaySeconds: number().int().min(0).max(300),
61882
- disappearDelaySeconds: number().int().min(0).max(300)
62627
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
62628
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
61883
62629
  });
61884
62630
  /**
61885
62631
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -61936,7 +62682,7 @@ object({
61936
62682
  /** null = unknown — never coerced to false (D393). */
61937
62683
  running: boolean().nullable(),
61938
62684
  /** Epoch ms the current boolean began; null while `running` is null. */
61939
- since: number().nullable(),
62685
+ since: number().nullable().meta(readingMeta("none")),
61940
62686
  source: RunningStateSourceSchema.meta({ label: "Source" }),
61941
62687
  phase: string().nullable(),
61942
62688
  /** null until a reading has been published (D393). */
@@ -61948,7 +62694,7 @@ object({
61948
62694
  phase: string().nullable()
61949
62695
  });
61950
62696
  Object.values(DeviceType);
61951
- DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
62697
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
61952
62698
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
61953
62699
  kind: "mutation",
61954
62700
  auth: "admin"
@@ -63134,9 +63880,9 @@ var RecordTriggerSchema = _enum([
63134
63880
  */
63135
63881
  var RecordWindowSchema = object({
63136
63882
  trigger: RecordTriggerSchema,
63137
- day: number().int().min(0).max(6),
63138
- startMinute: number().int().min(0).max(1439),
63139
- endMinute: number().int().min(1).max(1440)
63883
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
63884
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
63885
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
63140
63886
  });
63141
63887
  /** Status of one physical volume, as the camera itself describes it. */
63142
63888
  var OnboardStorageVolumeSchema = object({
@@ -63205,10 +63951,10 @@ object({
63205
63951
  /** Master "record to the card at all" switch. */
63206
63952
  enabled: boolean().nullable(),
63207
63953
  overwriteWhenFull: boolean().nullable(),
63208
- preRecordSec: number().nullable(),
63209
- postRecordSec: number().nullable(),
63954
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
63955
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
63210
63956
  /** Length of one recorded file, in minutes. */
63211
- segmentMinutes: number().nullable(),
63957
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
63212
63958
  /** The primary track's weekly windows, flattened. */
63213
63959
  windows: array(RecordWindowSchema),
63214
63960
  /**
@@ -63224,7 +63970,7 @@ object({
63224
63970
  * provider reporting a non-zero count also reports the schedule as not
63225
63971
  * writable.
63226
63972
  */
63227
- unreadableWindows: number(),
63973
+ unreadableWindows: number().meta(readingMeta("count")),
63228
63974
  /**
63229
63975
  * The camera is scheduled to record and has NO usable storage.
63230
63976
  *
@@ -63861,7 +64607,7 @@ var CamStreamDescriptorSchema = object({
63861
64607
  url: string().optional(),
63862
64608
  codec: string().optional(),
63863
64609
  resolution: CamStreamResolutionSchema.optional(),
63864
- fps: number().positive().optional(),
64610
+ fps: number().positive().optional().meta(readingMeta("rate")),
63865
64611
  label: string().optional(),
63866
64612
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
63867
64613
  deviceFeatures: array(string()).optional(),
@@ -63891,18 +64637,18 @@ var StreamProfileSchema = _enum([
63891
64637
  ]);
63892
64638
  /** The current encoding config of one stream profile. */
63893
64639
  var StreamProfileConfigSchema = object({
63894
- width: number(),
63895
- height: number(),
64640
+ width: number().meta(readingMeta("none")),
64641
+ height: number().meta(readingMeta("none")),
63896
64642
  codec: _enum(["h264", "h265"]),
63897
- framerate: number(),
63898
- bitrate: number(),
64643
+ framerate: number().meta(readingMeta("rate")),
64644
+ bitrate: number().meta(readingMeta("bitrate")),
63899
64645
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
63900
64646
  encoderProfile: _enum([
63901
64647
  "high",
63902
64648
  "main",
63903
64649
  "baseline"
63904
64650
  ]).optional(),
63905
- gop: number().optional(),
64651
+ gop: number().optional().meta(readingMeta("none")),
63906
64652
  /**
63907
64653
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
63908
64654
  *
@@ -63987,7 +64733,7 @@ object({
63987
64733
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
63988
64734
  lastChangedAt: number()
63989
64735
  });
63990
- DeviceType.Switch, DeviceType.Siren, DeviceType.Light, method(object({
64736
+ DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, method(object({
63991
64737
  deviceId: number(),
63992
64738
  on: boolean()
63993
64739
  }), _void(), {
@@ -64545,23 +65291,13 @@ object({
64545
65291
  });
64546
65292
  DeviceType.Sensor;
64547
65293
  object({
64548
- /** Current temperature in Celsius. */
64549
- celsius: number().meta({
64550
- unit: "°C",
65294
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
65295
+ celsius: number().nullable().meta(readingMeta("temperature", {
65296
+ precision: 1,
64551
65297
  label: "Temperature"
64552
- }),
65298
+ })),
64553
65299
  /** Ms epoch when the slice was last updated (push or poll). */
64554
- lastFetchedAt: number(),
64555
- /** Live display unit from the upstream source (e.g. HA
64556
- * `attributes.unit_of_measurement`). The UI prefers this over the
64557
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
64558
- * Absent → the UI falls back to the role's canonical unit. */
64559
- unit: string().optional(),
64560
- /** Suggested decimal places for numeric display.
64561
- * Populated live from the upstream source when provided (e.g. HA
64562
- * `attributes.suggested_display_precision`). Falls back to
64563
- * auto-formatting when absent. */
64564
- precision: number().int().min(0).max(10).optional()
65300
+ lastFetchedAt: number()
64565
65301
  });
64566
65302
  DeviceType.Sensor, DeviceType.Thermostat;
64567
65303
  /**
@@ -64813,16 +65549,16 @@ object({
64813
65549
  ]),
64814
65550
  /** 0..100 battery percentage. Null when the device has no battery
64815
65551
  * reading. */
64816
- batteryLevel: number().min(0).max(100).nullable(),
65552
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64817
65553
  /** Current fan-speed token (provider-verbatim). Null when unknown or
64818
65554
  * the vacuum has no speed control. */
64819
65555
  fanSpeed: string().nullable(),
64820
65556
  /** Speed tokens the hardware accepts — drives the UI selector. */
64821
65557
  availableFanSpeeds: array(string()),
64822
65558
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64823
- progressPercent: number().min(0).max(100).nullable(),
65559
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64824
65560
  /** Current error code (0 / null = no error). */
64825
- errorCode: number().nullable(),
65561
+ errorCode: number().nullable().meta(readingMeta("none")),
64826
65562
  /** Human label for {@link errorCode}, or null when none / undecodable. */
64827
65563
  errorLabel: string().nullable(),
64828
65564
  /** Ms epoch when the slice was last updated. */
@@ -64861,7 +65597,7 @@ object({
64861
65597
  ]),
64862
65598
  /** 0 = fully closed, 100 = fully open. Null when the device has no
64863
65599
  * intermediate position surface. */
64864
- position: number().min(0).max(100).nullable(),
65600
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64865
65601
  /** Ms epoch when the slice was last updated. */
64866
65602
  lastChangedAt: number()
64867
65603
  });
@@ -64889,9 +65625,9 @@ object({
64889
65625
  DeviceType.Sensor;
64890
65626
  object({
64891
65627
  /** Current measured temperature. Null when not reported. */
64892
- currentTemp: number().nullable(),
65628
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
64893
65629
  /** Target temperature setpoint. Null when no setpoint surface. */
64894
- targetTemp: number().nullable(),
65630
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
64895
65631
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
64896
65632
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
64897
65633
  * device reports an unknown state. */
@@ -64902,9 +65638,9 @@ object({
64902
65638
  * has no away surface. */
64903
65639
  away: boolean().nullable(),
64904
65640
  /** HA `min_temp` attribute. Null when not reported. */
64905
- minTemp: number().nullable(),
65641
+ minTemp: number().nullable().meta(readingMeta("temperature")),
64906
65642
  /** HA `max_temp` attribute. Null when not reported. */
64907
- maxTemp: number().nullable(),
65643
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
64908
65644
  /** Ms epoch when the slice was last updated. */
64909
65645
  lastChangedAt: number()
64910
65646
  });
@@ -64928,25 +65664,18 @@ DeviceType.WaterHeater, method(object({
64928
65664
  auth: "protected"
64929
65665
  });
64930
65666
  object({
64931
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
64932
- * when no condition has been reported yet. */
65667
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
64933
65668
  condition: string().nullable(),
64934
- /** Current temperature in the reported unit. Null when not provided. */
64935
- temperature: number().nullable(),
64936
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
64937
- temperatureUnit: string().nullable(),
65669
+ /** Current temperature in Celsius. Null when not provided or refused. */
65670
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
64938
65671
  /** Relative humidity (0..100). Null when not provided. */
64939
- humidity: number().min(0).max(100).nullable(),
64940
- /** Barometric pressure in the reported unit. Null when not provided. */
64941
- pressure: number().nullable(),
64942
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
64943
- pressureUnit: string().nullable(),
64944
- /** Wind speed in the reported unit. Null when not provided. */
64945
- windSpeed: number().nullable(),
64946
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
64947
- windSpeedUnit: string().nullable(),
65672
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
65673
+ /** Barometric pressure in hPa. Null when not provided or refused. */
65674
+ pressure: number().nullable().meta(readingMeta("pressure")),
65675
+ /** Wind speed in metres per second. Null when not provided or refused. */
65676
+ windSpeed: number().nullable().meta(readingMeta("speed")),
64948
65677
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
64949
- windBearing: number().nullable(),
65678
+ windBearing: number().nullable().meta(readingMeta("angle")),
64950
65679
  /** Ms epoch when the slice was last updated. */
64951
65680
  lastFetchedAt: number()
64952
65681
  });
@@ -65696,8 +66425,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
65696
66425
  kind: "mutation",
65697
66426
  auth: "admin"
65698
66427
  });
66428
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
65699
66429
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
65700
- 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;
66430
+ 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;
65701
66431
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
65702
66432
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
65703
66433
  new Set(CAP_NAMES);
@@ -65750,7 +66480,7 @@ var AccessoryKind = {
65750
66480
  PrivacyMask: DeviceRole.PrivacyMask
65751
66481
  };
65752
66482
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
65753
- 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;
66483
+ 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;
65754
66484
  new Set(Object.values(DeviceType));
65755
66485
  Object.freeze({
65756
66486
  "accessories.getStatus": {
@@ -65858,6 +66588,13 @@ Object.freeze({
65858
66588
  access: "view",
65859
66589
  role: "admin"
65860
66590
  },
66591
+ "addons.getPackageDocs": {
66592
+ capName: "addons",
66593
+ capScope: "system",
66594
+ addonId: null,
66595
+ access: "view",
66596
+ role: "admin"
66597
+ },
65861
66598
  "addons.getVersions": {
65862
66599
  capName: "addons",
65863
66600
  capScope: "system",
@@ -68966,6 +69703,27 @@ Object.freeze({
68966
69703
  access: "view",
68967
69704
  role: "viewer"
68968
69705
  },
69706
+ "groupActive.getStatus": {
69707
+ capName: "group-active",
69708
+ capScope: "device",
69709
+ addonId: null,
69710
+ access: "view",
69711
+ role: "viewer"
69712
+ },
69713
+ "groupCommands.getStatus": {
69714
+ capName: "group-commands",
69715
+ capScope: "device",
69716
+ addonId: null,
69717
+ access: "view",
69718
+ role: "viewer"
69719
+ },
69720
+ "groupStatus.getStatus": {
69721
+ capName: "group-status",
69722
+ capScope: "device",
69723
+ addonId: null,
69724
+ access: "view",
69725
+ role: "viewer"
69726
+ },
68969
69727
  "heatingValve.getStatus": {
68970
69728
  capName: "heating-valve",
68971
69729
  capScope: "device",
@@ -69918,6 +70676,13 @@ Object.freeze({
69918
70676
  access: "create",
69919
70677
  role: "admin"
69920
70678
  },
70679
+ "meter.getStatus": {
70680
+ capName: "meter",
70681
+ capScope: "device",
70682
+ addonId: null,
70683
+ access: "view",
70684
+ role: "viewer"
70685
+ },
69921
70686
  "metricsProvider.collectSnapshot": {
69922
70687
  capName: "metrics-provider",
69923
70688
  capScope: "system",
@@ -78016,7 +78781,39 @@ var APPLIANCE_CAP_VOCABULARY = {
78016
78781
  phrase: t("When {device} finishes", "Quando {device} finisce"),
78017
78782
  icon: "square"
78018
78783
  }
78019
- }
78784
+ },
78785
+ overrides: { byType: {
78786
+ [DeviceType.Washer]: {
78787
+ state: { running: {
78788
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
78789
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
78790
+ } },
78791
+ events: {
78792
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
78793
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
78794
+ }
78795
+ },
78796
+ [DeviceType.Dryer]: {
78797
+ state: { running: {
78798
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
78799
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
78800
+ } },
78801
+ events: {
78802
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
78803
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
78804
+ }
78805
+ },
78806
+ [DeviceType.Dishwasher]: {
78807
+ state: { running: {
78808
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
78809
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
78810
+ } },
78811
+ events: {
78812
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
78813
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
78814
+ }
78815
+ }
78816
+ } }
78020
78817
  },
78021
78818
  update: {
78022
78819
  scope: "device",
@@ -78788,6 +79585,41 @@ var SENSOR_CAP_VOCABULARY = {
78788
79585
  icon: "flame"
78789
79586
  }]
78790
79587
  },
79588
+ "group-active": {
79589
+ scope: "device",
79590
+ noun: t("Group state", "Stato del gruppo")
79591
+ },
79592
+ "group-commands": {
79593
+ scope: "device",
79594
+ noun: t("Group command", "Comando di gruppo")
79595
+ },
79596
+ "group-status": {
79597
+ scope: "device",
79598
+ noun: t("Group", "Gruppo"),
79599
+ state: [
79600
+ {
79601
+ kind: "numeric",
79602
+ field: "included",
79603
+ quantity: t("the members reporting", "i membri che rispondono"),
79604
+ label: t("Reporting", "Rispondono"),
79605
+ icon: "users"
79606
+ },
79607
+ {
79608
+ kind: "numeric",
79609
+ field: "excluded",
79610
+ quantity: t("the members left out", "i membri esclusi"),
79611
+ label: t("Excluded", "Esclusi"),
79612
+ icon: "users"
79613
+ },
79614
+ {
79615
+ kind: "numeric",
79616
+ field: "total",
79617
+ quantity: t("the member count", "il numero di membri"),
79618
+ label: t("Members", "Membri"),
79619
+ icon: "sigma"
79620
+ }
79621
+ ]
79622
+ },
78791
79623
  "humidity-sensor": {
78792
79624
  scope: "device",
78793
79625
  noun: t("Humidity", "Umidità"),
@@ -78813,6 +79645,17 @@ var SENSOR_CAP_VOCABULARY = {
78813
79645
  }],
78814
79646
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
78815
79647
  },
79648
+ meter: {
79649
+ scope: "device",
79650
+ noun: t("Meter", "Contatore"),
79651
+ state: [{
79652
+ kind: "numeric",
79653
+ field: "value",
79654
+ quantity: t("the total", "il totale"),
79655
+ label: t("Total", "Totale"),
79656
+ icon: "gauge"
79657
+ }]
79658
+ },
78816
79659
  "numeric-sensor": {
78817
79660
  scope: "device",
78818
79661
  noun: t("Sensor", "Sensore"),
@@ -78821,7 +79664,6 @@ var SENSOR_CAP_VOCABULARY = {
78821
79664
  field: "value",
78822
79665
  quantity: t("the reading", "il valore"),
78823
79666
  label: t("Reading", "Valore"),
78824
- unitField: "unit",
78825
79667
  icon: "gauge"
78826
79668
  }]
78827
79669
  },
@@ -78957,7 +79799,7 @@ var SENSOR_CAP_VOCABULARY = {
78957
79799
  field: "temperature",
78958
79800
  quantity: t("the outdoor temperature", "la temperatura esterna"),
78959
79801
  label: t("Temperature", "Temperatura"),
78960
- unitField: "temperatureUnit",
79802
+ unit: "°C",
78961
79803
  icon: "thermometer"
78962
79804
  },
78963
79805
  {
@@ -78973,7 +79815,7 @@ var SENSOR_CAP_VOCABULARY = {
78973
79815
  field: "windSpeed",
78974
79816
  quantity: t("the wind speed", "la velocità del vento"),
78975
79817
  label: t("Wind", "Vento"),
78976
- unitField: "windSpeedUnit",
79818
+ unit: "m/s",
78977
79819
  icon: "wind"
78978
79820
  },
78979
79821
  {
@@ -78981,7 +79823,7 @@ var SENSOR_CAP_VOCABULARY = {
78981
79823
  field: "pressure",
78982
79824
  quantity: t("the air pressure", "la pressione atmosferica"),
78983
79825
  label: t("Pressure", "Pressione"),
78984
- unitField: "pressureUnit",
79826
+ unit: "hPa",
78985
79827
  icon: "gauge"
78986
79828
  }
78987
79829
  ]
@@ -79286,8 +80128,47 @@ function projectAction(cap, action) {
79286
80128
  };
79287
80129
  }
79288
80130
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
79289
- 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;
80131
+ 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;
79290
80132
  DeviceFeature.BatteryOperated;
80133
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
80134
+ 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.";
80135
+ 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.";
80136
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
80137
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
80138
+ 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.";
80139
+ function withheld(reason) {
80140
+ return {
80141
+ [DeviceType.Washer]: {
80142
+ exported: false,
80143
+ reason
80144
+ },
80145
+ [DeviceType.Dryer]: {
80146
+ exported: false,
80147
+ reason
80148
+ },
80149
+ [DeviceType.Dishwasher]: {
80150
+ exported: false,
80151
+ reason
80152
+ }
80153
+ };
80154
+ }
80155
+ function offered(received) {
80156
+ return {
80157
+ [DeviceType.Washer]: {
80158
+ exported: true,
80159
+ as: received
80160
+ },
80161
+ [DeviceType.Dryer]: {
80162
+ exported: true,
80163
+ as: received
80164
+ },
80165
+ [DeviceType.Dishwasher]: {
80166
+ exported: true,
80167
+ as: received
80168
+ }
80169
+ };
80170
+ }
80171
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
79291
80172
  Object.freeze({
79292
80173
  "accessories.getStatus": [{
79293
80174
  name: "deviceId",
@@ -80254,6 +81135,21 @@ Object.freeze({
80254
81135
  form: "single",
80255
81136
  optional: false
80256
81137
  }],
81138
+ "groupActive.getStatus": [{
81139
+ name: "deviceId",
81140
+ form: "single",
81141
+ optional: false
81142
+ }],
81143
+ "groupCommands.getStatus": [{
81144
+ name: "deviceId",
81145
+ form: "single",
81146
+ optional: false
81147
+ }],
81148
+ "groupStatus.getStatus": [{
81149
+ name: "deviceId",
81150
+ form: "single",
81151
+ optional: false
81152
+ }],
80257
81153
  "heatingValve.getStatus": [{
80258
81154
  name: "deviceId",
80259
81155
  form: "single",
@@ -80454,6 +81350,11 @@ Object.freeze({
80454
81350
  form: "single",
80455
81351
  optional: false
80456
81352
  }],
81353
+ "meter.getStatus": [{
81354
+ name: "deviceId",
81355
+ form: "single",
81356
+ optional: false
81357
+ }],
80457
81358
  "motion.getStatus": [{
80458
81359
  name: "deviceId",
80459
81360
  form: "single",
@@ -82061,7 +82962,6 @@ Object.freeze({
82061
82962
  "network-access": "ingress",
82062
82963
  "smtp-provider": "email"
82063
82964
  });
82064
- DeviceType.Camera;
82065
82965
  /**
82066
82966
  * A saved Customize assignment (D936). The schema is the type: callers use
82067
82967
  * `z.infer`, they do not restate the shape.
@@ -82238,6 +83138,8 @@ new Map(Object.entries({
82238
83138
  "detection-blind": "device-events",
82239
83139
  "motion-noisy": "device-events",
82240
83140
  "motion-calm": "device-events",
83141
+ "package-llm-silent": "device-events",
83142
+ "package-llm-answered": "device-events",
82241
83143
  "recording-stalled": "device-events",
82242
83144
  "recording-healthy": "device-events",
82243
83145
  "recording-degraded": "device-events",
@@ -82321,6 +83223,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
82321
83223
  label: "Motion without a subject",
82322
83224
  kinds: ["motion-noisy", "motion-calm"]
82323
83225
  },
83226
+ {
83227
+ id: "package-llm",
83228
+ family: "device-events",
83229
+ label: "Package confirm",
83230
+ kinds: ["package-llm-silent", "package-llm-answered"]
83231
+ },
82324
83232
  {
82325
83233
  id: "recording",
82326
83234
  family: "device-events",
@@ -82502,6 +83410,8 @@ var DEVICE_EVENTS = [
82502
83410
  "detection-blind",
82503
83411
  "motion-noisy",
82504
83412
  "motion-calm",
83413
+ "package-llm-silent",
83414
+ "package-llm-answered",
82505
83415
  "recording-stalled",
82506
83416
  "recording-healthy",
82507
83417
  "recording-degraded",
@@ -82770,6 +83680,15 @@ new Map([
82770
83680
  snapshotLabel: "Cameras with constant motion now",
82771
83681
  snapshotSection: null
82772
83682
  },
83683
+ {
83684
+ id: "package-llm-confirm",
83685
+ family: "device-events",
83686
+ label: "Package confirm (not answering / answering again)",
83687
+ bad: ["package-llm-silent"],
83688
+ recovery: ["package-llm-answered"],
83689
+ snapshotLabel: "Cameras whose package confirm is not answering",
83690
+ snapshotSection: null
83691
+ },
82773
83692
  {
82774
83693
  id: "recording-health",
82775
83694
  family: "device-events",