@camstack/addon-google-services 0.2.31 → 0.2.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- //#region ../types/dist/event-category-DHXxB8t9.mjs
2
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
3
3
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
4
4
  EventCategory["SystemBoot"] = "system.boot";
5
5
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -325,6 +325,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
325
325
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
326
326
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
327
327
  /**
328
+ * A camera's package LLM confirm has failed closed three times in a row
329
+ * (D931, 2026-10-26). One edge per camera until
330
+ * {@link PipelinePackageLlmAnswered}.
331
+ */
332
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
333
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
334
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
335
+ /**
328
336
  * Per-camera pipeline config was mutated by the orchestrator
329
337
  * (a settings change via `setAgentInferenceDevices` /
330
338
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5567,7 +5575,7 @@ var ZodIssueCode = {
5567
5575
  var ZodFirstPartyTypeKind;
5568
5576
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5569
5577
  //#endregion
5570
- //#region ../types/dist/sleep-BErAxhul.mjs
5578
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5571
5579
  /**
5572
5580
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5573
5581
  * window to float samples (D455).
@@ -8309,6 +8317,99 @@ function defineEntryKind(name, spec) {
8309
8317
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8310
8318
  return defined;
8311
8319
  }
8320
+ var READING_QUANTITIES = [
8321
+ "temperature",
8322
+ "pressure",
8323
+ "speed",
8324
+ "precipitation",
8325
+ "precipitation-rate",
8326
+ "length",
8327
+ "area",
8328
+ "illuminance",
8329
+ "irradiance",
8330
+ "power",
8331
+ "energy",
8332
+ "voltage",
8333
+ "current",
8334
+ "apparent-power",
8335
+ "humidity",
8336
+ "percentage",
8337
+ "concentration-volume",
8338
+ "concentration-mass",
8339
+ "concentration-ppb",
8340
+ "volume",
8341
+ "duration",
8342
+ "duration-ms",
8343
+ "information",
8344
+ "sound-level",
8345
+ "signal-level",
8346
+ "colour-temperature",
8347
+ "angle",
8348
+ "bitrate",
8349
+ "rate",
8350
+ "mass",
8351
+ "index",
8352
+ "ratio",
8353
+ "count",
8354
+ "none",
8355
+ "device"
8356
+ ];
8357
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8358
+ new Set(READING_QUANTITIES);
8359
+ /**
8360
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8361
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8362
+ */
8363
+ var QUANTITY_BASE = {
8364
+ temperature: "°C",
8365
+ pressure: "hPa",
8366
+ speed: "m/s",
8367
+ precipitation: "mm",
8368
+ "precipitation-rate": "mm/h",
8369
+ length: "m",
8370
+ area: "m²",
8371
+ illuminance: "lx",
8372
+ irradiance: "W/m²",
8373
+ power: "W",
8374
+ energy: "kWh",
8375
+ voltage: "V",
8376
+ current: "A",
8377
+ "apparent-power": "VA",
8378
+ humidity: "%",
8379
+ percentage: "%",
8380
+ "concentration-volume": "ppm",
8381
+ "concentration-mass": "µg/m³",
8382
+ "concentration-ppb": "ppb",
8383
+ volume: "L",
8384
+ duration: "s",
8385
+ "duration-ms": "ms",
8386
+ information: "B",
8387
+ "sound-level": "dBFS",
8388
+ "signal-level": "dBm",
8389
+ "colour-temperature": "mired",
8390
+ angle: "°",
8391
+ bitrate: "kbps",
8392
+ rate: "fps",
8393
+ mass: "kg",
8394
+ index: "none",
8395
+ ratio: "none",
8396
+ count: "none",
8397
+ none: "none",
8398
+ device: "device"
8399
+ };
8400
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8401
+ function baseUnitOf(quantity) {
8402
+ const symbol = QUANTITY_BASE[quantity];
8403
+ if (symbol === "none" || symbol === "device") return void 0;
8404
+ return symbol;
8405
+ }
8406
+ function readingMeta(quantity, extras) {
8407
+ return {
8408
+ quantity,
8409
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8410
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8411
+ };
8412
+ }
8312
8413
  /** Shared Zod schemas used across streaming capabilities. */
8313
8414
  var CamProfileSchema = _enum([
8314
8415
  "high",
@@ -8336,8 +8437,8 @@ var CamStreamKindSchema = _enum([
8336
8437
  "derived"
8337
8438
  ]);
8338
8439
  var CamStreamResolutionSchema = object({
8339
- width: number().int().positive(),
8340
- height: number().int().positive()
8440
+ width: number().int().positive().meta(readingMeta("none")),
8441
+ height: number().int().positive().meta(readingMeta("none"))
8341
8442
  });
8342
8443
  var CameraStreamSchema = object({
8343
8444
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8851,6 +8952,9 @@ var CAP_NAMES = Object.freeze([
8851
8952
  "filesystem-browse",
8852
8953
  "flood",
8853
8954
  "gas",
8955
+ "group-active",
8956
+ "group-commands",
8957
+ "group-status",
8854
8958
  "heating-valve",
8855
8959
  "humidifier",
8856
8960
  "humidity-sensor",
@@ -8871,6 +8975,7 @@ var CAP_NAMES = Object.freeze([
8871
8975
  "login-method",
8872
8976
  "media-player",
8873
8977
  "mesh-network",
8978
+ "meter",
8874
8979
  "metrics-provider",
8875
8980
  "model-convert",
8876
8981
  "model-distributor",
@@ -8980,14 +9085,6 @@ Object.freeze({
8980
9085
  {
8981
9086
  "path": "aqi",
8982
9087
  "valueType": "number"
8983
- },
8984
- {
8985
- "path": "unit",
8986
- "valueType": "string"
8987
- },
8988
- {
8989
- "path": "precision",
8990
- "valueType": "number"
8991
9088
  }
8992
9089
  ],
8993
9090
  "alarm-panel": [
@@ -9023,20 +9120,10 @@ Object.freeze({
9023
9120
  "valueType": "boolean"
9024
9121
  }
9025
9122
  ],
9026
- "ambient-light-sensor": [
9027
- {
9028
- "path": "lux",
9029
- "valueType": "number"
9030
- },
9031
- {
9032
- "path": "unit",
9033
- "valueType": "string"
9034
- },
9035
- {
9036
- "path": "precision",
9037
- "valueType": "number"
9038
- }
9039
- ],
9123
+ "ambient-light-sensor": [{
9124
+ "path": "lux",
9125
+ "valueType": "number"
9126
+ }],
9040
9127
  "audio-metrics": [
9041
9128
  {
9042
9129
  "path": "ts",
@@ -9116,6 +9203,10 @@ Object.freeze({
9116
9203
  "path": "lastUpdated",
9117
9204
  "valueType": "number"
9118
9205
  },
9206
+ {
9207
+ "path": "volts",
9208
+ "valueType": "number"
9209
+ },
9119
9210
  {
9120
9211
  "path": "lastContactAt",
9121
9212
  "valueType": "number"
@@ -9358,10 +9449,6 @@ Object.freeze({
9358
9449
  "datetime"
9359
9450
  ]
9360
9451
  },
9361
- {
9362
- "path": "unit",
9363
- "valueType": "string"
9364
- },
9365
9452
  {
9366
9453
  "path": "min",
9367
9454
  "valueType": "number"
@@ -9373,19 +9460,6 @@ Object.freeze({
9373
9460
  {
9374
9461
  "path": "step",
9375
9462
  "valueType": "number"
9376
- },
9377
- {
9378
- "path": "precision",
9379
- "valueType": "number"
9380
- },
9381
- {
9382
- "path": "format",
9383
- "valueType": "enum",
9384
- "enumValues": [
9385
- "date",
9386
- "time",
9387
- "datetime"
9388
- ]
9389
9463
  }
9390
9464
  ],
9391
9465
  "cover": [
@@ -9626,13 +9700,8 @@ Object.freeze({
9626
9700
  "path": "value",
9627
9701
  "valueType": "string"
9628
9702
  }, {
9629
- "path": "format",
9630
- "valueType": "enum",
9631
- "enumValues": [
9632
- "date",
9633
- "time",
9634
- "datetime"
9635
- ]
9703
+ "path": "timestamp",
9704
+ "valueType": "boolean"
9636
9705
  }],
9637
9706
  "event-emitter": [
9638
9707
  {
@@ -9705,6 +9774,24 @@ Object.freeze({
9705
9774
  "path": "detected",
9706
9775
  "valueType": "boolean"
9707
9776
  }],
9777
+ "group-active": [{
9778
+ "path": "active",
9779
+ "valueType": "boolean"
9780
+ }],
9781
+ "group-status": [
9782
+ {
9783
+ "path": "included",
9784
+ "valueType": "number"
9785
+ },
9786
+ {
9787
+ "path": "excluded",
9788
+ "valueType": "number"
9789
+ },
9790
+ {
9791
+ "path": "total",
9792
+ "valueType": "number"
9793
+ }
9794
+ ],
9708
9795
  "heating-valve": [{
9709
9796
  "path": "position",
9710
9797
  "valueType": "number"
@@ -9739,20 +9826,10 @@ Object.freeze({
9739
9826
  "valueType": "number"
9740
9827
  }
9741
9828
  ],
9742
- "humidity-sensor": [
9743
- {
9744
- "path": "percent",
9745
- "valueType": "number"
9746
- },
9747
- {
9748
- "path": "unit",
9749
- "valueType": "string"
9750
- },
9751
- {
9752
- "path": "precision",
9753
- "valueType": "number"
9754
- }
9755
- ],
9829
+ "humidity-sensor": [{
9830
+ "path": "percent",
9831
+ "valueType": "number"
9832
+ }],
9756
9833
  "image": [{
9757
9834
  "path": "url",
9758
9835
  "valueType": "string"
@@ -9960,6 +10037,10 @@ Object.freeze({
9960
10037
  ]
9961
10038
  }
9962
10039
  ],
10040
+ "meter": [{
10041
+ "path": "value",
10042
+ "valueType": "number"
10043
+ }],
9963
10044
  "motion": [
9964
10045
  {
9965
10046
  "path": "detected",
@@ -10091,20 +10172,10 @@ Object.freeze({
10091
10172
  "valueType": "boolean"
10092
10173
  }
10093
10174
  ],
10094
- "numeric-sensor": [
10095
- {
10096
- "path": "value",
10097
- "valueType": "number"
10098
- },
10099
- {
10100
- "path": "unit",
10101
- "valueType": "string"
10102
- },
10103
- {
10104
- "path": "precision",
10105
- "valueType": "number"
10106
- }
10107
- ],
10175
+ "numeric-sensor": [{
10176
+ "path": "value",
10177
+ "valueType": "number"
10178
+ }],
10108
10179
  "pet-feeder": [
10109
10180
  {
10110
10181
  "path": "foodLevel",
@@ -10196,14 +10267,6 @@ Object.freeze({
10196
10267
  {
10197
10268
  "path": "powerFactor",
10198
10269
  "valueType": "number"
10199
- },
10200
- {
10201
- "path": "unit",
10202
- "valueType": "string"
10203
- },
10204
- {
10205
- "path": "precision",
10206
- "valueType": "number"
10207
10270
  }
10208
10271
  ],
10209
10272
  "presence": [
@@ -10232,20 +10295,10 @@ Object.freeze({
10232
10295
  "valueType": "number"
10233
10296
  }
10234
10297
  ],
10235
- "pressure-sensor": [
10236
- {
10237
- "path": "hpa",
10238
- "valueType": "number"
10239
- },
10240
- {
10241
- "path": "unit",
10242
- "valueType": "string"
10243
- },
10244
- {
10245
- "path": "precision",
10246
- "valueType": "number"
10247
- }
10248
- ],
10298
+ "pressure-sensor": [{
10299
+ "path": "hpa",
10300
+ "valueType": "number"
10301
+ }],
10249
10302
  "privacy-mask": [{
10250
10303
  "path": "enabled",
10251
10304
  "valueType": "boolean"
@@ -10556,20 +10609,10 @@ Object.freeze({
10556
10609
  "path": "tampered",
10557
10610
  "valueType": "boolean"
10558
10611
  }],
10559
- "temperature-sensor": [
10560
- {
10561
- "path": "celsius",
10562
- "valueType": "number"
10563
- },
10564
- {
10565
- "path": "unit",
10566
- "valueType": "string"
10567
- },
10568
- {
10569
- "path": "precision",
10570
- "valueType": "number"
10571
- }
10572
- ],
10612
+ "temperature-sensor": [{
10613
+ "path": "celsius",
10614
+ "valueType": "number"
10615
+ }],
10573
10616
  "update": [
10574
10617
  {
10575
10618
  "path": "currentVersion",
@@ -10680,10 +10723,6 @@ Object.freeze({
10680
10723
  "path": "temperature",
10681
10724
  "valueType": "number"
10682
10725
  },
10683
- {
10684
- "path": "temperatureUnit",
10685
- "valueType": "string"
10686
- },
10687
10726
  {
10688
10727
  "path": "humidity",
10689
10728
  "valueType": "number"
@@ -10692,18 +10731,10 @@ Object.freeze({
10692
10731
  "path": "pressure",
10693
10732
  "valueType": "number"
10694
10733
  },
10695
- {
10696
- "path": "pressureUnit",
10697
- "valueType": "string"
10698
- },
10699
10734
  {
10700
10735
  "path": "windSpeed",
10701
10736
  "valueType": "number"
10702
10737
  },
10703
- {
10704
- "path": "windSpeedUnit",
10705
- "valueType": "string"
10706
- },
10707
10738
  {
10708
10739
  "path": "windBearing",
10709
10740
  "valueType": "number"
@@ -10767,7 +10798,7 @@ function userError(code, params, englishFallback) {
10767
10798
  return new CamstackError(code, params, englishFallback);
10768
10799
  }
10769
10800
  //#endregion
10770
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10801
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10771
10802
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10772
10803
  DeviceType["Camera"] = "camera";
10773
10804
  DeviceType["Hub"] = "hub";
@@ -10861,8 +10892,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10861
10892
  DeviceType["PetFeeder"] = "pet-feeder";
10862
10893
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10863
10894
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10864
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10895
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10896
+ * A plug-built dishwasher carries running-state and power instead. */
10865
10897
  DeviceType["Dishwasher"] = "dishwasher";
10898
+ /** Washing machine. A plug-built one is running while its power stays
10899
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10900
+ DeviceType["Washer"] = "washer";
10901
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10902
+ DeviceType["Dryer"] = "dryer";
10866
10903
  return DeviceType;
10867
10904
  }({});
10868
10905
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -11110,7 +11147,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11110
11147
  return DeviceRole;
11111
11148
  }({});
11112
11149
  //#endregion
11113
- //#region ../types/dist/composition-kEl9mevu.mjs
11150
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
11114
11151
  /**
11115
11152
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11116
11153
  *
@@ -11425,6 +11462,13 @@ var table = {
11425
11462
  apply: () => {
11426
11463
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11427
11464
  }
11465
+ },
11466
+ schmitt: {
11467
+ minArgs: 3,
11468
+ maxArgs: 5,
11469
+ apply: () => {
11470
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11471
+ }
11428
11472
  }
11429
11473
  };
11430
11474
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11865,15 +11909,232 @@ function validateExpressionSource(src) {
11865
11909
  }
11866
11910
  return null;
11867
11911
  }
11868
- /** A composed device's stableId is this prefix plus the block id. */
11869
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11870
- /** Longest snippet a `code` source may carry (slice 4). */
11871
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11912
+ /**
11913
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11914
+ *
11915
+ * Standby of the control board is under 5 W; a motor or a heater is far
11916
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11917
+ * a cycle, so running starts only after 30 s above the line.
11918
+ *
11919
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11920
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11921
+ * report "finished" in the middle of the program, so the stop dwell is
11922
+ * 10 minutes on every type. The number is stored per type so one machine
11923
+ * can move later without moving the others.
11924
+ */
11925
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11926
+ var APPLIANCE_KINDS = [
11927
+ "washer",
11928
+ "dryer",
11929
+ "dishwasher"
11930
+ ];
11931
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11932
+ /**
11933
+ * Statistics and integrals over a history read (D944).
11934
+ *
11935
+ * Pure: the same points always give the same answer, so a restart recomputes
11936
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11937
+ * never the mean of the samples. A hole with no held value is not integrated
11938
+ * as zero. A truncated read is not published.
11939
+ */
11940
+ var STATISTIC_STATS = [
11941
+ "min",
11942
+ "max",
11943
+ "mean",
11944
+ "sum",
11945
+ "count",
11946
+ "change"
11947
+ ];
11948
+ /**
11949
+ * The range a statistics or integral helper asks history for (D944).
11950
+ *
11951
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11952
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11953
+ * is strictly longer than the longest that range can be. Forever qualifies.
11954
+ * Equal to a tier's retention is a refusal, not a short answer.
11955
+ */
11956
+ var MINUTE_MS = 6e4;
11957
+ var HOUR_MS = 60 * MINUTE_MS;
11958
+ var DAY_MS = 24 * HOUR_MS;
11959
+ var ROLLING_SPANS = [
11960
+ "1h",
11961
+ "24h",
11962
+ "2d",
11963
+ "7d",
11964
+ "30d",
11965
+ "custom"
11966
+ ];
11967
+ var CALENDAR_PERIODS = [
11968
+ "today",
11969
+ "yesterday",
11970
+ "this-week",
11971
+ "this-month"
11972
+ ];
11973
+ var RESET_PERIODS = [
11974
+ "never",
11975
+ "daily",
11976
+ "weekly",
11977
+ "monthly"
11978
+ ];
11979
+ 5 * MINUTE_MS;
11980
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11981
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11982
+ var rollingWindowObject = object({
11983
+ kind: literal("rolling"),
11984
+ span: _enum(ROLLING_SPANS),
11985
+ customMinutes: number().int().min(5).optional()
11986
+ }).check((payload) => {
11987
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11988
+ code: "custom",
11989
+ message: "a custom range needs a duration in minutes",
11990
+ path: ["customMinutes"],
11991
+ input: payload.value
11992
+ });
11993
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11994
+ code: "custom",
11995
+ message: "only a custom range carries a duration",
11996
+ path: ["customMinutes"],
11997
+ input: payload.value
11998
+ });
11999
+ });
12000
+ var RollingWindowSchema = rollingWindowObject;
12001
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
12002
+ kind: literal("calendar"),
12003
+ period: _enum(CALENDAR_PERIODS)
12004
+ })]);
12005
+ /**
12006
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
12007
+ * id. Split out so helper schemas can name it without a cycle through
12008
+ * `composition.ts` (D659).
12009
+ */
11872
12010
  var CompositionSourceRefSchema = object({
11873
12011
  /** The owning addon; `stableId` is unique only within it. */
11874
12012
  addonId: string().min(1),
11875
12013
  stableId: string().min(1)
11876
12014
  });
12015
+ var GROUP_TEMPLATE_IDS = [
12016
+ "temperature",
12017
+ "humidity",
12018
+ "pressure",
12019
+ "illuminance",
12020
+ "power",
12021
+ "energy",
12022
+ "battery",
12023
+ "numeric",
12024
+ "motion",
12025
+ "contact",
12026
+ "leak",
12027
+ "smoke",
12028
+ "occupancy",
12029
+ "binary",
12030
+ "switch",
12031
+ "light",
12032
+ "fan",
12033
+ "cover",
12034
+ "valve",
12035
+ "lock",
12036
+ "media-player",
12037
+ "climate"
12038
+ ];
12039
+ var NUMERIC_AGGREGATES = [
12040
+ "mean",
12041
+ "median",
12042
+ "min",
12043
+ "max",
12044
+ "sum"
12045
+ ];
12046
+ var BINARY_AGGREGATES = ["any", "all"];
12047
+ /** How an actuator group decides it is on. Unknown members never count as off. */
12048
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
12049
+ var AGGREGATE_REDUCERS = [
12050
+ "mean",
12051
+ "median",
12052
+ "min",
12053
+ "max",
12054
+ "sum",
12055
+ "any",
12056
+ "all"
12057
+ ];
12058
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
12059
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
12060
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
12061
+ /** One member. The field is stored so occupancy can name motion or binary. */
12062
+ var HelperMemberSchema = object({
12063
+ source: CompositionSourceRefSchema,
12064
+ cap: string().min(1),
12065
+ fieldPath: string().min(1)
12066
+ });
12067
+ var HelperSpecSchema = discriminatedUnion("kind", [
12068
+ object({
12069
+ kind: literal("group"),
12070
+ templateId: GroupTemplateIdSchema,
12071
+ aggregate: AggregateReducerSchema,
12072
+ members: array(HelperMemberSchema).min(1).max(64),
12073
+ /** Actuator groups only. Readings leave this unset. */
12074
+ active: ActiveCriterionSchema.optional()
12075
+ }),
12076
+ object({
12077
+ kind: literal("appliance"),
12078
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
12079
+ appliance: _enum(APPLIANCE_KINDS),
12080
+ thresholdWatts: number().positive(),
12081
+ startDwellMs: number().int().nonnegative(),
12082
+ stopDwellMs: number().int().nonnegative(),
12083
+ member: HelperMemberSchema
12084
+ }),
12085
+ object({
12086
+ kind: literal("statistic"),
12087
+ stat: _enum(STATISTIC_STATS),
12088
+ window: StatisticWindowSchema,
12089
+ source: HelperMemberSchema
12090
+ }),
12091
+ object({
12092
+ kind: literal("integral"),
12093
+ reset: _enum(RESET_PERIODS),
12094
+ output: _enum(["energy", "meter"]),
12095
+ source: HelperMemberSchema
12096
+ }),
12097
+ object({
12098
+ kind: literal("threshold"),
12099
+ source: HelperMemberSchema,
12100
+ high: number(),
12101
+ low: number(),
12102
+ onForMs: number().int().nonnegative(),
12103
+ offForMs: number().int().nonnegative()
12104
+ }),
12105
+ object({
12106
+ kind: literal("arithmetic"),
12107
+ op: _enum([
12108
+ "sum",
12109
+ "difference",
12110
+ "ratio"
12111
+ ]),
12112
+ inputs: array(HelperMemberSchema).min(2).max(64)
12113
+ }),
12114
+ object({
12115
+ kind: literal("expression"),
12116
+ expr: string().min(1),
12117
+ inputs: array(object({
12118
+ name: string().min(1),
12119
+ member: HelperMemberSchema
12120
+ })).min(1).max(64),
12121
+ output: _enum(["number", "boolean"])
12122
+ }),
12123
+ object({
12124
+ kind: literal("derivative"),
12125
+ window: RollingWindowSchema,
12126
+ per: _enum([
12127
+ "second",
12128
+ "minute",
12129
+ "hour"
12130
+ ]),
12131
+ source: HelperMemberSchema
12132
+ })
12133
+ ]);
12134
+ /** A composed device's stableId is this prefix plus the block id. */
12135
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12136
+ /** Longest snippet a `code` source may carry (slice 4). */
12137
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11877
12138
  /** One field of one capability of one source device. */
11878
12139
  var CompositionFieldReadSchema = object({
11879
12140
  source: CompositionSourceRefSchema,
@@ -11917,7 +12178,59 @@ var CompositionCodeSourceSchema = object({
11917
12178
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11918
12179
  CompositionFromSourceSchema,
11919
12180
  CompositionExpressionSourceSchema,
11920
- CompositionCodeSourceSchema
12181
+ CompositionCodeSourceSchema,
12182
+ object({
12183
+ kind: literal("aggregate"),
12184
+ reducer: _enum([
12185
+ "mean",
12186
+ "median",
12187
+ "min",
12188
+ "max",
12189
+ "sum",
12190
+ "any",
12191
+ "all"
12192
+ ]),
12193
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12194
+ /**
12195
+ * Set when the reads are an enum state, not a yes/no: the listed states
12196
+ * count as active (a cover is `open`). A state outside the list is inactive;
12197
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
12198
+ */
12199
+ activeWhen: array(string().min(1)).min(1).optional(),
12200
+ /** `value` is the reading; the others are the member counts. */
12201
+ publish: _enum([
12202
+ "value",
12203
+ "included",
12204
+ "excluded",
12205
+ "total"
12206
+ ]),
12207
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12208
+ empty: _enum([
12209
+ "null",
12210
+ "omit",
12211
+ "offline"
12212
+ ])
12213
+ }),
12214
+ object({
12215
+ kind: literal("series"),
12216
+ read: CompositionFieldReadSchema,
12217
+ /** `value` is the statistic or the integral. The others are the gap counts. */
12218
+ publish: _enum([
12219
+ "value",
12220
+ "included",
12221
+ "excluded",
12222
+ "total"
12223
+ ]),
12224
+ /**
12225
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
12226
+ * device unseeded until history answers, so a restart cannot publish 0.
12227
+ */
12228
+ empty: _enum([
12229
+ "null",
12230
+ "omit",
12231
+ "offline"
12232
+ ])
12233
+ })
11921
12234
  ]);
11922
12235
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11923
12236
  kind: literal("forward"),
@@ -11963,7 +12276,13 @@ var CompositionSchema = object({
11963
12276
  kind: literal("existing"),
11964
12277
  device: CompositionSourceRefSchema
11965
12278
  })]),
11966
- features: array(CompositionFeatureSchema).min(1).max(16)
12279
+ features: array(CompositionFeatureSchema).min(1).max(16),
12280
+ /**
12281
+ * Present when the block is a helper template. Absent on a free
12282
+ * composition (the room example, device 4603). The editor reads this,
12283
+ * never the compiled features.
12284
+ */
12285
+ helper: HelperSpecSchema.optional()
11967
12286
  }).superRefine((composition, ctx) => {
11968
12287
  const seen = /* @__PURE__ */ new Set();
11969
12288
  composition.features.forEach((feature, index) => {
@@ -17206,7 +17525,22 @@ var BrokerProviderInfoSchema = object({
17206
17525
  */
17207
17526
  interactiveConnectPath: string().min(1).optional(),
17208
17527
  /** Relative hub path to disconnect the linked account (same popup pattern). */
17209
- interactiveDisconnectPath: string().min(1).optional()
17528
+ interactiveDisconnectPath: string().min(1).optional(),
17529
+ /**
17530
+ * The kind signs in through a server-held interactive session instead of
17531
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17532
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17533
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17534
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17535
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17536
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17537
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17538
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17539
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17540
+ * offers it as the default path; the fields the session fills are
17541
+ * marked `filledBySignIn` in the kind's settings schema.
17542
+ */
17543
+ signIn: object({ basePath: string().min(1) }).optional()
17210
17544
  }))
17211
17545
  });
17212
17546
  var ListInputSchema = object({
@@ -18250,7 +18584,12 @@ var CompositionProblemSchema = object({
18250
18584
  "item-field-reserved",
18251
18585
  "target-missing",
18252
18586
  "target-unreadable",
18253
- "not-replaceable"
18587
+ "not-replaceable",
18588
+ "native-capability-replace",
18589
+ "mixed-quantity",
18590
+ "helper-mismatch",
18591
+ "member-constraint",
18592
+ "retention"
18254
18593
  ]),
18255
18594
  path: string(),
18256
18595
  message: string()
@@ -18271,7 +18610,8 @@ var CompositionUnavailableModeSchema = _enum([
18271
18610
  "null",
18272
18611
  "offline",
18273
18612
  "unknown",
18274
- "release"
18613
+ "release",
18614
+ "omit"
18275
18615
  ]);
18276
18616
  /**
18277
18617
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18308,9 +18648,11 @@ var CompositionFieldPlanSchema = object({
18308
18648
  reads: array(CompositionFieldReadSchema)
18309
18649
  });
18310
18650
  /**
18311
- * What a feature does to its target (D663): `new` builds a capability on a new
18312
- * device; on an existing device it `add`s a capability the device lacks, or
18313
- * `replace`s named fields of one it has natively.
18651
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18652
+ * new device; on an existing device it `add`s a capability the device lacks.
18653
+ * Replacing a capability the device already serves is refused
18654
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18655
+ * wire for a claim the hub already holds.
18314
18656
  */
18315
18657
  var CompositionFeatureModeSchema = _enum([
18316
18658
  "new",
@@ -18481,8 +18823,392 @@ Object.freeze({
18481
18823
  latchedAt: {
18482
18824
  kind: "fixed",
18483
18825
  result: ["number", "null"]
18826
+ },
18827
+ schmitt: {
18828
+ kind: "fixed",
18829
+ result: ["boolean", "null"]
18484
18830
  }
18485
18831
  });
18832
+ /**
18833
+ * One catalog row per base reading a group helper can build (D944 §3).
18834
+ * Data, not a form default written into the block.
18835
+ */
18836
+ var NOW$2 = {
18837
+ fieldPath: "lastFetchedAt",
18838
+ expr: "now"
18839
+ };
18840
+ var CHANGED = {
18841
+ fieldPath: "lastChangedAt",
18842
+ expr: "now"
18843
+ };
18844
+ function numeric(input) {
18845
+ return {
18846
+ ...input,
18847
+ family: "numeric",
18848
+ deviceType: DeviceType.Sensor,
18849
+ memberDeviceType: DeviceType.Sensor,
18850
+ memberDeviceTypes: [DeviceType.Sensor],
18851
+ activeWhen: void 0,
18852
+ commandCaps: [],
18853
+ baseUnit: baseUnitOf(input.quantity),
18854
+ aggregates: NUMERIC_AGGREGATES,
18855
+ accept: [{
18856
+ cap: input.outputCap,
18857
+ fieldPath: input.outputField
18858
+ }]
18859
+ };
18860
+ }
18861
+ function binary(input) {
18862
+ return {
18863
+ ...input,
18864
+ family: "binary",
18865
+ deviceType: DeviceType.Sensor,
18866
+ memberDeviceType: DeviceType.Sensor,
18867
+ memberDeviceTypes: [DeviceType.Sensor],
18868
+ activeWhen: void 0,
18869
+ commandCaps: [],
18870
+ quantity: "none",
18871
+ baseUnit: void 0,
18872
+ aggregates: BINARY_AGGREGATES,
18873
+ empty: "offline"
18874
+ };
18875
+ }
18876
+ var GROUP_TEMPLATES = [
18877
+ numeric({
18878
+ id: "temperature",
18879
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18880
+ role: DeviceRole.TemperatureSensor,
18881
+ outputCap: "temperature-sensor",
18882
+ outputField: "celsius",
18883
+ quantity: "temperature",
18884
+ defaultAggregate: "mean",
18885
+ empty: "null",
18886
+ companions: [NOW$2]
18887
+ }),
18888
+ numeric({
18889
+ id: "humidity",
18890
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18891
+ role: DeviceRole.HumiditySensor,
18892
+ outputCap: "humidity-sensor",
18893
+ outputField: "percent",
18894
+ quantity: "humidity",
18895
+ defaultAggregate: "mean",
18896
+ empty: "null",
18897
+ companions: [NOW$2]
18898
+ }),
18899
+ numeric({
18900
+ id: "pressure",
18901
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18902
+ role: DeviceRole.PressureSensor,
18903
+ outputCap: "pressure-sensor",
18904
+ outputField: "hpa",
18905
+ quantity: "pressure",
18906
+ defaultAggregate: "mean",
18907
+ empty: "null",
18908
+ companions: [NOW$2]
18909
+ }),
18910
+ numeric({
18911
+ id: "illuminance",
18912
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18913
+ role: DeviceRole.AmbientLightSensor,
18914
+ outputCap: "ambient-light-sensor",
18915
+ outputField: "lux",
18916
+ quantity: "illuminance",
18917
+ defaultAggregate: "mean",
18918
+ empty: "null",
18919
+ companions: [NOW$2]
18920
+ }),
18921
+ numeric({
18922
+ id: "power",
18923
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18924
+ role: DeviceRole.PowerSensor,
18925
+ outputCap: "power-meter",
18926
+ outputField: "watts",
18927
+ quantity: "power",
18928
+ defaultAggregate: "sum",
18929
+ empty: "omit",
18930
+ companions: [NOW$2]
18931
+ }),
18932
+ numeric({
18933
+ id: "energy",
18934
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18935
+ role: DeviceRole.EnergySensor,
18936
+ outputCap: "power-meter",
18937
+ outputField: "kwhTotal",
18938
+ quantity: "energy",
18939
+ defaultAggregate: "sum",
18940
+ empty: "omit",
18941
+ companions: [NOW$2]
18942
+ }),
18943
+ numeric({
18944
+ id: "battery",
18945
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18946
+ role: DeviceRole.BatterySensor,
18947
+ outputCap: "battery",
18948
+ outputField: "percentage",
18949
+ quantity: "percentage",
18950
+ defaultAggregate: "mean",
18951
+ empty: "null",
18952
+ companions: [
18953
+ {
18954
+ fieldPath: "charging",
18955
+ expr: "\"unknown\""
18956
+ },
18957
+ {
18958
+ fieldPath: "sleeping",
18959
+ expr: "false"
18960
+ },
18961
+ {
18962
+ fieldPath: "lastUpdated",
18963
+ expr: "now"
18964
+ }
18965
+ ]
18966
+ }),
18967
+ numeric({
18968
+ id: "numeric",
18969
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18970
+ role: DeviceRole.NumericSensor,
18971
+ outputCap: "numeric-sensor",
18972
+ outputField: "value",
18973
+ quantity: "device",
18974
+ defaultAggregate: "mean",
18975
+ empty: "null",
18976
+ companions: [NOW$2]
18977
+ }),
18978
+ binary({
18979
+ id: "motion",
18980
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18981
+ role: DeviceRole.MotionSensor,
18982
+ outputCap: "motion",
18983
+ outputField: "detected",
18984
+ defaultAggregate: "any",
18985
+ accept: [{
18986
+ cap: "motion",
18987
+ fieldPath: "detected"
18988
+ }],
18989
+ companions: [{
18990
+ fieldPath: "lastDetectedAt",
18991
+ expr: "null"
18992
+ }, {
18993
+ fieldPath: "autoClearAfterMs",
18994
+ expr: "null"
18995
+ }]
18996
+ }),
18997
+ binary({
18998
+ id: "contact",
18999
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
19000
+ role: DeviceRole.ContactSensor,
19001
+ outputCap: "contact",
19002
+ outputField: "entryOpen",
19003
+ defaultAggregate: "any",
19004
+ accept: [{
19005
+ cap: "contact",
19006
+ fieldPath: "entryOpen"
19007
+ }],
19008
+ companions: [CHANGED]
19009
+ }),
19010
+ binary({
19011
+ id: "leak",
19012
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
19013
+ role: DeviceRole.LeakSensor,
19014
+ outputCap: "flood",
19015
+ outputField: "flooded",
19016
+ defaultAggregate: "any",
19017
+ accept: [{
19018
+ cap: "flood",
19019
+ fieldPath: "flooded"
19020
+ }],
19021
+ companions: [CHANGED]
19022
+ }),
19023
+ binary({
19024
+ id: "smoke",
19025
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
19026
+ role: DeviceRole.SmokeSensor,
19027
+ outputCap: "smoke",
19028
+ outputField: "detected",
19029
+ defaultAggregate: "any",
19030
+ accept: [{
19031
+ cap: "smoke",
19032
+ fieldPath: "detected"
19033
+ }],
19034
+ companions: [CHANGED]
19035
+ }),
19036
+ binary({
19037
+ id: "occupancy",
19038
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
19039
+ role: DeviceRole.BinarySensor,
19040
+ outputCap: "binary",
19041
+ outputField: "on",
19042
+ defaultAggregate: "any",
19043
+ accept: [{
19044
+ cap: "motion",
19045
+ fieldPath: "detected"
19046
+ }, {
19047
+ cap: "binary",
19048
+ fieldPath: "on"
19049
+ }],
19050
+ companions: [CHANGED]
19051
+ }),
19052
+ binary({
19053
+ id: "binary",
19054
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
19055
+ role: DeviceRole.BinarySensor,
19056
+ outputCap: "binary",
19057
+ outputField: "on",
19058
+ defaultAggregate: "any",
19059
+ accept: [{
19060
+ cap: "binary",
19061
+ fieldPath: "on"
19062
+ }],
19063
+ companions: [CHANGED]
19064
+ }),
19065
+ actuator({
19066
+ id: "switch",
19067
+ 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.",
19068
+ deviceType: DeviceType.Switch,
19069
+ outputCap: "switch",
19070
+ outputField: "on",
19071
+ member: {
19072
+ cap: "switch",
19073
+ fieldPath: "on"
19074
+ },
19075
+ commandCaps: ["switch"]
19076
+ }),
19077
+ actuator({
19078
+ id: "light",
19079
+ 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.",
19080
+ deviceType: DeviceType.Light,
19081
+ outputCap: "switch",
19082
+ outputField: "on",
19083
+ member: {
19084
+ cap: "switch",
19085
+ fieldPath: "on"
19086
+ },
19087
+ commandCaps: [
19088
+ "switch",
19089
+ "brightness",
19090
+ "color"
19091
+ ]
19092
+ }),
19093
+ actuator({
19094
+ id: "fan",
19095
+ 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.",
19096
+ deviceType: DeviceType.Fan,
19097
+ outputCap: "switch",
19098
+ outputField: "on",
19099
+ member: {
19100
+ cap: "switch",
19101
+ fieldPath: "on"
19102
+ },
19103
+ commandCaps: ["switch", "fan-control"]
19104
+ }),
19105
+ actuator({
19106
+ id: "cover",
19107
+ 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.",
19108
+ deviceType: DeviceType.Cover,
19109
+ outputCap: "group-active",
19110
+ outputField: "active",
19111
+ member: {
19112
+ cap: "cover",
19113
+ fieldPath: "state"
19114
+ },
19115
+ activeWhen: [
19116
+ "open",
19117
+ "opening",
19118
+ "stopped"
19119
+ ],
19120
+ commandCaps: ["cover"]
19121
+ }),
19122
+ actuator({
19123
+ id: "valve",
19124
+ 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.",
19125
+ deviceType: DeviceType.Valve,
19126
+ outputCap: "group-active",
19127
+ outputField: "active",
19128
+ member: {
19129
+ cap: "valve",
19130
+ fieldPath: "state"
19131
+ },
19132
+ activeWhen: [
19133
+ "open",
19134
+ "opening",
19135
+ "stopped"
19136
+ ],
19137
+ commandCaps: ["valve"]
19138
+ }),
19139
+ actuator({
19140
+ id: "lock",
19141
+ 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.",
19142
+ deviceType: DeviceType.Lock,
19143
+ outputCap: "group-active",
19144
+ outputField: "active",
19145
+ member: {
19146
+ cap: "lock-control",
19147
+ fieldPath: "state"
19148
+ },
19149
+ activeWhen: ["locked"],
19150
+ commandCaps: ["lock-control"]
19151
+ }),
19152
+ actuator({
19153
+ id: "media-player",
19154
+ 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.",
19155
+ deviceType: DeviceType.MediaPlayer,
19156
+ outputCap: "group-active",
19157
+ outputField: "active",
19158
+ member: {
19159
+ cap: "media-player",
19160
+ fieldPath: "state"
19161
+ },
19162
+ activeWhen: ["playing", "buffering"],
19163
+ commandCaps: ["media-player"]
19164
+ }),
19165
+ actuator({
19166
+ id: "climate",
19167
+ 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.",
19168
+ deviceType: DeviceType.Thermostat,
19169
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
19170
+ outputCap: "group-active",
19171
+ outputField: "active",
19172
+ member: {
19173
+ cap: "climate-control",
19174
+ fieldPath: "mode"
19175
+ },
19176
+ activeWhen: [
19177
+ "heat",
19178
+ "cool",
19179
+ "auto",
19180
+ "heat_cool",
19181
+ "fan_only",
19182
+ "dry"
19183
+ ],
19184
+ commandCaps: ["climate-control"]
19185
+ })
19186
+ ];
19187
+ function actuator(input) {
19188
+ return {
19189
+ id: input.id,
19190
+ summary: input.summary,
19191
+ deviceType: input.deviceType,
19192
+ outputCap: input.outputCap,
19193
+ outputField: input.outputField,
19194
+ commandCaps: input.commandCaps,
19195
+ family: "actuator",
19196
+ role: void 0,
19197
+ memberDeviceType: input.deviceType,
19198
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
19199
+ activeWhen: input.activeWhen,
19200
+ quantity: "none",
19201
+ baseUnit: void 0,
19202
+ aggregates: BINARY_AGGREGATES,
19203
+ defaultAggregate: "any",
19204
+ empty: "offline",
19205
+ accept: [input.member],
19206
+ companions: [CHANGED]
19207
+ };
19208
+ }
19209
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19210
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19211
+ DeviceType.Camera;
18486
19212
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18487
19213
  /**
18488
19214
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20196,12 +20922,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20196
20922
  * providers detect renames by matching `uniqueId` and patch `id`
20197
20923
  * in-place so dispatch keeps working.
20198
20924
  *
20199
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20200
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20201
- * state push and are the single source of truth. The `device_class` tag is
20202
- * an HA-specific input that is consumed at the discovery boundary
20203
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20204
- * stored after that point.
20925
+ * **Display unit and precision are NOT stored here.** They are a read-time
20926
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20927
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20928
+ * neither is repeated on each sample. The `device_class` tag is an
20929
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20930
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20205
20931
  *
20206
20932
  * The `raw` escape hatch carries provider-specific attributes that don't
20207
20933
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20215,6 +20941,14 @@ var SourceInfoSchema = object({
20215
20941
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20216
20942
  * Used to detect rename when `id` changes. */
20217
20943
  uniqueId: string().optional(),
20944
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20945
+ quantity: ReadingQuantitySchema.optional(),
20946
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20947
+ dateTimeShape: _enum([
20948
+ "date",
20949
+ "time",
20950
+ "datetime"
20951
+ ]).optional(),
20218
20952
  /** Provider-specific extras that don't fit the structured slots.
20219
20953
  * Whitelist what goes here per provider — do NOT dump entire HA
20220
20954
  * attribute blobs (would explode the meta JSON column). */
@@ -41127,7 +41861,8 @@ method(object({
41127
41861
  "enum"
41128
41862
  ]),
41129
41863
  enumValues: array(string()).optional(),
41130
- item: boolean().optional()
41864
+ item: boolean().optional(),
41865
+ quantity: ReadingQuantitySchema.optional()
41131
41866
  })).readonly(),
41132
41867
  itemArray: object({
41133
41868
  path: string(),
@@ -43488,7 +44223,7 @@ var NetworkAccessStatusSchema = object({
43488
44223
  });
43489
44224
  /**
43490
44225
  * Optional, richer endpoint shape returned by providers that expose
43491
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44226
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43492
44227
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43493
44228
  * the originating provider config (mode + sourcePort) so the
43494
44229
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44927,8 +45662,8 @@ var MaskPolygonShapeSchema = object({
44927
45662
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44928
45663
  var MaskGridShapeSchema = object({
44929
45664
  kind: literal("grid"),
44930
- gridWidth: number(),
44931
- gridHeight: number(),
45665
+ gridWidth: number().meta(readingMeta("none")),
45666
+ gridHeight: number().meta(readingMeta("none")),
44932
45667
  cells: array(boolean())
44933
45668
  });
44934
45669
  discriminatedUnion("kind", [
@@ -45040,6 +45775,8 @@ var NcSystemEventKindSchema = _enum([
45040
45775
  "detection-blind",
45041
45776
  "motion-noisy",
45042
45777
  "motion-calm",
45778
+ "package-llm-silent",
45779
+ "package-llm-answered",
45043
45780
  "recording-stalled",
45044
45781
  "recording-healthy",
45045
45782
  "recording-degraded",
@@ -46129,7 +46866,20 @@ var NcRuleInputSchema = object({
46129
46866
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46130
46867
  * path does not run Zod defaults.
46131
46868
  */
46132
- groupingSec: number().int().min(5).max(60).optional()
46869
+ groupingSec: number().int().min(5).max(60).optional(),
46870
+ /**
46871
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46872
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46873
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46874
+ * same mark the Library writes, and records whose notification it was and
46875
+ * when. A channel with no buttons gets the same two choices as links.
46876
+ *
46877
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46878
+ * cap path): absent and `false` both mean off, which is what every rule
46879
+ * authored before this field meant. An editor turns it off by writing
46880
+ * `false`, so it needs no `clear` key.
46881
+ */
46882
+ askVerdict: boolean().optional()
46133
46883
  });
46134
46884
  /**
46135
46885
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -56067,6 +56817,28 @@ var PackageVersionInfoSchema = object({
56067
56817
  deprecated: string().optional(),
56068
56818
  distTags: array(string()).readonly()
56069
56819
  });
56820
+ /**
56821
+ * One published document. `ok` is markdown from the registry. `missing` means
56822
+ * the registry answered and the file was not there. `error` is a registry or
56823
+ * tarball failure — never an empty string standing in for either.
56824
+ */
56825
+ var PackageDocBodySchema = discriminatedUnion("status", [
56826
+ object({
56827
+ status: literal("ok"),
56828
+ markdown: string()
56829
+ }),
56830
+ object({ status: literal("missing") }),
56831
+ object({
56832
+ status: literal("error"),
56833
+ message: string()
56834
+ })
56835
+ ]);
56836
+ var PackageDocsSchema = object({
56837
+ name: string(),
56838
+ version: string(),
56839
+ readme: PackageDocBodySchema,
56840
+ changelog: PackageDocBodySchema
56841
+ });
56070
56842
  var SearchResultSchema = object({
56071
56843
  name: string(),
56072
56844
  version: string(),
@@ -56285,7 +57057,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56285
57057
  }), object({ success: literal(true) }), {
56286
57058
  kind: "mutation",
56287
57059
  auth: "admin"
56288
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57060
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57061
+ name: string().min(1),
57062
+ version: string().min(1).optional()
57063
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56289
57064
  kind: "mutation",
56290
57065
  auth: "admin"
56291
57066
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56335,46 +57110,27 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56335
57110
  }), LogStreamEntrySchema, { kind: "subscription" });
56336
57111
  object({
56337
57112
  /** Carbon dioxide concentration in ppm. */
56338
- co2Ppm: number().min(0).optional(),
57113
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56339
57114
  /** Total volatile organic compounds in ppb. */
56340
- vocPpb: number().min(0).optional(),
57115
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56341
57116
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56342
- pm25: number().min(0).optional(),
57117
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56343
57118
  /** Particulate matter ≤ 10 μm in µg/m³. */
56344
- pm10: number().min(0).optional(),
57119
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56345
57120
  /** Composite AQI value (typically 0..500). */
56346
- aqi: number().optional(),
57121
+ aqi: number().optional().meta(readingMeta("index")),
56347
57122
  /** Ms epoch when the slice was last updated. */
56348
- lastFetchedAt: number(),
56349
- /** Live display unit of the single metric this slice carries (e.g. HA
56350
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56351
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56352
- * per slice is unambiguous. */
56353
- unit: string().optional(),
56354
- /** Suggested decimal places for numeric display.
56355
- * Populated live from the upstream source when provided (e.g. HA
56356
- * `attributes.suggested_display_precision`). Falls back to
56357
- * auto-formatting when absent. */
56358
- precision: number().int().min(0).max(10).optional()
57123
+ lastFetchedAt: number()
56359
57124
  });
56360
57125
  DeviceType.Sensor;
56361
57126
  object({
56362
- /** Current illuminance in lux (lx). */
56363
- lux: number().min(0).meta({
56364
- unit: "lx",
57127
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57128
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57129
+ precision: 0,
56365
57130
  label: "Illuminance"
56366
- }),
57131
+ })),
56367
57132
  /** Ms epoch when the slice was last updated. */
56368
- lastFetchedAt: number(),
56369
- /** Live display unit from the upstream source (e.g. HA
56370
- * `attributes.unit_of_measurement`). The UI prefers this over the
56371
- * role's canonical unit. Absent → fall back to the canonical unit. */
56372
- unit: string().optional(),
56373
- /** Suggested decimal places for numeric display.
56374
- * Populated live from the upstream source when provided (e.g. HA
56375
- * `attributes.suggested_display_precision`). Falls back to
56376
- * auto-formatting when absent. */
56377
- precision: number().int().min(0).max(10).optional()
57133
+ lastFetchedAt: number()
56378
57134
  });
56379
57135
  DeviceType.Sensor;
56380
57136
  /**
@@ -56519,10 +57275,7 @@ var BatteryStatusSchema = object({
56519
57275
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56520
57276
  * declines to band a non-finite reading, which is the correct shape.
56521
57277
  */
56522
- percentage: number().min(0).max(100).nullable().meta({
56523
- unit: "%",
56524
- label: "Charge"
56525
- }),
57278
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56526
57279
  /**
56527
57280
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56528
57281
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56549,7 +57302,14 @@ var BatteryStatusSchema = object({
56549
57302
  */
56550
57303
  sleeping: boolean().meta({ label: "Sleeping" }),
56551
57304
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56552
- lastUpdated: number(),
57305
+ lastUpdated: number().meta(readingMeta("none")),
57306
+ /**
57307
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57308
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57309
+ * that only report charge. A second reading on this cap, not a second
57310
+ * `numeric-sensor` on the same device.
57311
+ */
57312
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56553
57313
  /**
56554
57314
  * Ms epoch of the last time the device PROVED it was reachable — a
56555
57315
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56589,7 +57349,7 @@ var BatteryStatusSchema = object({
56589
57349
  */
56590
57350
  binary: boolean().optional().meta({ label: "Binary indicator" })
56591
57351
  });
56592
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat;
57352
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter;
56593
57353
  method(object({
56594
57354
  deviceId: number(),
56595
57355
  /** Bound on the wait. Sensible range 3000–10000ms. */
@@ -56609,7 +57369,7 @@ object({
56609
57369
  DeviceType.Sensor;
56610
57370
  object({
56611
57371
  /** Current level as 0..100 inclusive. Firmware-reported. */
56612
- percentage: number().min(0).max(100),
57372
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56613
57373
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56614
57374
  lastChangedAt: number()
56615
57375
  });
@@ -57004,21 +57764,21 @@ object({
57004
57764
  /** Single setpoint in Celsius. Used by single-target modes
57005
57765
  * (heat / cool / dry). Null when the device is in a range mode
57006
57766
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57007
- target: number().nullable(),
57767
+ target: number().nullable().meta(readingMeta("temperature")),
57008
57768
  /** Upper bound of the dual setpoint in Celsius. Populated only
57009
57769
  * in `heat_cool` mode. */
57010
- targetHigh: number().nullable(),
57770
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57011
57771
  /** Lower bound of the dual setpoint in Celsius. Populated only
57012
57772
  * in `heat_cool` mode. */
57013
- targetLow: number().nullable(),
57773
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57014
57774
  /** Target relative humidity (0..100). Null when the device has no
57015
57775
  * humidity control. */
57016
- targetHumidity: number().min(0).max(100).nullable(),
57776
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57017
57777
  /** Current measured temperature in Celsius. Read-only — pushed by
57018
57778
  * the device's internal sensor. */
57019
- currentTemp: number().nullable(),
57779
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57020
57780
  /** Current measured relative humidity (0..100). Read-only. */
57021
- currentHumidity: number().min(0).max(100).nullable(),
57781
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57022
57782
  /** Vertical louver swing on/off. Null when the device has no vertical
57023
57783
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57024
57784
  swingVertical: boolean().nullable(),
@@ -57026,11 +57786,11 @@ object({
57026
57786
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57027
57787
  swingHorizontal: boolean().nullable(),
57028
57788
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57029
- minTemp: number().nullable().optional(),
57789
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57030
57790
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57031
- maxTemp: number().nullable().optional(),
57791
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57032
57792
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57033
- tempStep: number().positive().nullable().optional(),
57793
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57034
57794
  /** Ms epoch when the slice was last updated (push or command). */
57035
57795
  lastFetchedAt: number()
57036
57796
  });
@@ -57109,17 +57869,17 @@ DeviceType.Thermostat, DeviceType.Climate, method(object({
57109
57869
  * modes to expose in the picker without a slice fetch.
57110
57870
  */
57111
57871
  var RgbTripletSchema = object({
57112
- r: number().int().min(0).max(255),
57113
- g: number().int().min(0).max(255),
57114
- b: number().int().min(0).max(255)
57872
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57873
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57874
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57115
57875
  });
57116
57876
  var HsvTripletSchema = object({
57117
57877
  /** Hue in degrees, 0..360. */
57118
- h: number().min(0).max(360),
57878
+ h: number().min(0).max(360).meta(readingMeta("none")),
57119
57879
  /** Saturation as 0..100 inclusive. */
57120
- s: number().min(0).max(100),
57880
+ s: number().min(0).max(100).meta(readingMeta("none")),
57121
57881
  /** Value/brightness as 0..100 inclusive. */
57122
- v: number().min(0).max(100)
57882
+ v: number().min(0).max(100).meta(readingMeta("none"))
57123
57883
  });
57124
57884
  var ColorInputSchema = discriminatedUnion("mode", [
57125
57885
  object({
@@ -57148,7 +57908,7 @@ object({
57148
57908
  /** Populated when `mode === 'hsv'`. */
57149
57909
  hsv: HsvTripletSchema.optional(),
57150
57910
  /** Populated when `mode === 'mired'`. */
57151
- mireds: number().int().optional(),
57911
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57152
57912
  /** Ms epoch of the last operator-driven change. */
57153
57913
  lastChangedAt: number()
57154
57914
  });
@@ -57272,18 +58032,12 @@ _enum([
57272
58032
  "empty",
57273
58033
  "full"
57274
58034
  ]);
57275
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57276
- * each. */
57277
- var ConsumableRemainingUnitSchema = _enum([
57278
- "days",
57279
- "hours",
57280
- "cycles"
57281
- ]);
57282
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57283
- * days). `value` null = unknown (D393). */
58035
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58036
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58037
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57284
58038
  var ConsumableRemainingSchema = object({
57285
- value: number().min(0).nullable(),
57286
- unit: ConsumableRemainingUnitSchema
58039
+ value: number().min(0).nullable().meta(readingMeta("device")),
58040
+ kind: ConsumableRemainingKindSchema
57287
58041
  });
57288
58042
  var ConsumablesStatusSchema = object({
57289
58043
  items: array(object({
@@ -57292,7 +58046,7 @@ var ConsumablesStatusSchema = object({
57292
58046
  /** Display name. */
57293
58047
  label: string().min(1),
57294
58048
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57295
- level: number().min(0).max(100).nullable(),
58049
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57296
58050
  /** The provider's discrete verdict when known (binary mode). */
57297
58051
  status: ConsumableStatusSchema.nullable(),
57298
58052
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57309,7 +58063,7 @@ var ConsumablesStatusSchema = object({
57309
58063
  })),
57310
58064
  lastChangedAt: number()
57311
58065
  });
57312
- Object.values(DeviceType), [...ConsumableRemainingUnitSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
58066
+ Object.values(DeviceType), [...ConsumableRemainingKindSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57313
58067
  Object.values(DeviceType), method(object({
57314
58068
  deviceId: number().int().nonnegative(),
57315
58069
  key: string().min(1)
@@ -57334,39 +58088,17 @@ object({
57334
58088
  /** Current value. Numeric kind → number; select/text/datetime → string.
57335
58089
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
57336
58090
  * or time-only — HA accepts all three forms). */
57337
- value: union([number(), string()]),
58091
+ value: union([number().meta(readingMeta("device")), string()]),
57338
58092
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
57339
58093
  options: array(string()),
57340
58094
  /** Ms epoch when the slice was last updated. */
57341
58095
  lastChangedAt: number(),
57342
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
57343
- * Populated live from HA `attributes.unit_of_measurement` on each push.
57344
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57345
- unit: string().optional(),
57346
- /** Minimum allowed value. Populated live from HA `attributes.min`.
57347
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57348
- min: number().optional(),
57349
- /** Maximum allowed value. Populated live from HA `attributes.max`.
57350
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57351
- max: number().optional(),
57352
- /** Step increment. Populated live from HA `attributes.step`.
57353
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57354
- step: number().optional(),
57355
- /** Suggested decimal places for numeric display. Populated live from HA
57356
- * `attributes.suggested_display_precision`, or derived from `step` when the
57357
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
57358
- * other kinds. Falls back to auto-formatting when absent. */
57359
- precision: number().int().min(0).max(10).optional(),
57360
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
57361
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
57362
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
57363
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
57364
- * `has_date`/`has_time` attributes on each push. */
57365
- format: _enum([
57366
- "date",
57367
- "time",
57368
- "datetime"
57369
- ]).optional()
58096
+ /** Minimum allowed value, in the device quantity's base unit. */
58097
+ min: number().optional().meta(readingMeta("device")),
58098
+ /** Maximum allowed value, in the device quantity's base unit. */
58099
+ max: number().optional().meta(readingMeta("device")),
58100
+ /** Step increment, in the device quantity's base unit. */
58101
+ step: number().optional().meta(readingMeta("device"))
57370
58102
  });
57371
58103
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
57372
58104
  object({
@@ -57404,9 +58136,9 @@ object({
57404
58136
  ]),
57405
58137
  /** 0 = fully closed, 100 = fully open. Null when the device has
57406
58138
  * no intermediate position surface. */
57407
- position: number().min(0).max(100).nullable(),
58139
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57408
58140
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
57409
- tiltPosition: number().min(0).max(100).nullable(),
58141
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57410
58142
  /** Ms epoch when the slice was last updated. */
57411
58143
  lastChangedAt: number()
57412
58144
  });
@@ -57464,9 +58196,9 @@ var NormalizedRangeSchema$1 = object({
57464
58196
  object({
57465
58197
  mode: DayNightModeSchema,
57466
58198
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
57467
- sensitivity: number().optional(),
58199
+ sensitivity: number().optional().meta(readingMeta("none")),
57468
58200
  /** Delay before the IR-cut filter flips, in seconds. */
57469
- switchDelaySec: number().optional(),
58201
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
57470
58202
  lastFetchedAt: number()
57471
58203
  });
57472
58204
  /**
@@ -57621,11 +58353,11 @@ object({
57621
58353
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
57622
58354
  availablePrograms: array(string()),
57623
58355
  /** Seconds remaining on the active cycle, or null when not running / not known. */
57624
- remainingSeconds: number().int().nonnegative().nullable(),
58356
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57625
58357
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
57626
- delayedStartSeconds: number().int().nonnegative().nullable(),
58358
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57627
58359
  /** Cycle completion 0..100, or null when not applicable. */
57628
- progressPercent: number().min(0).max(100).nullable(),
58360
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57629
58361
  /** True when the door is reported open; null when the door state is not known. */
57630
58362
  doorOpen: boolean().nullable(),
57631
58363
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -57649,7 +58381,7 @@ object({
57649
58381
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57650
58382
  machineCareDue: boolean().nullable(),
57651
58383
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57652
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
58384
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
57653
58385
  /** Option states: null when the appliance does not expose the option or has not said. */
57654
58386
  halfLoad: boolean().nullable(),
57655
58387
  hygienePlus: boolean().nullable(),
@@ -57664,7 +58396,7 @@ object({
57664
58396
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
57665
58397
  activeEvents: array(string()),
57666
58398
  /** Numeric error code when the appliance reports one; null otherwise. */
57667
- errorCode: number().nullable(),
58399
+ errorCode: number().nullable().meta(readingMeta("none")),
57668
58400
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
57669
58401
  errorLabel: string().nullable(),
57670
58402
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -57810,24 +58542,14 @@ object({
57810
58542
  /** Ms epoch of the last press. null = never observed since this provider started. */
57811
58543
  lastPressedAt: number().nullable(),
57812
58544
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
57813
- pressCountSinceStart: number()
58545
+ pressCountSinceStart: number().meta(readingMeta("count"))
57814
58546
  });
57815
58547
  object({
57816
58548
  deviceId: number(),
57817
58549
  timestamp: number()
57818
58550
  });
57819
58551
  DeviceType.Button, DeviceType.Camera;
57820
- /**
57821
- * Enum-state sensor — a string value picked from a finite option set.
57822
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
57823
- * states, weather conditions, contact-source channels, mode strings).
57824
- * The option set is stable across observations; it is persisted in the
57825
- * device config blob (via the control slice at adoption) and is NOT
57826
- * stored in `sourceInfo`.
57827
- */
57828
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
57829
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
57830
- var EnumSensorDateTimeFormatSchema = _enum([
58552
+ _enum([
57831
58553
  "date",
57832
58554
  "time",
57833
58555
  "datetime"
@@ -57835,12 +58557,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
57835
58557
  object({
57836
58558
  value: string(),
57837
58559
  /**
57838
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
57839
- * as a locale date/time/datetime; absent for plain enum sensors. HA
57840
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
57841
- * a time-only sensor → `'time'`.
58560
+ * Set when `value` is a timestamp rather than an enum state, so history
58561
+ * does not learn ISO strings as states. The display shape (date / time /
58562
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
57842
58563
  */
57843
- format: EnumSensorDateTimeFormatSchema.optional(),
58564
+ timestamp: boolean().optional(),
57844
58565
  /** Ms epoch when the slice was last updated. */
57845
58566
  lastFetchedAt: number()
57846
58567
  });
@@ -57859,13 +58580,13 @@ var EventFireSchema = object({
57859
58580
  deviceId: number(),
57860
58581
  eventType: string(),
57861
58582
  data: record(string(), unknown()).nullable(),
57862
- timestamp: number(),
57863
- seq: number()
58583
+ timestamp: number().meta(readingMeta("none")),
58584
+ seq: number().meta(readingMeta("count"))
57864
58585
  });
57865
58586
  object({
57866
58587
  eventTypes: array(string()),
57867
58588
  lastEvent: EventFireSchema.nullable(),
57868
- eventCountSinceStart: number()
58589
+ eventCountSinceStart: number().meta(readingMeta("count"))
57869
58590
  });
57870
58591
  DeviceType.EventEmitter;
57871
58592
  var IdentitySchema = object({
@@ -58254,10 +58975,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
58254
58975
  object({
58255
58976
  /** Active speed as 0..100 inclusive. Null when the device has no
58256
58977
  * speed surface (single-speed fan). */
58257
- percentage: number().min(0).max(100).nullable(),
58978
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58258
58979
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
58259
58980
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
58260
- percentageStep: number().positive().optional(),
58981
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
58261
58982
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
58262
58983
  * — empty string when no preset is active or supported. */
58263
58984
  preset: string(),
@@ -58339,7 +59060,7 @@ var FeatureProbeStatusSchema = object({
58339
59060
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
58340
59061
  model: string().nullable(),
58341
59062
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
58342
- channelCount: number().nullable(),
59063
+ channelCount: number().nullable().meta(readingMeta("count")),
58343
59064
  /**
58344
59065
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
58345
59066
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -58371,9 +59092,45 @@ object({
58371
59092
  lastChangedAt: number()
58372
59093
  });
58373
59094
  DeviceType.Sensor;
59095
+ object({
59096
+ /** One row per member that has been commanded. A later command replaces that member's row. */
59097
+ results: array(object({
59098
+ deviceId: number().int().nonnegative(),
59099
+ cap: string().min(1),
59100
+ method: string().min(1),
59101
+ ok: boolean(),
59102
+ error: string().optional(),
59103
+ /** Ms epoch of the attempt. */
59104
+ at: number()
59105
+ })) });
59106
+ DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
59107
+ object({
59108
+ active: boolean().meta({ label: "Active" }),
59109
+ /** Ms epoch when the slice was last updated. */
59110
+ lastChangedAt: number()
59111
+ });
59112
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Climate, DeviceType.Thermostat;
59113
+ object({
59114
+ /** Members whose current value entered the aggregate. */
59115
+ included: number().int().nonnegative().meta(readingMeta("count", {
59116
+ precision: 0,
59117
+ label: "Reporting"
59118
+ })),
59119
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
59120
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
59121
+ precision: 0,
59122
+ label: "Excluded"
59123
+ })),
59124
+ /** `included` + `excluded`. */
59125
+ total: number().int().nonnegative().meta(readingMeta("count", {
59126
+ precision: 0,
59127
+ label: "Members"
59128
+ }))
59129
+ });
59130
+ DeviceType.Sensor, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
58374
59131
  object({
58375
59132
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
58376
- position: number().min(0).max(100).nullable(),
59133
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58377
59134
  /** Ms epoch when the slice was last updated (push or poll). */
58378
59135
  lastFetchedAt: number()
58379
59136
  });
@@ -58382,9 +59139,9 @@ object({
58382
59139
  /** Whether the humidifier is currently on. */
58383
59140
  on: boolean(),
58384
59141
  /** Current measured relative humidity (0..100). Null when not reported. */
58385
- currentHumidity: number().min(0).max(100).nullable(),
59142
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58386
59143
  /** Target relative humidity (0..100). Null when no setpoint surface. */
58387
- targetHumidity: number().min(0).max(100).nullable(),
59144
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58388
59145
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
58389
59146
  * device has no mode surface. */
58390
59147
  mode: string().nullable(),
@@ -58394,9 +59151,9 @@ object({
58394
59151
  * `idle` / `off`). Null when the device doesn't report it. */
58395
59152
  action: string().nullable(),
58396
59153
  /** HA `min_humidity` attribute. Null → UI uses 0. */
58397
- minHumidity: number().nullable(),
59154
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
58398
59155
  /** HA `max_humidity` attribute. Null → UI uses 100. */
58399
- maxHumidity: number().nullable(),
59156
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
58400
59157
  /** Ms epoch when the slice was last updated. */
58401
59158
  lastChangedAt: number()
58402
59159
  });
@@ -58420,22 +59177,13 @@ DeviceType.Humidifier, method(object({
58420
59177
  auth: "protected"
58421
59178
  });
58422
59179
  object({
58423
- /** Current relative humidity, 0..100. */
58424
- percent: number().min(0).max(100).meta({
58425
- unit: "%",
59180
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
59181
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
59182
+ precision: 0,
58426
59183
  label: "Humidity"
58427
- }),
59184
+ })),
58428
59185
  /** Ms epoch when the slice was last updated. */
58429
- lastFetchedAt: number(),
58430
- /** Live display unit from the upstream source (e.g. HA
58431
- * `attributes.unit_of_measurement`). The UI prefers this over the
58432
- * role's canonical unit. Absent → fall back to the canonical unit. */
58433
- unit: string().optional(),
58434
- /** Suggested decimal places for numeric display.
58435
- * Populated live from the upstream source when provided (e.g. HA
58436
- * `attributes.suggested_display_precision`). Falls back to
58437
- * auto-formatting when absent. */
58438
- precision: number().int().min(0).max(10).optional()
59186
+ lastFetchedAt: number()
58439
59187
  });
58440
59188
  DeviceType.Sensor, DeviceType.Thermostat;
58441
59189
  object({
@@ -58443,7 +59191,7 @@ object({
58443
59191
  * entity exposes no `entity_picture` (yet). */
58444
59192
  url: string().nullable(),
58445
59193
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
58446
- lastUpdated: number().nullable()
59194
+ lastUpdated: number().nullable().meta(readingMeta("none"))
58447
59195
  });
58448
59196
  DeviceType.Image;
58449
59197
  /**
@@ -58499,19 +59247,19 @@ var NormalizedRangeSchema = object({
58499
59247
  });
58500
59248
  object({
58501
59249
  /** Normalized 0–100. */
58502
- brightness: number().optional(),
59250
+ brightness: number().optional().meta(readingMeta("none")),
58503
59251
  /** Normalized 0–100. */
58504
- contrast: number().optional(),
59252
+ contrast: number().optional().meta(readingMeta("none")),
58505
59253
  /** Normalized 0–100. */
58506
- saturation: number().optional(),
59254
+ saturation: number().optional().meta(readingMeta("none")),
58507
59255
  /** Normalized 0–100. */
58508
- sharpness: number().optional(),
59256
+ sharpness: number().optional().meta(readingMeta("none")),
58509
59257
  mirror: boolean().optional(),
58510
59258
  flip: boolean().optional(),
58511
59259
  rotate: ImageRotateSchema.optional(),
58512
59260
  whiteBalance: WhiteBalanceModeSchema.optional(),
58513
59261
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
58514
- warmth: number().optional(),
59262
+ warmth: number().optional().meta(readingMeta("none")),
58515
59263
  exposureMode: ExposureModeSchema.optional(),
58516
59264
  backlightMode: BacklightModeSchema.optional(),
58517
59265
  lastFetchedAt: number()
@@ -58756,10 +59504,10 @@ var IntercomAbilitySchema = object({
58756
59504
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
58757
59505
  codecs: array(string()),
58758
59506
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
58759
- sampleRate: number(),
59507
+ sampleRate: number().meta(readingMeta("none")),
58760
59508
  duplex: _enum(["half", "full"]),
58761
59509
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
58762
- maxBacklogMs: number()
59510
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
58763
59511
  });
58764
59512
  var IntercomStatusSchema = object({
58765
59513
  /** True while a talk session is open. */
@@ -58870,13 +59618,13 @@ object({
58870
59618
  activity: LawnMowerActivitySchema,
58871
59619
  /** 0..100 battery percentage. Null when the device has no battery
58872
59620
  * reading. */
58873
- batteryLevel: number().min(0).max(100).nullable(),
59621
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58874
59622
  /** 0..100 mowing-completion percentage of the current task, or null when no
58875
59623
  * task is active / progress is unavailable. */
58876
- progressPercent: number().min(0).max(100).nullable(),
59624
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58877
59625
  /** Current device/event code (dynamic — mostly status, sometimes an error),
58878
59626
  * or null when unknown. */
58879
- currentCode: number().nullable(),
59627
+ currentCode: number().nullable().meta(readingMeta("none")),
58880
59628
  /** Human label for {@link currentCode}, or null when undecodable. */
58881
59629
  currentCodeLabel: string().nullable(),
58882
59630
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -59286,7 +60034,14 @@ DeviceType.Lock, method(object({
59286
60034
  auth: "protected"
59287
60035
  }), method(object({
59288
60036
  deviceId: number().int().nonnegative(),
59289
- code: string().min(1).optional()
60037
+ code: string().min(1).optional(),
60038
+ /**
60039
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
60040
+ * with one call is refused unless the caller says it was confirmed. A
60041
+ * single lock ignores it. The refusal lives in the callee, so no
60042
+ * automation or API caller reaches the doors by skipping a dialog.
60043
+ */
60044
+ confirm: boolean().optional()
59290
60045
  }), _void(), {
59291
60046
  kind: "mutation",
59292
60047
  auth: "protected"
@@ -59294,6 +60049,11 @@ DeviceType.Lock, method(object({
59294
60049
  kind: "mutation",
59295
60050
  auth: "protected"
59296
60051
  });
60052
+ object({
60053
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
60054
+ lastFetchedAt: number()
60055
+ });
60056
+ DeviceType.Sensor;
59297
60057
  /**
59298
60058
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
59299
60059
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
@@ -59338,7 +60098,7 @@ object({
59338
60098
  state: MediaPlayerStateSchema,
59339
60099
  /** Volume as 0..100 inclusive. Null when the device has no volume
59340
60100
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
59341
- volumeLevel: number().min(0).max(100).nullable(),
60101
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59342
60102
  /** Mute toggle distinct from volume=0. Null when no mute surface.
59343
60103
  * Pair with `DeviceFeature.MediaPlayerMute`. */
59344
60104
  isMuted: boolean().nullable(),
@@ -59351,10 +60111,10 @@ object({
59351
60111
  currentMedia: MediaInfoSchema.nullable(),
59352
60112
  /** Current playback position in ms. Null when not seekable or
59353
60113
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
59354
- positionMs: number().int().nonnegative().nullable(),
60114
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59355
60115
  /** Total duration of the current media in ms. Null when unknown
59356
60116
  * (live stream) or nothing is playing. */
59357
- durationMs: number().int().nonnegative().nullable(),
60117
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59358
60118
  /** Shuffle toggle. Null when no shuffle surface.
59359
60119
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
59360
60120
  shuffle: boolean().nullable(),
@@ -59654,7 +60414,7 @@ object({
59654
60414
  * it, every provider seeded it at boot, and every session left it
59655
60415
  * `detected: true` after its detach.
59656
60416
  */
59657
- autoClearAfterMs: number().nullable()
60417
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
59658
60418
  });
59659
60419
  /**
59660
60420
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -59704,7 +60464,7 @@ var MotionZoneRegionSchema = object({
59704
60464
  });
59705
60465
  object({
59706
60466
  enabled: boolean(),
59707
- sensitivity: number(),
60467
+ sensitivity: number().meta(readingMeta("none")),
59708
60468
  /** Grid region(s). Today exactly one `grid` shape. */
59709
60469
  regions: array(MotionZoneRegionSchema),
59710
60470
  lastFetchedAt: number()
@@ -60008,7 +60768,7 @@ var NavigationStatusSchema = object({
60008
60768
  following: boolean(),
60009
60769
  flash: boolean(),
60010
60770
  lightMode: NavigationLightModeSchema,
60011
- lightLevel: number().min(40).max(100),
60771
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
60012
60772
  /** Ms epoch when the slice was last updated. */
60013
60773
  lastChangedAt: number()
60014
60774
  });
@@ -60054,13 +60814,13 @@ var NetworkLinkStatusSchema = object({
60054
60814
  * one whose reading has not landed must not be drawn at 0 %. Consumers
60055
60815
  * SKIP a null rather than coerce it.
60056
60816
  */
60057
- signalPercent: number().min(0).max(100).nullable(),
60817
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60058
60818
  /** Raw received signal strength in dBm, when the firmware reports one. */
60059
- rssiDbm: number().optional(),
60819
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
60060
60820
  /** Network name of a wireless link, when the firmware reports it. */
60061
60821
  ssid: string().optional(),
60062
60822
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
60063
- lastUpdated: number()
60823
+ lastUpdated: number().meta(readingMeta("none"))
60064
60824
  });
60065
60825
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
60066
60826
  deviceId: number(),
@@ -60379,7 +61139,7 @@ object({
60379
61139
  lastError: string().nullable(),
60380
61140
  /** Number of deliveries currently buffered server-side (rate-limited
60381
61141
  * notifiers may queue). 0 when the provider sends synchronously. */
60382
- queueDepth: number().int().nonnegative(),
61142
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
60383
61143
  /** Per-feature capability matrix — authoritative source for the
60384
61144
  * compose-form field gating. */
60385
61145
  supports: NotifierSupportsSchema
@@ -60429,15 +61189,8 @@ DeviceType.Notifier, method(NotifierSendInputSchema, NotifierSendResultSchema, {
60429
61189
  acceptedAt: number()
60430
61190
  });
60431
61191
  object({
60432
- value: number(),
60433
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
60434
- * Populated live from the upstream source on each state push.
60435
- * Absent for unitless sensors. */
60436
- unit: string().optional(),
60437
- /** Suggested decimal places for numeric display.
60438
- * Populated live from the upstream source when provided.
60439
- * Falls back to auto-formatting when absent. */
60440
- precision: number().int().min(0).max(10).optional(),
61192
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
61193
+ value: number().nullable().meta(readingMeta("device")),
60441
61194
  /** Ms epoch when the slice was last updated. */
60442
61195
  lastFetchedAt: number()
60443
61196
  });
@@ -60727,23 +61480,26 @@ object({
60727
61480
  /** Food currently in the bowl (grams). Null when the device has not
60728
61481
  * reported a reading yet. On dual-hopper models this is the combined
60729
61482
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
60730
- foodLevel: number().nullable(),
61483
+ foodLevel: number().nullable().meta(readingMeta("none")),
60731
61484
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
60732
61485
  * single-hopper models. */
60733
- food1: number().nullable(),
61486
+ food1: number().nullable().meta(readingMeta("none")),
60734
61487
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
60735
61488
  * single-hopper models. */
60736
- food2: number().nullable(),
61489
+ food2: number().nullable().meta(readingMeta("none")),
60737
61490
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
60738
61491
  * (`device_class: problem`, on = low). True when the bowl is empty /
60739
61492
  * below the feeder's low threshold. */
60740
61493
  lowFood: boolean(),
60741
61494
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
60742
61495
  * device has no battery reading. */
60743
- batteryPower: number().min(0).max(100).nullable(),
60744
- /** Days of desiccant life remaining. Null when the model has no
60745
- * desiccant sensor. */
60746
- desiccantLeftDays: number().nullable(),
61496
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61497
+ /**
61498
+ * Desiccant life remaining, stored in seconds (duration base, D944).
61499
+ * Providers convert from the days the feeder reports. Null when the
61500
+ * model has no desiccant sensor.
61501
+ */
61502
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
60747
61503
  /** True while a feed is in progress. */
60748
61504
  feeding: boolean(),
60749
61505
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -60754,7 +61510,7 @@ object({
60754
61510
  * with `errorCode` for consumers that want the raw integer. */
60755
61511
  error: string().nullable(),
60756
61512
  /** Raw device error code (0 / null = no error). */
60757
- errorCode: number().nullable(),
61513
+ errorCode: number().nullable().meta(readingMeta("none")),
60758
61514
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
60759
61515
  isDualHopper: boolean(),
60760
61516
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -60764,7 +61520,7 @@ object({
60764
61520
  /** Play a chime when dispensing. */
60765
61521
  feedSound: boolean(),
60766
61522
  /** Speaker / prompt volume level (device-scaled integer). */
60767
- volume: number(),
61523
+ volume: number().meta(readingMeta("none")),
60768
61524
  /** Ms epoch when the slice was last refreshed from the cloud. */
60769
61525
  lastFetchedAt: number()
60770
61526
  });
@@ -61201,38 +61957,37 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
61201
61957
  });
61202
61958
  object({
61203
61959
  /** Instantaneous power draw in watts. */
61204
- watts: number().optional().meta({
61205
- unit: "W",
61206
- label: "Power"
61207
- }),
61960
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
61208
61961
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
61209
- kwhTotal: number().optional().meta({
61210
- unit: "kWh",
61211
- label: "Energy"
61212
- }),
61962
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
61213
61963
  /** Voltage in volts. */
61214
- volts: number().optional(),
61964
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
61215
61965
  /** Current in amperes. */
61216
- amps: number().optional(),
61217
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
61218
- va: number().optional(),
61966
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
61967
+ /** Apparent power in volt-amperes. */
61968
+ va: number().optional().meta(readingMeta("apparent-power")),
61219
61969
  /** Power factor (0..1) if reported. */
61220
- powerFactor: number().min(0).max(1).optional(),
61970
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
61221
61971
  /** Ms epoch when the slice was last updated. */
61222
- lastFetchedAt: number(),
61223
- /** Live display unit of the single metric this slice carries (e.g. HA
61224
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
61225
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
61226
- * unit per slice is unambiguous. The UI prefers this over the role's
61227
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
61228
- unit: string().optional(),
61229
- /** Suggested decimal places for numeric display.
61230
- * Populated live from the upstream source when provided (e.g. HA
61231
- * `attributes.suggested_display_precision`). Falls back to
61232
- * auto-formatting when absent. */
61233
- precision: number().int().min(0).max(10).optional()
61972
+ lastFetchedAt: number()
61234
61973
  });
61235
- DeviceType.Sensor;
61974
+ [
61975
+ DeviceType.Sensor,
61976
+ DeviceType.Switch,
61977
+ DeviceType.Light,
61978
+ DeviceType.Fan,
61979
+ DeviceType.Climate,
61980
+ DeviceType.Thermostat,
61981
+ DeviceType.Humidifier,
61982
+ DeviceType.WaterHeater,
61983
+ DeviceType.MediaPlayer,
61984
+ DeviceType.Washer,
61985
+ DeviceType.Dryer,
61986
+ DeviceType.Dishwasher,
61987
+ DeviceType.Vacuum,
61988
+ DeviceType.LawnMower,
61989
+ DeviceType.PetFeeder
61990
+ ].filter((type) => type !== DeviceType.Sensor);
61236
61991
  /**
61237
61992
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
61238
61993
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -61251,11 +62006,11 @@ DeviceType.Sensor;
61251
62006
  */
61252
62007
  var GpsLocationSchema = object({
61253
62008
  /** Latitude in decimal degrees, -90..90. */
61254
- latitude: number().min(-90).max(90),
62009
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
61255
62010
  /** Longitude in decimal degrees, -180..180. */
61256
- longitude: number().min(-180).max(180),
62011
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
61257
62012
  /** Reported accuracy in meters (lower = better). */
61258
- accuracyMeters: number().nonnegative()
62013
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
61259
62014
  });
61260
62015
  object({
61261
62016
  /** `home` / `not_home` / any user-defined zone name. */
@@ -61267,25 +62022,16 @@ object({
61267
62022
  gps: GpsLocationSchema.nullable(),
61268
62023
  /** Optional battery level of the tracking device (0..100). Null
61269
62024
  * when not reported. */
61270
- batteryPercent: number().min(0).max(100).nullable(),
62025
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61271
62026
  /** Ms epoch when the slice was last updated. */
61272
62027
  lastChangedAt: number()
61273
62028
  });
61274
62029
  DeviceType.Presence;
61275
62030
  object({
61276
- /** Current pressure in hPa. */
61277
- hpa: number(),
62031
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
62032
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
61278
62033
  /** Ms epoch when the slice was last updated. */
61279
- lastFetchedAt: number(),
61280
- /** Live display unit from the upstream source (e.g. HA
61281
- * `attributes.unit_of_measurement`). The UI prefers this over the
61282
- * role's canonical unit. Absent → fall back to the canonical unit. */
61283
- unit: string().optional(),
61284
- /** Suggested decimal places for numeric display.
61285
- * Populated live from the upstream source when provided (e.g. HA
61286
- * `attributes.suggested_display_precision`). Falls back to
61287
- * auto-formatting when absent. */
61288
- precision: number().int().min(0).max(10).optional()
62034
+ lastFetchedAt: number()
61289
62035
  });
61290
62036
  DeviceType.Sensor;
61291
62037
  /**
@@ -61618,8 +62364,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
61618
62364
  }), _void(), { kind: "mutation" });
61619
62365
  var PtzAutotrackSettingsSchema = object({
61620
62366
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
61621
- stopDelaySeconds: number().int().min(0).max(300),
61622
- disappearDelaySeconds: number().int().min(0).max(300)
62367
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
62368
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
61623
62369
  });
61624
62370
  /**
61625
62371
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -61676,7 +62422,7 @@ object({
61676
62422
  /** null = unknown — never coerced to false (D393). */
61677
62423
  running: boolean().nullable(),
61678
62424
  /** Epoch ms the current boolean began; null while `running` is null. */
61679
- since: number().nullable(),
62425
+ since: number().nullable().meta(readingMeta("none")),
61680
62426
  source: RunningStateSourceSchema.meta({ label: "Source" }),
61681
62427
  phase: string().nullable(),
61682
62428
  /** null until a reading has been published (D393). */
@@ -61688,7 +62434,7 @@ object({
61688
62434
  phase: string().nullable()
61689
62435
  });
61690
62436
  Object.values(DeviceType);
61691
- DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
62437
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
61692
62438
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
61693
62439
  kind: "mutation",
61694
62440
  auth: "admin"
@@ -62874,9 +63620,9 @@ var RecordTriggerSchema = _enum([
62874
63620
  */
62875
63621
  var RecordWindowSchema = object({
62876
63622
  trigger: RecordTriggerSchema,
62877
- day: number().int().min(0).max(6),
62878
- startMinute: number().int().min(0).max(1439),
62879
- endMinute: number().int().min(1).max(1440)
63623
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
63624
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
63625
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
62880
63626
  });
62881
63627
  /** Status of one physical volume, as the camera itself describes it. */
62882
63628
  var OnboardStorageVolumeSchema = object({
@@ -62945,10 +63691,10 @@ object({
62945
63691
  /** Master "record to the card at all" switch. */
62946
63692
  enabled: boolean().nullable(),
62947
63693
  overwriteWhenFull: boolean().nullable(),
62948
- preRecordSec: number().nullable(),
62949
- postRecordSec: number().nullable(),
63694
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
63695
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
62950
63696
  /** Length of one recorded file, in minutes. */
62951
- segmentMinutes: number().nullable(),
63697
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
62952
63698
  /** The primary track's weekly windows, flattened. */
62953
63699
  windows: array(RecordWindowSchema),
62954
63700
  /**
@@ -62964,7 +63710,7 @@ object({
62964
63710
  * provider reporting a non-zero count also reports the schedule as not
62965
63711
  * writable.
62966
63712
  */
62967
- unreadableWindows: number(),
63713
+ unreadableWindows: number().meta(readingMeta("count")),
62968
63714
  /**
62969
63715
  * The camera is scheduled to record and has NO usable storage.
62970
63716
  *
@@ -63601,7 +64347,7 @@ var CamStreamDescriptorSchema = object({
63601
64347
  url: string().optional(),
63602
64348
  codec: string().optional(),
63603
64349
  resolution: CamStreamResolutionSchema.optional(),
63604
- fps: number().positive().optional(),
64350
+ fps: number().positive().optional().meta(readingMeta("rate")),
63605
64351
  label: string().optional(),
63606
64352
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
63607
64353
  deviceFeatures: array(string()).optional(),
@@ -63631,18 +64377,18 @@ var StreamProfileSchema = _enum([
63631
64377
  ]);
63632
64378
  /** The current encoding config of one stream profile. */
63633
64379
  var StreamProfileConfigSchema = object({
63634
- width: number(),
63635
- height: number(),
64380
+ width: number().meta(readingMeta("none")),
64381
+ height: number().meta(readingMeta("none")),
63636
64382
  codec: _enum(["h264", "h265"]),
63637
- framerate: number(),
63638
- bitrate: number(),
64383
+ framerate: number().meta(readingMeta("rate")),
64384
+ bitrate: number().meta(readingMeta("bitrate")),
63639
64385
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
63640
64386
  encoderProfile: _enum([
63641
64387
  "high",
63642
64388
  "main",
63643
64389
  "baseline"
63644
64390
  ]).optional(),
63645
- gop: number().optional(),
64391
+ gop: number().optional().meta(readingMeta("none")),
63646
64392
  /**
63647
64393
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
63648
64394
  *
@@ -63727,7 +64473,7 @@ object({
63727
64473
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
63728
64474
  lastChangedAt: number()
63729
64475
  });
63730
- DeviceType.Switch, DeviceType.Siren, DeviceType.Light, method(object({
64476
+ DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, method(object({
63731
64477
  deviceId: number(),
63732
64478
  on: boolean()
63733
64479
  }), _void(), {
@@ -64285,23 +65031,13 @@ object({
64285
65031
  });
64286
65032
  DeviceType.Sensor;
64287
65033
  object({
64288
- /** Current temperature in Celsius. */
64289
- celsius: number().meta({
64290
- unit: "°C",
65034
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
65035
+ celsius: number().nullable().meta(readingMeta("temperature", {
65036
+ precision: 1,
64291
65037
  label: "Temperature"
64292
- }),
65038
+ })),
64293
65039
  /** Ms epoch when the slice was last updated (push or poll). */
64294
- lastFetchedAt: number(),
64295
- /** Live display unit from the upstream source (e.g. HA
64296
- * `attributes.unit_of_measurement`). The UI prefers this over the
64297
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
64298
- * Absent → the UI falls back to the role's canonical unit. */
64299
- unit: string().optional(),
64300
- /** Suggested decimal places for numeric display.
64301
- * Populated live from the upstream source when provided (e.g. HA
64302
- * `attributes.suggested_display_precision`). Falls back to
64303
- * auto-formatting when absent. */
64304
- precision: number().int().min(0).max(10).optional()
65040
+ lastFetchedAt: number()
64305
65041
  });
64306
65042
  DeviceType.Sensor, DeviceType.Thermostat;
64307
65043
  /**
@@ -64553,16 +65289,16 @@ object({
64553
65289
  ]),
64554
65290
  /** 0..100 battery percentage. Null when the device has no battery
64555
65291
  * reading. */
64556
- batteryLevel: number().min(0).max(100).nullable(),
65292
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64557
65293
  /** Current fan-speed token (provider-verbatim). Null when unknown or
64558
65294
  * the vacuum has no speed control. */
64559
65295
  fanSpeed: string().nullable(),
64560
65296
  /** Speed tokens the hardware accepts — drives the UI selector. */
64561
65297
  availableFanSpeeds: array(string()),
64562
65298
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64563
- progressPercent: number().min(0).max(100).nullable(),
65299
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64564
65300
  /** Current error code (0 / null = no error). */
64565
- errorCode: number().nullable(),
65301
+ errorCode: number().nullable().meta(readingMeta("none")),
64566
65302
  /** Human label for {@link errorCode}, or null when none / undecodable. */
64567
65303
  errorLabel: string().nullable(),
64568
65304
  /** Ms epoch when the slice was last updated. */
@@ -64601,7 +65337,7 @@ object({
64601
65337
  ]),
64602
65338
  /** 0 = fully closed, 100 = fully open. Null when the device has no
64603
65339
  * intermediate position surface. */
64604
- position: number().min(0).max(100).nullable(),
65340
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64605
65341
  /** Ms epoch when the slice was last updated. */
64606
65342
  lastChangedAt: number()
64607
65343
  });
@@ -64629,9 +65365,9 @@ object({
64629
65365
  DeviceType.Sensor;
64630
65366
  object({
64631
65367
  /** Current measured temperature. Null when not reported. */
64632
- currentTemp: number().nullable(),
65368
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
64633
65369
  /** Target temperature setpoint. Null when no setpoint surface. */
64634
- targetTemp: number().nullable(),
65370
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
64635
65371
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
64636
65372
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
64637
65373
  * device reports an unknown state. */
@@ -64642,9 +65378,9 @@ object({
64642
65378
  * has no away surface. */
64643
65379
  away: boolean().nullable(),
64644
65380
  /** HA `min_temp` attribute. Null when not reported. */
64645
- minTemp: number().nullable(),
65381
+ minTemp: number().nullable().meta(readingMeta("temperature")),
64646
65382
  /** HA `max_temp` attribute. Null when not reported. */
64647
- maxTemp: number().nullable(),
65383
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
64648
65384
  /** Ms epoch when the slice was last updated. */
64649
65385
  lastChangedAt: number()
64650
65386
  });
@@ -64668,25 +65404,18 @@ DeviceType.WaterHeater, method(object({
64668
65404
  auth: "protected"
64669
65405
  });
64670
65406
  object({
64671
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
64672
- * when no condition has been reported yet. */
65407
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
64673
65408
  condition: string().nullable(),
64674
- /** Current temperature in the reported unit. Null when not provided. */
64675
- temperature: number().nullable(),
64676
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
64677
- temperatureUnit: string().nullable(),
65409
+ /** Current temperature in Celsius. Null when not provided or refused. */
65410
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
64678
65411
  /** Relative humidity (0..100). Null when not provided. */
64679
- humidity: number().min(0).max(100).nullable(),
64680
- /** Barometric pressure in the reported unit. Null when not provided. */
64681
- pressure: number().nullable(),
64682
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
64683
- pressureUnit: string().nullable(),
64684
- /** Wind speed in the reported unit. Null when not provided. */
64685
- windSpeed: number().nullable(),
64686
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
64687
- windSpeedUnit: string().nullable(),
65412
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
65413
+ /** Barometric pressure in hPa. Null when not provided or refused. */
65414
+ pressure: number().nullable().meta(readingMeta("pressure")),
65415
+ /** Wind speed in metres per second. Null when not provided or refused. */
65416
+ windSpeed: number().nullable().meta(readingMeta("speed")),
64688
65417
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
64689
- windBearing: number().nullable(),
65418
+ windBearing: number().nullable().meta(readingMeta("angle")),
64690
65419
  /** Ms epoch when the slice was last updated. */
64691
65420
  lastFetchedAt: number()
64692
65421
  });
@@ -65436,8 +66165,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
65436
66165
  kind: "mutation",
65437
66166
  auth: "admin"
65438
66167
  });
66168
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
65439
66169
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
65440
- 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;
66170
+ 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;
65441
66171
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
65442
66172
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
65443
66173
  new Set(CAP_NAMES);
@@ -65490,7 +66220,7 @@ var AccessoryKind = {
65490
66220
  PrivacyMask: DeviceRole.PrivacyMask
65491
66221
  };
65492
66222
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
65493
- 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;
66223
+ 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;
65494
66224
  new Set(Object.values(DeviceType));
65495
66225
  Object.freeze({
65496
66226
  "accessories.getStatus": {
@@ -65598,6 +66328,13 @@ Object.freeze({
65598
66328
  access: "view",
65599
66329
  role: "admin"
65600
66330
  },
66331
+ "addons.getPackageDocs": {
66332
+ capName: "addons",
66333
+ capScope: "system",
66334
+ addonId: null,
66335
+ access: "view",
66336
+ role: "admin"
66337
+ },
65601
66338
  "addons.getVersions": {
65602
66339
  capName: "addons",
65603
66340
  capScope: "system",
@@ -68706,6 +69443,27 @@ Object.freeze({
68706
69443
  access: "view",
68707
69444
  role: "viewer"
68708
69445
  },
69446
+ "groupActive.getStatus": {
69447
+ capName: "group-active",
69448
+ capScope: "device",
69449
+ addonId: null,
69450
+ access: "view",
69451
+ role: "viewer"
69452
+ },
69453
+ "groupCommands.getStatus": {
69454
+ capName: "group-commands",
69455
+ capScope: "device",
69456
+ addonId: null,
69457
+ access: "view",
69458
+ role: "viewer"
69459
+ },
69460
+ "groupStatus.getStatus": {
69461
+ capName: "group-status",
69462
+ capScope: "device",
69463
+ addonId: null,
69464
+ access: "view",
69465
+ role: "viewer"
69466
+ },
68709
69467
  "heatingValve.getStatus": {
68710
69468
  capName: "heating-valve",
68711
69469
  capScope: "device",
@@ -69658,6 +70416,13 @@ Object.freeze({
69658
70416
  access: "create",
69659
70417
  role: "admin"
69660
70418
  },
70419
+ "meter.getStatus": {
70420
+ capName: "meter",
70421
+ capScope: "device",
70422
+ addonId: null,
70423
+ access: "view",
70424
+ role: "viewer"
70425
+ },
69661
70426
  "metricsProvider.collectSnapshot": {
69662
70427
  capName: "metrics-provider",
69663
70428
  capScope: "system",
@@ -77756,7 +78521,39 @@ var APPLIANCE_CAP_VOCABULARY = {
77756
78521
  phrase: t("When {device} finishes", "Quando {device} finisce"),
77757
78522
  icon: "square"
77758
78523
  }
77759
- }
78524
+ },
78525
+ overrides: { byType: {
78526
+ [DeviceType.Washer]: {
78527
+ state: { running: {
78528
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
78529
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
78530
+ } },
78531
+ events: {
78532
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
78533
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
78534
+ }
78535
+ },
78536
+ [DeviceType.Dryer]: {
78537
+ state: { running: {
78538
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
78539
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
78540
+ } },
78541
+ events: {
78542
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
78543
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
78544
+ }
78545
+ },
78546
+ [DeviceType.Dishwasher]: {
78547
+ state: { running: {
78548
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
78549
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
78550
+ } },
78551
+ events: {
78552
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
78553
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
78554
+ }
78555
+ }
78556
+ } }
77760
78557
  },
77761
78558
  update: {
77762
78559
  scope: "device",
@@ -78528,6 +79325,41 @@ var SENSOR_CAP_VOCABULARY = {
78528
79325
  icon: "flame"
78529
79326
  }]
78530
79327
  },
79328
+ "group-active": {
79329
+ scope: "device",
79330
+ noun: t("Group state", "Stato del gruppo")
79331
+ },
79332
+ "group-commands": {
79333
+ scope: "device",
79334
+ noun: t("Group command", "Comando di gruppo")
79335
+ },
79336
+ "group-status": {
79337
+ scope: "device",
79338
+ noun: t("Group", "Gruppo"),
79339
+ state: [
79340
+ {
79341
+ kind: "numeric",
79342
+ field: "included",
79343
+ quantity: t("the members reporting", "i membri che rispondono"),
79344
+ label: t("Reporting", "Rispondono"),
79345
+ icon: "users"
79346
+ },
79347
+ {
79348
+ kind: "numeric",
79349
+ field: "excluded",
79350
+ quantity: t("the members left out", "i membri esclusi"),
79351
+ label: t("Excluded", "Esclusi"),
79352
+ icon: "users"
79353
+ },
79354
+ {
79355
+ kind: "numeric",
79356
+ field: "total",
79357
+ quantity: t("the member count", "il numero di membri"),
79358
+ label: t("Members", "Membri"),
79359
+ icon: "sigma"
79360
+ }
79361
+ ]
79362
+ },
78531
79363
  "humidity-sensor": {
78532
79364
  scope: "device",
78533
79365
  noun: t("Humidity", "Umidità"),
@@ -78553,6 +79385,17 @@ var SENSOR_CAP_VOCABULARY = {
78553
79385
  }],
78554
79386
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
78555
79387
  },
79388
+ meter: {
79389
+ scope: "device",
79390
+ noun: t("Meter", "Contatore"),
79391
+ state: [{
79392
+ kind: "numeric",
79393
+ field: "value",
79394
+ quantity: t("the total", "il totale"),
79395
+ label: t("Total", "Totale"),
79396
+ icon: "gauge"
79397
+ }]
79398
+ },
78556
79399
  "numeric-sensor": {
78557
79400
  scope: "device",
78558
79401
  noun: t("Sensor", "Sensore"),
@@ -78561,7 +79404,6 @@ var SENSOR_CAP_VOCABULARY = {
78561
79404
  field: "value",
78562
79405
  quantity: t("the reading", "il valore"),
78563
79406
  label: t("Reading", "Valore"),
78564
- unitField: "unit",
78565
79407
  icon: "gauge"
78566
79408
  }]
78567
79409
  },
@@ -78697,7 +79539,7 @@ var SENSOR_CAP_VOCABULARY = {
78697
79539
  field: "temperature",
78698
79540
  quantity: t("the outdoor temperature", "la temperatura esterna"),
78699
79541
  label: t("Temperature", "Temperatura"),
78700
- unitField: "temperatureUnit",
79542
+ unit: "°C",
78701
79543
  icon: "thermometer"
78702
79544
  },
78703
79545
  {
@@ -78713,7 +79555,7 @@ var SENSOR_CAP_VOCABULARY = {
78713
79555
  field: "windSpeed",
78714
79556
  quantity: t("the wind speed", "la velocità del vento"),
78715
79557
  label: t("Wind", "Vento"),
78716
- unitField: "windSpeedUnit",
79558
+ unit: "m/s",
78717
79559
  icon: "wind"
78718
79560
  },
78719
79561
  {
@@ -78721,7 +79563,7 @@ var SENSOR_CAP_VOCABULARY = {
78721
79563
  field: "pressure",
78722
79564
  quantity: t("the air pressure", "la pressione atmosferica"),
78723
79565
  label: t("Pressure", "Pressione"),
78724
- unitField: "pressureUnit",
79566
+ unit: "hPa",
78725
79567
  icon: "gauge"
78726
79568
  }
78727
79569
  ]
@@ -79026,8 +79868,47 @@ function projectAction(cap, action) {
79026
79868
  };
79027
79869
  }
79028
79870
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
79029
- 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;
79871
+ 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;
79030
79872
  DeviceFeature.BatteryOperated;
79873
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
79874
+ 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.";
79875
+ 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.";
79876
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
79877
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
79878
+ 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.";
79879
+ function withheld(reason) {
79880
+ return {
79881
+ [DeviceType.Washer]: {
79882
+ exported: false,
79883
+ reason
79884
+ },
79885
+ [DeviceType.Dryer]: {
79886
+ exported: false,
79887
+ reason
79888
+ },
79889
+ [DeviceType.Dishwasher]: {
79890
+ exported: false,
79891
+ reason
79892
+ }
79893
+ };
79894
+ }
79895
+ function offered(received) {
79896
+ return {
79897
+ [DeviceType.Washer]: {
79898
+ exported: true,
79899
+ as: received
79900
+ },
79901
+ [DeviceType.Dryer]: {
79902
+ exported: true,
79903
+ as: received
79904
+ },
79905
+ [DeviceType.Dishwasher]: {
79906
+ exported: true,
79907
+ as: received
79908
+ }
79909
+ };
79910
+ }
79911
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
79031
79912
  Object.freeze({
79032
79913
  "accessories.getStatus": [{
79033
79914
  name: "deviceId",
@@ -79994,6 +80875,21 @@ Object.freeze({
79994
80875
  form: "single",
79995
80876
  optional: false
79996
80877
  }],
80878
+ "groupActive.getStatus": [{
80879
+ name: "deviceId",
80880
+ form: "single",
80881
+ optional: false
80882
+ }],
80883
+ "groupCommands.getStatus": [{
80884
+ name: "deviceId",
80885
+ form: "single",
80886
+ optional: false
80887
+ }],
80888
+ "groupStatus.getStatus": [{
80889
+ name: "deviceId",
80890
+ form: "single",
80891
+ optional: false
80892
+ }],
79997
80893
  "heatingValve.getStatus": [{
79998
80894
  name: "deviceId",
79999
80895
  form: "single",
@@ -80194,6 +81090,11 @@ Object.freeze({
80194
81090
  form: "single",
80195
81091
  optional: false
80196
81092
  }],
81093
+ "meter.getStatus": [{
81094
+ name: "deviceId",
81095
+ form: "single",
81096
+ optional: false
81097
+ }],
80197
81098
  "motion.getStatus": [{
80198
81099
  name: "deviceId",
80199
81100
  form: "single",
@@ -81801,7 +82702,6 @@ Object.freeze({
81801
82702
  "network-access": "ingress",
81802
82703
  "smtp-provider": "email"
81803
82704
  });
81804
- DeviceType.Camera;
81805
82705
  /**
81806
82706
  * A saved Customize assignment (D936). The schema is the type: callers use
81807
82707
  * `z.infer`, they do not restate the shape.
@@ -81978,6 +82878,8 @@ new Map(Object.entries({
81978
82878
  "detection-blind": "device-events",
81979
82879
  "motion-noisy": "device-events",
81980
82880
  "motion-calm": "device-events",
82881
+ "package-llm-silent": "device-events",
82882
+ "package-llm-answered": "device-events",
81981
82883
  "recording-stalled": "device-events",
81982
82884
  "recording-healthy": "device-events",
81983
82885
  "recording-degraded": "device-events",
@@ -82061,6 +82963,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
82061
82963
  label: "Motion without a subject",
82062
82964
  kinds: ["motion-noisy", "motion-calm"]
82063
82965
  },
82966
+ {
82967
+ id: "package-llm",
82968
+ family: "device-events",
82969
+ label: "Package confirm",
82970
+ kinds: ["package-llm-silent", "package-llm-answered"]
82971
+ },
82064
82972
  {
82065
82973
  id: "recording",
82066
82974
  family: "device-events",
@@ -82242,6 +83150,8 @@ var DEVICE_EVENTS = [
82242
83150
  "detection-blind",
82243
83151
  "motion-noisy",
82244
83152
  "motion-calm",
83153
+ "package-llm-silent",
83154
+ "package-llm-answered",
82245
83155
  "recording-stalled",
82246
83156
  "recording-healthy",
82247
83157
  "recording-degraded",
@@ -82510,6 +83420,15 @@ new Map([
82510
83420
  snapshotLabel: "Cameras with constant motion now",
82511
83421
  snapshotSection: null
82512
83422
  },
83423
+ {
83424
+ id: "package-llm-confirm",
83425
+ family: "device-events",
83426
+ label: "Package confirm (not answering / answering again)",
83427
+ bad: ["package-llm-silent"],
83428
+ recovery: ["package-llm-answered"],
83429
+ snapshotLabel: "Cameras whose package confirm is not answering",
83430
+ snapshotSection: null
83431
+ },
82513
83432
  {
82514
83433
  id: "recording-health",
82515
83434
  family: "device-events",
@@ -84113,7 +85032,7 @@ function pickOauthRedirectOrigin(book) {
84113
85032
  return book.roles.publicEndpoint.resolved ?? book.roles.internal.resolved ?? null;
84114
85033
  }
84115
85034
  var googleBrokersEntryKind = defineEntryKind("brokers", {
84116
- title: "Google Services brokers",
85035
+ title: "Google brokers",
84117
85036
  schema: entryBody({
84118
85037
  clientId: GoogleBrokerEntrySchema.shape.clientId,
84119
85038
  clientSecret: GoogleBrokerEntrySchema.shape.clientSecret,
@@ -84182,7 +85101,7 @@ var GoogleBrokerStore = class GoogleBrokerStore {
84182
85101
  //#endregion
84183
85102
  //#region src/google-services.addon.ts
84184
85103
  /**
84185
- * Google Services — hub-only broker kind `google` + calendar-source + Gmail smtp.
85104
+ * Google — hub-only broker kind `google` + calendar-source + Gmail smtp.
84186
85105
  * One OAuth account = one broker entry; feature caps aggregate connected brokers.
84187
85106
  */
84188
85107
  var ADDON_ID = "google-services";