@camstack/addon-smtp-nodemailer 1.2.141 → 1.2.143

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.
@@ -36,7 +36,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
36
36
  }) : target, mod));
37
37
  var __toCommonJS = (mod) => __hasOwnProp.call(mod, "module.exports") ? mod["module.exports"] : __copyProps(__defProp({}, "__esModule", { value: true }), mod);
38
38
  //#endregion
39
- //#region ../types/dist/event-category-DHXxB8t9.mjs
39
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
40
40
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
41
41
  EventCategory["SystemBoot"] = "system.boot";
42
42
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -362,6 +362,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
362
362
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
363
363
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
364
364
  /**
365
+ * A camera's package LLM confirm has failed closed three times in a row
366
+ * (D931, 2026-10-26). One edge per camera until
367
+ * {@link PipelinePackageLlmAnswered}.
368
+ */
369
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
370
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
371
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
372
+ /**
365
373
  * Per-camera pipeline config was mutated by the orchestrator
366
374
  * (a settings change via `setAgentInferenceDevices` /
367
375
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5471,7 +5479,7 @@ var ZodIssueCode = {
5471
5479
  var ZodFirstPartyTypeKind;
5472
5480
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5473
5481
  //#endregion
5474
- //#region ../types/dist/sleep-BErAxhul.mjs
5482
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5475
5483
  /**
5476
5484
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5477
5485
  * window to float samples (D455).
@@ -7958,6 +7966,99 @@ var RecordKindDeclarationSchema = object({
7958
7966
  }).strict();
7959
7967
  /** The kind names: lower-case, dash separated, short enough for an index name. */
7960
7968
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7969
+ var READING_QUANTITIES = [
7970
+ "temperature",
7971
+ "pressure",
7972
+ "speed",
7973
+ "precipitation",
7974
+ "precipitation-rate",
7975
+ "length",
7976
+ "area",
7977
+ "illuminance",
7978
+ "irradiance",
7979
+ "power",
7980
+ "energy",
7981
+ "voltage",
7982
+ "current",
7983
+ "apparent-power",
7984
+ "humidity",
7985
+ "percentage",
7986
+ "concentration-volume",
7987
+ "concentration-mass",
7988
+ "concentration-ppb",
7989
+ "volume",
7990
+ "duration",
7991
+ "duration-ms",
7992
+ "information",
7993
+ "sound-level",
7994
+ "signal-level",
7995
+ "colour-temperature",
7996
+ "angle",
7997
+ "bitrate",
7998
+ "rate",
7999
+ "mass",
8000
+ "index",
8001
+ "ratio",
8002
+ "count",
8003
+ "none",
8004
+ "device"
8005
+ ];
8006
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8007
+ new Set(READING_QUANTITIES);
8008
+ /**
8009
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8010
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8011
+ */
8012
+ var QUANTITY_BASE = {
8013
+ temperature: "°C",
8014
+ pressure: "hPa",
8015
+ speed: "m/s",
8016
+ precipitation: "mm",
8017
+ "precipitation-rate": "mm/h",
8018
+ length: "m",
8019
+ area: "m²",
8020
+ illuminance: "lx",
8021
+ irradiance: "W/m²",
8022
+ power: "W",
8023
+ energy: "kWh",
8024
+ voltage: "V",
8025
+ current: "A",
8026
+ "apparent-power": "VA",
8027
+ humidity: "%",
8028
+ percentage: "%",
8029
+ "concentration-volume": "ppm",
8030
+ "concentration-mass": "µg/m³",
8031
+ "concentration-ppb": "ppb",
8032
+ volume: "L",
8033
+ duration: "s",
8034
+ "duration-ms": "ms",
8035
+ information: "B",
8036
+ "sound-level": "dBFS",
8037
+ "signal-level": "dBm",
8038
+ "colour-temperature": "mired",
8039
+ angle: "°",
8040
+ bitrate: "kbps",
8041
+ rate: "fps",
8042
+ mass: "kg",
8043
+ index: "none",
8044
+ ratio: "none",
8045
+ count: "none",
8046
+ none: "none",
8047
+ device: "device"
8048
+ };
8049
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8050
+ function baseUnitOf(quantity) {
8051
+ const symbol = QUANTITY_BASE[quantity];
8052
+ if (symbol === "none" || symbol === "device") return void 0;
8053
+ return symbol;
8054
+ }
8055
+ function readingMeta(quantity, extras) {
8056
+ return {
8057
+ quantity,
8058
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8059
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8060
+ };
8061
+ }
7961
8062
  /** Shared Zod schemas used across streaming capabilities. */
7962
8063
  var CamProfileSchema = _enum([
7963
8064
  "high",
@@ -7985,8 +8086,8 @@ var CamStreamKindSchema = _enum([
7985
8086
  "derived"
7986
8087
  ]);
7987
8088
  var CamStreamResolutionSchema = object({
7988
- width: number().int().positive(),
7989
- height: number().int().positive()
8089
+ width: number().int().positive().meta(readingMeta("none")),
8090
+ height: number().int().positive().meta(readingMeta("none"))
7990
8091
  });
7991
8092
  var CameraStreamSchema = object({
7992
8093
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8500,6 +8601,9 @@ var CAP_NAMES = Object.freeze([
8500
8601
  "filesystem-browse",
8501
8602
  "flood",
8502
8603
  "gas",
8604
+ "group-active",
8605
+ "group-commands",
8606
+ "group-status",
8503
8607
  "heating-valve",
8504
8608
  "humidifier",
8505
8609
  "humidity-sensor",
@@ -8520,6 +8624,7 @@ var CAP_NAMES = Object.freeze([
8520
8624
  "login-method",
8521
8625
  "media-player",
8522
8626
  "mesh-network",
8627
+ "meter",
8523
8628
  "metrics-provider",
8524
8629
  "model-convert",
8525
8630
  "model-distributor",
@@ -8629,14 +8734,6 @@ Object.freeze({
8629
8734
  {
8630
8735
  "path": "aqi",
8631
8736
  "valueType": "number"
8632
- },
8633
- {
8634
- "path": "unit",
8635
- "valueType": "string"
8636
- },
8637
- {
8638
- "path": "precision",
8639
- "valueType": "number"
8640
8737
  }
8641
8738
  ],
8642
8739
  "alarm-panel": [
@@ -8672,20 +8769,10 @@ Object.freeze({
8672
8769
  "valueType": "boolean"
8673
8770
  }
8674
8771
  ],
8675
- "ambient-light-sensor": [
8676
- {
8677
- "path": "lux",
8678
- "valueType": "number"
8679
- },
8680
- {
8681
- "path": "unit",
8682
- "valueType": "string"
8683
- },
8684
- {
8685
- "path": "precision",
8686
- "valueType": "number"
8687
- }
8688
- ],
8772
+ "ambient-light-sensor": [{
8773
+ "path": "lux",
8774
+ "valueType": "number"
8775
+ }],
8689
8776
  "audio-metrics": [
8690
8777
  {
8691
8778
  "path": "ts",
@@ -8765,6 +8852,10 @@ Object.freeze({
8765
8852
  "path": "lastUpdated",
8766
8853
  "valueType": "number"
8767
8854
  },
8855
+ {
8856
+ "path": "volts",
8857
+ "valueType": "number"
8858
+ },
8768
8859
  {
8769
8860
  "path": "lastContactAt",
8770
8861
  "valueType": "number"
@@ -9007,10 +9098,6 @@ Object.freeze({
9007
9098
  "datetime"
9008
9099
  ]
9009
9100
  },
9010
- {
9011
- "path": "unit",
9012
- "valueType": "string"
9013
- },
9014
9101
  {
9015
9102
  "path": "min",
9016
9103
  "valueType": "number"
@@ -9022,19 +9109,6 @@ Object.freeze({
9022
9109
  {
9023
9110
  "path": "step",
9024
9111
  "valueType": "number"
9025
- },
9026
- {
9027
- "path": "precision",
9028
- "valueType": "number"
9029
- },
9030
- {
9031
- "path": "format",
9032
- "valueType": "enum",
9033
- "enumValues": [
9034
- "date",
9035
- "time",
9036
- "datetime"
9037
- ]
9038
9112
  }
9039
9113
  ],
9040
9114
  "cover": [
@@ -9275,13 +9349,8 @@ Object.freeze({
9275
9349
  "path": "value",
9276
9350
  "valueType": "string"
9277
9351
  }, {
9278
- "path": "format",
9279
- "valueType": "enum",
9280
- "enumValues": [
9281
- "date",
9282
- "time",
9283
- "datetime"
9284
- ]
9352
+ "path": "timestamp",
9353
+ "valueType": "boolean"
9285
9354
  }],
9286
9355
  "event-emitter": [
9287
9356
  {
@@ -9354,6 +9423,24 @@ Object.freeze({
9354
9423
  "path": "detected",
9355
9424
  "valueType": "boolean"
9356
9425
  }],
9426
+ "group-active": [{
9427
+ "path": "active",
9428
+ "valueType": "boolean"
9429
+ }],
9430
+ "group-status": [
9431
+ {
9432
+ "path": "included",
9433
+ "valueType": "number"
9434
+ },
9435
+ {
9436
+ "path": "excluded",
9437
+ "valueType": "number"
9438
+ },
9439
+ {
9440
+ "path": "total",
9441
+ "valueType": "number"
9442
+ }
9443
+ ],
9357
9444
  "heating-valve": [{
9358
9445
  "path": "position",
9359
9446
  "valueType": "number"
@@ -9388,20 +9475,10 @@ Object.freeze({
9388
9475
  "valueType": "number"
9389
9476
  }
9390
9477
  ],
9391
- "humidity-sensor": [
9392
- {
9393
- "path": "percent",
9394
- "valueType": "number"
9395
- },
9396
- {
9397
- "path": "unit",
9398
- "valueType": "string"
9399
- },
9400
- {
9401
- "path": "precision",
9402
- "valueType": "number"
9403
- }
9404
- ],
9478
+ "humidity-sensor": [{
9479
+ "path": "percent",
9480
+ "valueType": "number"
9481
+ }],
9405
9482
  "image": [{
9406
9483
  "path": "url",
9407
9484
  "valueType": "string"
@@ -9609,6 +9686,10 @@ Object.freeze({
9609
9686
  ]
9610
9687
  }
9611
9688
  ],
9689
+ "meter": [{
9690
+ "path": "value",
9691
+ "valueType": "number"
9692
+ }],
9612
9693
  "motion": [
9613
9694
  {
9614
9695
  "path": "detected",
@@ -9740,20 +9821,10 @@ Object.freeze({
9740
9821
  "valueType": "boolean"
9741
9822
  }
9742
9823
  ],
9743
- "numeric-sensor": [
9744
- {
9745
- "path": "value",
9746
- "valueType": "number"
9747
- },
9748
- {
9749
- "path": "unit",
9750
- "valueType": "string"
9751
- },
9752
- {
9753
- "path": "precision",
9754
- "valueType": "number"
9755
- }
9756
- ],
9824
+ "numeric-sensor": [{
9825
+ "path": "value",
9826
+ "valueType": "number"
9827
+ }],
9757
9828
  "pet-feeder": [
9758
9829
  {
9759
9830
  "path": "foodLevel",
@@ -9845,14 +9916,6 @@ Object.freeze({
9845
9916
  {
9846
9917
  "path": "powerFactor",
9847
9918
  "valueType": "number"
9848
- },
9849
- {
9850
- "path": "unit",
9851
- "valueType": "string"
9852
- },
9853
- {
9854
- "path": "precision",
9855
- "valueType": "number"
9856
9919
  }
9857
9920
  ],
9858
9921
  "presence": [
@@ -9881,20 +9944,10 @@ Object.freeze({
9881
9944
  "valueType": "number"
9882
9945
  }
9883
9946
  ],
9884
- "pressure-sensor": [
9885
- {
9886
- "path": "hpa",
9887
- "valueType": "number"
9888
- },
9889
- {
9890
- "path": "unit",
9891
- "valueType": "string"
9892
- },
9893
- {
9894
- "path": "precision",
9895
- "valueType": "number"
9896
- }
9897
- ],
9947
+ "pressure-sensor": [{
9948
+ "path": "hpa",
9949
+ "valueType": "number"
9950
+ }],
9898
9951
  "privacy-mask": [{
9899
9952
  "path": "enabled",
9900
9953
  "valueType": "boolean"
@@ -10205,20 +10258,10 @@ Object.freeze({
10205
10258
  "path": "tampered",
10206
10259
  "valueType": "boolean"
10207
10260
  }],
10208
- "temperature-sensor": [
10209
- {
10210
- "path": "celsius",
10211
- "valueType": "number"
10212
- },
10213
- {
10214
- "path": "unit",
10215
- "valueType": "string"
10216
- },
10217
- {
10218
- "path": "precision",
10219
- "valueType": "number"
10220
- }
10221
- ],
10261
+ "temperature-sensor": [{
10262
+ "path": "celsius",
10263
+ "valueType": "number"
10264
+ }],
10222
10265
  "update": [
10223
10266
  {
10224
10267
  "path": "currentVersion",
@@ -10329,10 +10372,6 @@ Object.freeze({
10329
10372
  "path": "temperature",
10330
10373
  "valueType": "number"
10331
10374
  },
10332
- {
10333
- "path": "temperatureUnit",
10334
- "valueType": "string"
10335
- },
10336
10375
  {
10337
10376
  "path": "humidity",
10338
10377
  "valueType": "number"
@@ -10341,18 +10380,10 @@ Object.freeze({
10341
10380
  "path": "pressure",
10342
10381
  "valueType": "number"
10343
10382
  },
10344
- {
10345
- "path": "pressureUnit",
10346
- "valueType": "string"
10347
- },
10348
10383
  {
10349
10384
  "path": "windSpeed",
10350
10385
  "valueType": "number"
10351
10386
  },
10352
- {
10353
- "path": "windSpeedUnit",
10354
- "valueType": "string"
10355
- },
10356
10387
  {
10357
10388
  "path": "windBearing",
10358
10389
  "valueType": "number"
@@ -10402,7 +10433,7 @@ object({
10402
10433
  params: record(string(), union([string(), number()])).optional()
10403
10434
  });
10404
10435
  //#endregion
10405
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10436
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10406
10437
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10407
10438
  DeviceType["Camera"] = "camera";
10408
10439
  DeviceType["Hub"] = "hub";
@@ -10496,8 +10527,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10496
10527
  DeviceType["PetFeeder"] = "pet-feeder";
10497
10528
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10498
10529
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10499
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10530
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10531
+ * A plug-built dishwasher carries running-state and power instead. */
10500
10532
  DeviceType["Dishwasher"] = "dishwasher";
10533
+ /** Washing machine. A plug-built one is running while its power stays
10534
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10535
+ DeviceType["Washer"] = "washer";
10536
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10537
+ DeviceType["Dryer"] = "dryer";
10501
10538
  return DeviceType;
10502
10539
  }({});
10503
10540
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -10745,7 +10782,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
10745
10782
  return DeviceRole;
10746
10783
  }({});
10747
10784
  //#endregion
10748
- //#region ../types/dist/composition-kEl9mevu.mjs
10785
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
10749
10786
  /**
10750
10787
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10751
10788
  *
@@ -11060,6 +11097,13 @@ var table = {
11060
11097
  apply: () => {
11061
11098
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11062
11099
  }
11100
+ },
11101
+ schmitt: {
11102
+ minArgs: 3,
11103
+ maxArgs: 5,
11104
+ apply: () => {
11105
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11106
+ }
11063
11107
  }
11064
11108
  };
11065
11109
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11500,15 +11544,232 @@ function validateExpressionSource(src) {
11500
11544
  }
11501
11545
  return null;
11502
11546
  }
11503
- /** A composed device's stableId is this prefix plus the block id. */
11504
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11505
- /** Longest snippet a `code` source may carry (slice 4). */
11506
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11547
+ /**
11548
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11549
+ *
11550
+ * Standby of the control board is under 5 W; a motor or a heater is far
11551
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11552
+ * a cycle, so running starts only after 30 s above the line.
11553
+ *
11554
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11555
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11556
+ * report "finished" in the middle of the program, so the stop dwell is
11557
+ * 10 minutes on every type. The number is stored per type so one machine
11558
+ * can move later without moving the others.
11559
+ */
11560
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11561
+ var APPLIANCE_KINDS = [
11562
+ "washer",
11563
+ "dryer",
11564
+ "dishwasher"
11565
+ ];
11566
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11567
+ /**
11568
+ * Statistics and integrals over a history read (D944).
11569
+ *
11570
+ * Pure: the same points always give the same answer, so a restart recomputes
11571
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11572
+ * never the mean of the samples. A hole with no held value is not integrated
11573
+ * as zero. A truncated read is not published.
11574
+ */
11575
+ var STATISTIC_STATS = [
11576
+ "min",
11577
+ "max",
11578
+ "mean",
11579
+ "sum",
11580
+ "count",
11581
+ "change"
11582
+ ];
11583
+ /**
11584
+ * The range a statistics or integral helper asks history for (D944).
11585
+ *
11586
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11587
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11588
+ * is strictly longer than the longest that range can be. Forever qualifies.
11589
+ * Equal to a tier's retention is a refusal, not a short answer.
11590
+ */
11591
+ var MINUTE_MS = 6e4;
11592
+ var HOUR_MS = 60 * MINUTE_MS;
11593
+ var DAY_MS = 24 * HOUR_MS;
11594
+ var ROLLING_SPANS = [
11595
+ "1h",
11596
+ "24h",
11597
+ "2d",
11598
+ "7d",
11599
+ "30d",
11600
+ "custom"
11601
+ ];
11602
+ var CALENDAR_PERIODS = [
11603
+ "today",
11604
+ "yesterday",
11605
+ "this-week",
11606
+ "this-month"
11607
+ ];
11608
+ var RESET_PERIODS = [
11609
+ "never",
11610
+ "daily",
11611
+ "weekly",
11612
+ "monthly"
11613
+ ];
11614
+ 5 * MINUTE_MS;
11615
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11616
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11617
+ var rollingWindowObject = object({
11618
+ kind: literal("rolling"),
11619
+ span: _enum(ROLLING_SPANS),
11620
+ customMinutes: number().int().min(5).optional()
11621
+ }).check((payload) => {
11622
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11623
+ code: "custom",
11624
+ message: "a custom range needs a duration in minutes",
11625
+ path: ["customMinutes"],
11626
+ input: payload.value
11627
+ });
11628
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11629
+ code: "custom",
11630
+ message: "only a custom range carries a duration",
11631
+ path: ["customMinutes"],
11632
+ input: payload.value
11633
+ });
11634
+ });
11635
+ var RollingWindowSchema = rollingWindowObject;
11636
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
11637
+ kind: literal("calendar"),
11638
+ period: _enum(CALENDAR_PERIODS)
11639
+ })]);
11640
+ /**
11641
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11642
+ * id. Split out so helper schemas can name it without a cycle through
11643
+ * `composition.ts` (D659).
11644
+ */
11507
11645
  var CompositionSourceRefSchema = object({
11508
11646
  /** The owning addon; `stableId` is unique only within it. */
11509
11647
  addonId: string().min(1),
11510
11648
  stableId: string().min(1)
11511
11649
  });
11650
+ var GROUP_TEMPLATE_IDS = [
11651
+ "temperature",
11652
+ "humidity",
11653
+ "pressure",
11654
+ "illuminance",
11655
+ "power",
11656
+ "energy",
11657
+ "battery",
11658
+ "numeric",
11659
+ "motion",
11660
+ "contact",
11661
+ "leak",
11662
+ "smoke",
11663
+ "occupancy",
11664
+ "binary",
11665
+ "switch",
11666
+ "light",
11667
+ "fan",
11668
+ "cover",
11669
+ "valve",
11670
+ "lock",
11671
+ "media-player",
11672
+ "climate"
11673
+ ];
11674
+ var NUMERIC_AGGREGATES = [
11675
+ "mean",
11676
+ "median",
11677
+ "min",
11678
+ "max",
11679
+ "sum"
11680
+ ];
11681
+ var BINARY_AGGREGATES = ["any", "all"];
11682
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11683
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11684
+ var AGGREGATE_REDUCERS = [
11685
+ "mean",
11686
+ "median",
11687
+ "min",
11688
+ "max",
11689
+ "sum",
11690
+ "any",
11691
+ "all"
11692
+ ];
11693
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11694
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11695
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11696
+ /** One member. The field is stored so occupancy can name motion or binary. */
11697
+ var HelperMemberSchema = object({
11698
+ source: CompositionSourceRefSchema,
11699
+ cap: string().min(1),
11700
+ fieldPath: string().min(1)
11701
+ });
11702
+ var HelperSpecSchema = discriminatedUnion("kind", [
11703
+ object({
11704
+ kind: literal("group"),
11705
+ templateId: GroupTemplateIdSchema,
11706
+ aggregate: AggregateReducerSchema,
11707
+ members: array(HelperMemberSchema).min(1).max(64),
11708
+ /** Actuator groups only. Readings leave this unset. */
11709
+ active: ActiveCriterionSchema.optional()
11710
+ }),
11711
+ object({
11712
+ kind: literal("appliance"),
11713
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
11714
+ appliance: _enum(APPLIANCE_KINDS),
11715
+ thresholdWatts: number().positive(),
11716
+ startDwellMs: number().int().nonnegative(),
11717
+ stopDwellMs: number().int().nonnegative(),
11718
+ member: HelperMemberSchema
11719
+ }),
11720
+ object({
11721
+ kind: literal("statistic"),
11722
+ stat: _enum(STATISTIC_STATS),
11723
+ window: StatisticWindowSchema,
11724
+ source: HelperMemberSchema
11725
+ }),
11726
+ object({
11727
+ kind: literal("integral"),
11728
+ reset: _enum(RESET_PERIODS),
11729
+ output: _enum(["energy", "meter"]),
11730
+ source: HelperMemberSchema
11731
+ }),
11732
+ object({
11733
+ kind: literal("threshold"),
11734
+ source: HelperMemberSchema,
11735
+ high: number(),
11736
+ low: number(),
11737
+ onForMs: number().int().nonnegative(),
11738
+ offForMs: number().int().nonnegative()
11739
+ }),
11740
+ object({
11741
+ kind: literal("arithmetic"),
11742
+ op: _enum([
11743
+ "sum",
11744
+ "difference",
11745
+ "ratio"
11746
+ ]),
11747
+ inputs: array(HelperMemberSchema).min(2).max(64)
11748
+ }),
11749
+ object({
11750
+ kind: literal("expression"),
11751
+ expr: string().min(1),
11752
+ inputs: array(object({
11753
+ name: string().min(1),
11754
+ member: HelperMemberSchema
11755
+ })).min(1).max(64),
11756
+ output: _enum(["number", "boolean"])
11757
+ }),
11758
+ object({
11759
+ kind: literal("derivative"),
11760
+ window: RollingWindowSchema,
11761
+ per: _enum([
11762
+ "second",
11763
+ "minute",
11764
+ "hour"
11765
+ ]),
11766
+ source: HelperMemberSchema
11767
+ })
11768
+ ]);
11769
+ /** A composed device's stableId is this prefix plus the block id. */
11770
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11771
+ /** Longest snippet a `code` source may carry (slice 4). */
11772
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11512
11773
  /** One field of one capability of one source device. */
11513
11774
  var CompositionFieldReadSchema = object({
11514
11775
  source: CompositionSourceRefSchema,
@@ -11552,7 +11813,59 @@ var CompositionCodeSourceSchema = object({
11552
11813
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11553
11814
  CompositionFromSourceSchema,
11554
11815
  CompositionExpressionSourceSchema,
11555
- CompositionCodeSourceSchema
11816
+ CompositionCodeSourceSchema,
11817
+ object({
11818
+ kind: literal("aggregate"),
11819
+ reducer: _enum([
11820
+ "mean",
11821
+ "median",
11822
+ "min",
11823
+ "max",
11824
+ "sum",
11825
+ "any",
11826
+ "all"
11827
+ ]),
11828
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11829
+ /**
11830
+ * Set when the reads are an enum state, not a yes/no: the listed states
11831
+ * count as active (a cover is `open`). A state outside the list is inactive;
11832
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
11833
+ */
11834
+ activeWhen: array(string().min(1)).min(1).optional(),
11835
+ /** `value` is the reading; the others are the member counts. */
11836
+ publish: _enum([
11837
+ "value",
11838
+ "included",
11839
+ "excluded",
11840
+ "total"
11841
+ ]),
11842
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11843
+ empty: _enum([
11844
+ "null",
11845
+ "omit",
11846
+ "offline"
11847
+ ])
11848
+ }),
11849
+ object({
11850
+ kind: literal("series"),
11851
+ read: CompositionFieldReadSchema,
11852
+ /** `value` is the statistic or the integral. The others are the gap counts. */
11853
+ publish: _enum([
11854
+ "value",
11855
+ "included",
11856
+ "excluded",
11857
+ "total"
11858
+ ]),
11859
+ /**
11860
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
11861
+ * device unseeded until history answers, so a restart cannot publish 0.
11862
+ */
11863
+ empty: _enum([
11864
+ "null",
11865
+ "omit",
11866
+ "offline"
11867
+ ])
11868
+ })
11556
11869
  ]);
11557
11870
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11558
11871
  kind: literal("forward"),
@@ -11598,7 +11911,13 @@ var CompositionSchema = object({
11598
11911
  kind: literal("existing"),
11599
11912
  device: CompositionSourceRefSchema
11600
11913
  })]),
11601
- features: array(CompositionFeatureSchema).min(1).max(16)
11914
+ features: array(CompositionFeatureSchema).min(1).max(16),
11915
+ /**
11916
+ * Present when the block is a helper template. Absent on a free
11917
+ * composition (the room example, device 4603). The editor reads this,
11918
+ * never the compiled features.
11919
+ */
11920
+ helper: HelperSpecSchema.optional()
11602
11921
  }).superRefine((composition, ctx) => {
11603
11922
  const seen = /* @__PURE__ */ new Set();
11604
11923
  composition.features.forEach((feature, index) => {
@@ -16705,7 +17024,22 @@ var BrokerProviderInfoSchema = object({
16705
17024
  */
16706
17025
  interactiveConnectPath: string().min(1).optional(),
16707
17026
  /** Relative hub path to disconnect the linked account (same popup pattern). */
16708
- interactiveDisconnectPath: string().min(1).optional()
17027
+ interactiveDisconnectPath: string().min(1).optional(),
17028
+ /**
17029
+ * The kind signs in through a server-held interactive session instead of
17030
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17031
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17032
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17033
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17034
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17035
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17036
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17037
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17038
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17039
+ * offers it as the default path; the fields the session fills are
17040
+ * marked `filledBySignIn` in the kind's settings schema.
17041
+ */
17042
+ signIn: object({ basePath: string().min(1) }).optional()
16709
17043
  }))
16710
17044
  });
16711
17045
  var ListInputSchema = object({
@@ -17700,7 +18034,12 @@ var CompositionProblemSchema = object({
17700
18034
  "item-field-reserved",
17701
18035
  "target-missing",
17702
18036
  "target-unreadable",
17703
- "not-replaceable"
18037
+ "not-replaceable",
18038
+ "native-capability-replace",
18039
+ "mixed-quantity",
18040
+ "helper-mismatch",
18041
+ "member-constraint",
18042
+ "retention"
17704
18043
  ]),
17705
18044
  path: string(),
17706
18045
  message: string()
@@ -17721,7 +18060,8 @@ var CompositionUnavailableModeSchema = _enum([
17721
18060
  "null",
17722
18061
  "offline",
17723
18062
  "unknown",
17724
- "release"
18063
+ "release",
18064
+ "omit"
17725
18065
  ]);
17726
18066
  /**
17727
18067
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17758,9 +18098,11 @@ var CompositionFieldPlanSchema = object({
17758
18098
  reads: array(CompositionFieldReadSchema)
17759
18099
  });
17760
18100
  /**
17761
- * What a feature does to its target (D663): `new` builds a capability on a new
17762
- * device; on an existing device it `add`s a capability the device lacks, or
17763
- * `replace`s named fields of one it has natively.
18101
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18102
+ * new device; on an existing device it `add`s a capability the device lacks.
18103
+ * Replacing a capability the device already serves is refused
18104
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18105
+ * wire for a claim the hub already holds.
17764
18106
  */
17765
18107
  var CompositionFeatureModeSchema = _enum([
17766
18108
  "new",
@@ -17931,8 +18273,392 @@ Object.freeze({
17931
18273
  latchedAt: {
17932
18274
  kind: "fixed",
17933
18275
  result: ["number", "null"]
18276
+ },
18277
+ schmitt: {
18278
+ kind: "fixed",
18279
+ result: ["boolean", "null"]
17934
18280
  }
17935
18281
  });
18282
+ /**
18283
+ * One catalog row per base reading a group helper can build (D944 §3).
18284
+ * Data, not a form default written into the block.
18285
+ */
18286
+ var NOW$2 = {
18287
+ fieldPath: "lastFetchedAt",
18288
+ expr: "now"
18289
+ };
18290
+ var CHANGED = {
18291
+ fieldPath: "lastChangedAt",
18292
+ expr: "now"
18293
+ };
18294
+ function numeric(input) {
18295
+ return {
18296
+ ...input,
18297
+ family: "numeric",
18298
+ deviceType: DeviceType.Sensor,
18299
+ memberDeviceType: DeviceType.Sensor,
18300
+ memberDeviceTypes: [DeviceType.Sensor],
18301
+ activeWhen: void 0,
18302
+ commandCaps: [],
18303
+ baseUnit: baseUnitOf(input.quantity),
18304
+ aggregates: NUMERIC_AGGREGATES,
18305
+ accept: [{
18306
+ cap: input.outputCap,
18307
+ fieldPath: input.outputField
18308
+ }]
18309
+ };
18310
+ }
18311
+ function binary(input) {
18312
+ return {
18313
+ ...input,
18314
+ family: "binary",
18315
+ deviceType: DeviceType.Sensor,
18316
+ memberDeviceType: DeviceType.Sensor,
18317
+ memberDeviceTypes: [DeviceType.Sensor],
18318
+ activeWhen: void 0,
18319
+ commandCaps: [],
18320
+ quantity: "none",
18321
+ baseUnit: void 0,
18322
+ aggregates: BINARY_AGGREGATES,
18323
+ empty: "offline"
18324
+ };
18325
+ }
18326
+ var GROUP_TEMPLATES = [
18327
+ numeric({
18328
+ id: "temperature",
18329
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18330
+ role: DeviceRole.TemperatureSensor,
18331
+ outputCap: "temperature-sensor",
18332
+ outputField: "celsius",
18333
+ quantity: "temperature",
18334
+ defaultAggregate: "mean",
18335
+ empty: "null",
18336
+ companions: [NOW$2]
18337
+ }),
18338
+ numeric({
18339
+ id: "humidity",
18340
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18341
+ role: DeviceRole.HumiditySensor,
18342
+ outputCap: "humidity-sensor",
18343
+ outputField: "percent",
18344
+ quantity: "humidity",
18345
+ defaultAggregate: "mean",
18346
+ empty: "null",
18347
+ companions: [NOW$2]
18348
+ }),
18349
+ numeric({
18350
+ id: "pressure",
18351
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18352
+ role: DeviceRole.PressureSensor,
18353
+ outputCap: "pressure-sensor",
18354
+ outputField: "hpa",
18355
+ quantity: "pressure",
18356
+ defaultAggregate: "mean",
18357
+ empty: "null",
18358
+ companions: [NOW$2]
18359
+ }),
18360
+ numeric({
18361
+ id: "illuminance",
18362
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18363
+ role: DeviceRole.AmbientLightSensor,
18364
+ outputCap: "ambient-light-sensor",
18365
+ outputField: "lux",
18366
+ quantity: "illuminance",
18367
+ defaultAggregate: "mean",
18368
+ empty: "null",
18369
+ companions: [NOW$2]
18370
+ }),
18371
+ numeric({
18372
+ id: "power",
18373
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18374
+ role: DeviceRole.PowerSensor,
18375
+ outputCap: "power-meter",
18376
+ outputField: "watts",
18377
+ quantity: "power",
18378
+ defaultAggregate: "sum",
18379
+ empty: "omit",
18380
+ companions: [NOW$2]
18381
+ }),
18382
+ numeric({
18383
+ id: "energy",
18384
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18385
+ role: DeviceRole.EnergySensor,
18386
+ outputCap: "power-meter",
18387
+ outputField: "kwhTotal",
18388
+ quantity: "energy",
18389
+ defaultAggregate: "sum",
18390
+ empty: "omit",
18391
+ companions: [NOW$2]
18392
+ }),
18393
+ numeric({
18394
+ id: "battery",
18395
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18396
+ role: DeviceRole.BatterySensor,
18397
+ outputCap: "battery",
18398
+ outputField: "percentage",
18399
+ quantity: "percentage",
18400
+ defaultAggregate: "mean",
18401
+ empty: "null",
18402
+ companions: [
18403
+ {
18404
+ fieldPath: "charging",
18405
+ expr: "\"unknown\""
18406
+ },
18407
+ {
18408
+ fieldPath: "sleeping",
18409
+ expr: "false"
18410
+ },
18411
+ {
18412
+ fieldPath: "lastUpdated",
18413
+ expr: "now"
18414
+ }
18415
+ ]
18416
+ }),
18417
+ numeric({
18418
+ id: "numeric",
18419
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18420
+ role: DeviceRole.NumericSensor,
18421
+ outputCap: "numeric-sensor",
18422
+ outputField: "value",
18423
+ quantity: "device",
18424
+ defaultAggregate: "mean",
18425
+ empty: "null",
18426
+ companions: [NOW$2]
18427
+ }),
18428
+ binary({
18429
+ id: "motion",
18430
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18431
+ role: DeviceRole.MotionSensor,
18432
+ outputCap: "motion",
18433
+ outputField: "detected",
18434
+ defaultAggregate: "any",
18435
+ accept: [{
18436
+ cap: "motion",
18437
+ fieldPath: "detected"
18438
+ }],
18439
+ companions: [{
18440
+ fieldPath: "lastDetectedAt",
18441
+ expr: "null"
18442
+ }, {
18443
+ fieldPath: "autoClearAfterMs",
18444
+ expr: "null"
18445
+ }]
18446
+ }),
18447
+ binary({
18448
+ id: "contact",
18449
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18450
+ role: DeviceRole.ContactSensor,
18451
+ outputCap: "contact",
18452
+ outputField: "entryOpen",
18453
+ defaultAggregate: "any",
18454
+ accept: [{
18455
+ cap: "contact",
18456
+ fieldPath: "entryOpen"
18457
+ }],
18458
+ companions: [CHANGED]
18459
+ }),
18460
+ binary({
18461
+ id: "leak",
18462
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18463
+ role: DeviceRole.LeakSensor,
18464
+ outputCap: "flood",
18465
+ outputField: "flooded",
18466
+ defaultAggregate: "any",
18467
+ accept: [{
18468
+ cap: "flood",
18469
+ fieldPath: "flooded"
18470
+ }],
18471
+ companions: [CHANGED]
18472
+ }),
18473
+ binary({
18474
+ id: "smoke",
18475
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18476
+ role: DeviceRole.SmokeSensor,
18477
+ outputCap: "smoke",
18478
+ outputField: "detected",
18479
+ defaultAggregate: "any",
18480
+ accept: [{
18481
+ cap: "smoke",
18482
+ fieldPath: "detected"
18483
+ }],
18484
+ companions: [CHANGED]
18485
+ }),
18486
+ binary({
18487
+ id: "occupancy",
18488
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18489
+ role: DeviceRole.BinarySensor,
18490
+ outputCap: "binary",
18491
+ outputField: "on",
18492
+ defaultAggregate: "any",
18493
+ accept: [{
18494
+ cap: "motion",
18495
+ fieldPath: "detected"
18496
+ }, {
18497
+ cap: "binary",
18498
+ fieldPath: "on"
18499
+ }],
18500
+ companions: [CHANGED]
18501
+ }),
18502
+ binary({
18503
+ id: "binary",
18504
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18505
+ role: DeviceRole.BinarySensor,
18506
+ outputCap: "binary",
18507
+ outputField: "on",
18508
+ defaultAggregate: "any",
18509
+ accept: [{
18510
+ cap: "binary",
18511
+ fieldPath: "on"
18512
+ }],
18513
+ companions: [CHANGED]
18514
+ }),
18515
+ actuator({
18516
+ id: "switch",
18517
+ 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.",
18518
+ deviceType: DeviceType.Switch,
18519
+ outputCap: "switch",
18520
+ outputField: "on",
18521
+ member: {
18522
+ cap: "switch",
18523
+ fieldPath: "on"
18524
+ },
18525
+ commandCaps: ["switch"]
18526
+ }),
18527
+ actuator({
18528
+ id: "light",
18529
+ 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.",
18530
+ deviceType: DeviceType.Light,
18531
+ outputCap: "switch",
18532
+ outputField: "on",
18533
+ member: {
18534
+ cap: "switch",
18535
+ fieldPath: "on"
18536
+ },
18537
+ commandCaps: [
18538
+ "switch",
18539
+ "brightness",
18540
+ "color"
18541
+ ]
18542
+ }),
18543
+ actuator({
18544
+ id: "fan",
18545
+ 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.",
18546
+ deviceType: DeviceType.Fan,
18547
+ outputCap: "switch",
18548
+ outputField: "on",
18549
+ member: {
18550
+ cap: "switch",
18551
+ fieldPath: "on"
18552
+ },
18553
+ commandCaps: ["switch", "fan-control"]
18554
+ }),
18555
+ actuator({
18556
+ id: "cover",
18557
+ 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.",
18558
+ deviceType: DeviceType.Cover,
18559
+ outputCap: "group-active",
18560
+ outputField: "active",
18561
+ member: {
18562
+ cap: "cover",
18563
+ fieldPath: "state"
18564
+ },
18565
+ activeWhen: [
18566
+ "open",
18567
+ "opening",
18568
+ "stopped"
18569
+ ],
18570
+ commandCaps: ["cover"]
18571
+ }),
18572
+ actuator({
18573
+ id: "valve",
18574
+ 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.",
18575
+ deviceType: DeviceType.Valve,
18576
+ outputCap: "group-active",
18577
+ outputField: "active",
18578
+ member: {
18579
+ cap: "valve",
18580
+ fieldPath: "state"
18581
+ },
18582
+ activeWhen: [
18583
+ "open",
18584
+ "opening",
18585
+ "stopped"
18586
+ ],
18587
+ commandCaps: ["valve"]
18588
+ }),
18589
+ actuator({
18590
+ id: "lock",
18591
+ 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.",
18592
+ deviceType: DeviceType.Lock,
18593
+ outputCap: "group-active",
18594
+ outputField: "active",
18595
+ member: {
18596
+ cap: "lock-control",
18597
+ fieldPath: "state"
18598
+ },
18599
+ activeWhen: ["locked"],
18600
+ commandCaps: ["lock-control"]
18601
+ }),
18602
+ actuator({
18603
+ id: "media-player",
18604
+ 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.",
18605
+ deviceType: DeviceType.MediaPlayer,
18606
+ outputCap: "group-active",
18607
+ outputField: "active",
18608
+ member: {
18609
+ cap: "media-player",
18610
+ fieldPath: "state"
18611
+ },
18612
+ activeWhen: ["playing", "buffering"],
18613
+ commandCaps: ["media-player"]
18614
+ }),
18615
+ actuator({
18616
+ id: "climate",
18617
+ 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.",
18618
+ deviceType: DeviceType.Thermostat,
18619
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
18620
+ outputCap: "group-active",
18621
+ outputField: "active",
18622
+ member: {
18623
+ cap: "climate-control",
18624
+ fieldPath: "mode"
18625
+ },
18626
+ activeWhen: [
18627
+ "heat",
18628
+ "cool",
18629
+ "auto",
18630
+ "heat_cool",
18631
+ "fan_only",
18632
+ "dry"
18633
+ ],
18634
+ commandCaps: ["climate-control"]
18635
+ })
18636
+ ];
18637
+ function actuator(input) {
18638
+ return {
18639
+ id: input.id,
18640
+ summary: input.summary,
18641
+ deviceType: input.deviceType,
18642
+ outputCap: input.outputCap,
18643
+ outputField: input.outputField,
18644
+ commandCaps: input.commandCaps,
18645
+ family: "actuator",
18646
+ role: void 0,
18647
+ memberDeviceType: input.deviceType,
18648
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
18649
+ activeWhen: input.activeWhen,
18650
+ quantity: "none",
18651
+ baseUnit: void 0,
18652
+ aggregates: BINARY_AGGREGATES,
18653
+ defaultAggregate: "any",
18654
+ empty: "offline",
18655
+ accept: [input.member],
18656
+ companions: [CHANGED]
18657
+ };
18658
+ }
18659
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18660
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18661
+ DeviceType.Camera;
17936
18662
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
17937
18663
  /**
17938
18664
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19646,12 +20372,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19646
20372
  * providers detect renames by matching `uniqueId` and patch `id`
19647
20373
  * in-place so dispatch keeps working.
19648
20374
  *
19649
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19650
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19651
- * state push and are the single source of truth. The `device_class` tag is
19652
- * an HA-specific input that is consumed at the discovery boundary
19653
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19654
- * stored after that point.
20375
+ * **Display unit and precision are NOT stored here.** They are a read-time
20376
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20377
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20378
+ * neither is repeated on each sample. The `device_class` tag is an
20379
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20380
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19655
20381
  *
19656
20382
  * The `raw` escape hatch carries provider-specific attributes that don't
19657
20383
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19665,6 +20391,14 @@ var SourceInfoSchema = object({
19665
20391
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19666
20392
  * Used to detect rename when `id` changes. */
19667
20393
  uniqueId: string().optional(),
20394
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20395
+ quantity: ReadingQuantitySchema.optional(),
20396
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20397
+ dateTimeShape: _enum([
20398
+ "date",
20399
+ "time",
20400
+ "datetime"
20401
+ ]).optional(),
19668
20402
  /** Provider-specific extras that don't fit the structured slots.
19669
20403
  * Whitelist what goes here per provider — do NOT dump entire HA
19670
20404
  * attribute blobs (would explode the meta JSON column). */
@@ -40577,7 +41311,8 @@ method(object({
40577
41311
  "enum"
40578
41312
  ]),
40579
41313
  enumValues: array(string()).optional(),
40580
- item: boolean().optional()
41314
+ item: boolean().optional(),
41315
+ quantity: ReadingQuantitySchema.optional()
40581
41316
  })).readonly(),
40582
41317
  itemArray: object({
40583
41318
  path: string(),
@@ -42938,7 +43673,7 @@ var NetworkAccessStatusSchema = object({
42938
43673
  });
42939
43674
  /**
42940
43675
  * Optional, richer endpoint shape returned by providers that expose
42941
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
43676
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
42942
43677
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
42943
43678
  * the originating provider config (mode + sourcePort) so the
42944
43679
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44377,8 +45112,8 @@ var MaskPolygonShapeSchema = object({
44377
45112
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44378
45113
  var MaskGridShapeSchema = object({
44379
45114
  kind: literal("grid"),
44380
- gridWidth: number(),
44381
- gridHeight: number(),
45115
+ gridWidth: number().meta(readingMeta("none")),
45116
+ gridHeight: number().meta(readingMeta("none")),
44382
45117
  cells: array(boolean())
44383
45118
  });
44384
45119
  discriminatedUnion("kind", [
@@ -44490,6 +45225,8 @@ var NcSystemEventKindSchema = _enum([
44490
45225
  "detection-blind",
44491
45226
  "motion-noisy",
44492
45227
  "motion-calm",
45228
+ "package-llm-silent",
45229
+ "package-llm-answered",
44493
45230
  "recording-stalled",
44494
45231
  "recording-healthy",
44495
45232
  "recording-degraded",
@@ -45579,7 +46316,20 @@ var NcRuleInputSchema = object({
45579
46316
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
45580
46317
  * path does not run Zod defaults.
45581
46318
  */
45582
- groupingSec: number().int().min(5).max(60).optional()
46319
+ groupingSec: number().int().min(5).max(60).optional(),
46320
+ /**
46321
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46322
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46323
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46324
+ * same mark the Library writes, and records whose notification it was and
46325
+ * when. A channel with no buttons gets the same two choices as links.
46326
+ *
46327
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46328
+ * cap path): absent and `false` both mean off, which is what every rule
46329
+ * authored before this field meant. An editor turns it off by writing
46330
+ * `false`, so it needs no `clear` key.
46331
+ */
46332
+ askVerdict: boolean().optional()
45583
46333
  });
45584
46334
  /**
45585
46335
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -55517,6 +56267,28 @@ var PackageVersionInfoSchema = object({
55517
56267
  deprecated: string().optional(),
55518
56268
  distTags: array(string()).readonly()
55519
56269
  });
56270
+ /**
56271
+ * One published document. `ok` is markdown from the registry. `missing` means
56272
+ * the registry answered and the file was not there. `error` is a registry or
56273
+ * tarball failure — never an empty string standing in for either.
56274
+ */
56275
+ var PackageDocBodySchema = discriminatedUnion("status", [
56276
+ object({
56277
+ status: literal("ok"),
56278
+ markdown: string()
56279
+ }),
56280
+ object({ status: literal("missing") }),
56281
+ object({
56282
+ status: literal("error"),
56283
+ message: string()
56284
+ })
56285
+ ]);
56286
+ var PackageDocsSchema = object({
56287
+ name: string(),
56288
+ version: string(),
56289
+ readme: PackageDocBodySchema,
56290
+ changelog: PackageDocBodySchema
56291
+ });
55520
56292
  var SearchResultSchema = object({
55521
56293
  name: string(),
55522
56294
  version: string(),
@@ -55735,7 +56507,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
55735
56507
  }), object({ success: literal(true) }), {
55736
56508
  kind: "mutation",
55737
56509
  auth: "admin"
55738
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
56510
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
56511
+ name: string().min(1),
56512
+ version: string().min(1).optional()
56513
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
55739
56514
  kind: "mutation",
55740
56515
  auth: "admin"
55741
56516
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -55785,46 +56560,27 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
55785
56560
  }), LogStreamEntrySchema, { kind: "subscription" });
55786
56561
  object({
55787
56562
  /** Carbon dioxide concentration in ppm. */
55788
- co2Ppm: number().min(0).optional(),
56563
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
55789
56564
  /** Total volatile organic compounds in ppb. */
55790
- vocPpb: number().min(0).optional(),
56565
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
55791
56566
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
55792
- pm25: number().min(0).optional(),
56567
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
55793
56568
  /** Particulate matter ≤ 10 μm in µg/m³. */
55794
- pm10: number().min(0).optional(),
56569
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
55795
56570
  /** Composite AQI value (typically 0..500). */
55796
- aqi: number().optional(),
56571
+ aqi: number().optional().meta(readingMeta("index")),
55797
56572
  /** Ms epoch when the slice was last updated. */
55798
- lastFetchedAt: number(),
55799
- /** Live display unit of the single metric this slice carries (e.g. HA
55800
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
55801
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
55802
- * per slice is unambiguous. */
55803
- unit: string().optional(),
55804
- /** Suggested decimal places for numeric display.
55805
- * Populated live from the upstream source when provided (e.g. HA
55806
- * `attributes.suggested_display_precision`). Falls back to
55807
- * auto-formatting when absent. */
55808
- precision: number().int().min(0).max(10).optional()
56573
+ lastFetchedAt: number()
55809
56574
  });
55810
56575
  DeviceType.Sensor;
55811
56576
  object({
55812
- /** Current illuminance in lux (lx). */
55813
- lux: number().min(0).meta({
55814
- unit: "lx",
56577
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56578
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56579
+ precision: 0,
55815
56580
  label: "Illuminance"
55816
- }),
56581
+ })),
55817
56582
  /** Ms epoch when the slice was last updated. */
55818
- lastFetchedAt: number(),
55819
- /** Live display unit from the upstream source (e.g. HA
55820
- * `attributes.unit_of_measurement`). The UI prefers this over the
55821
- * role's canonical unit. Absent → fall back to the canonical unit. */
55822
- unit: string().optional(),
55823
- /** Suggested decimal places for numeric display.
55824
- * Populated live from the upstream source when provided (e.g. HA
55825
- * `attributes.suggested_display_precision`). Falls back to
55826
- * auto-formatting when absent. */
55827
- precision: number().int().min(0).max(10).optional()
56583
+ lastFetchedAt: number()
55828
56584
  });
55829
56585
  DeviceType.Sensor;
55830
56586
  /**
@@ -55969,10 +56725,7 @@ var BatteryStatusSchema = object({
55969
56725
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
55970
56726
  * declines to band a non-finite reading, which is the correct shape.
55971
56727
  */
55972
- percentage: number().min(0).max(100).nullable().meta({
55973
- unit: "%",
55974
- label: "Charge"
55975
- }),
56728
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
55976
56729
  /**
55977
56730
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
55978
56731
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -55999,7 +56752,14 @@ var BatteryStatusSchema = object({
55999
56752
  */
56000
56753
  sleeping: boolean().meta({ label: "Sleeping" }),
56001
56754
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56002
- lastUpdated: number(),
56755
+ lastUpdated: number().meta(readingMeta("none")),
56756
+ /**
56757
+ * Battery voltage in volts, for a cell the firmware reports as voltage
56758
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
56759
+ * that only report charge. A second reading on this cap, not a second
56760
+ * `numeric-sensor` on the same device.
56761
+ */
56762
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56003
56763
  /**
56004
56764
  * Ms epoch of the last time the device PROVED it was reachable — a
56005
56765
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56039,7 +56799,7 @@ var BatteryStatusSchema = object({
56039
56799
  */
56040
56800
  binary: boolean().optional().meta({ label: "Binary indicator" })
56041
56801
  });
56042
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat;
56802
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter;
56043
56803
  method(object({
56044
56804
  deviceId: number(),
56045
56805
  /** Bound on the wait. Sensible range 3000–10000ms. */
@@ -56059,7 +56819,7 @@ object({
56059
56819
  DeviceType.Sensor;
56060
56820
  object({
56061
56821
  /** Current level as 0..100 inclusive. Firmware-reported. */
56062
- percentage: number().min(0).max(100),
56822
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56063
56823
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56064
56824
  lastChangedAt: number()
56065
56825
  });
@@ -56444,21 +57204,21 @@ object({
56444
57204
  /** Single setpoint in Celsius. Used by single-target modes
56445
57205
  * (heat / cool / dry). Null when the device is in a range mode
56446
57206
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
56447
- target: number().nullable(),
57207
+ target: number().nullable().meta(readingMeta("temperature")),
56448
57208
  /** Upper bound of the dual setpoint in Celsius. Populated only
56449
57209
  * in `heat_cool` mode. */
56450
- targetHigh: number().nullable(),
57210
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
56451
57211
  /** Lower bound of the dual setpoint in Celsius. Populated only
56452
57212
  * in `heat_cool` mode. */
56453
- targetLow: number().nullable(),
57213
+ targetLow: number().nullable().meta(readingMeta("temperature")),
56454
57214
  /** Target relative humidity (0..100). Null when the device has no
56455
57215
  * humidity control. */
56456
- targetHumidity: number().min(0).max(100).nullable(),
57216
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56457
57217
  /** Current measured temperature in Celsius. Read-only — pushed by
56458
57218
  * the device's internal sensor. */
56459
- currentTemp: number().nullable(),
57219
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
56460
57220
  /** Current measured relative humidity (0..100). Read-only. */
56461
- currentHumidity: number().min(0).max(100).nullable(),
57221
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56462
57222
  /** Vertical louver swing on/off. Null when the device has no vertical
56463
57223
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
56464
57224
  swingVertical: boolean().nullable(),
@@ -56466,11 +57226,11 @@ object({
56466
57226
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
56467
57227
  swingHorizontal: boolean().nullable(),
56468
57228
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
56469
- minTemp: number().nullable().optional(),
57229
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
56470
57230
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
56471
- maxTemp: number().nullable().optional(),
57231
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
56472
57232
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
56473
- tempStep: number().positive().nullable().optional(),
57233
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
56474
57234
  /** Ms epoch when the slice was last updated (push or command). */
56475
57235
  lastFetchedAt: number()
56476
57236
  });
@@ -56549,17 +57309,17 @@ DeviceType.Thermostat, DeviceType.Climate, method(object({
56549
57309
  * modes to expose in the picker without a slice fetch.
56550
57310
  */
56551
57311
  var RgbTripletSchema = object({
56552
- r: number().int().min(0).max(255),
56553
- g: number().int().min(0).max(255),
56554
- b: number().int().min(0).max(255)
57312
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57313
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57314
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
56555
57315
  });
56556
57316
  var HsvTripletSchema = object({
56557
57317
  /** Hue in degrees, 0..360. */
56558
- h: number().min(0).max(360),
57318
+ h: number().min(0).max(360).meta(readingMeta("none")),
56559
57319
  /** Saturation as 0..100 inclusive. */
56560
- s: number().min(0).max(100),
57320
+ s: number().min(0).max(100).meta(readingMeta("none")),
56561
57321
  /** Value/brightness as 0..100 inclusive. */
56562
- v: number().min(0).max(100)
57322
+ v: number().min(0).max(100).meta(readingMeta("none"))
56563
57323
  });
56564
57324
  var ColorInputSchema = discriminatedUnion("mode", [
56565
57325
  object({
@@ -56588,7 +57348,7 @@ object({
56588
57348
  /** Populated when `mode === 'hsv'`. */
56589
57349
  hsv: HsvTripletSchema.optional(),
56590
57350
  /** Populated when `mode === 'mired'`. */
56591
- mireds: number().int().optional(),
57351
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
56592
57352
  /** Ms epoch of the last operator-driven change. */
56593
57353
  lastChangedAt: number()
56594
57354
  });
@@ -56712,18 +57472,12 @@ _enum([
56712
57472
  "empty",
56713
57473
  "full"
56714
57474
  ]);
56715
- /** Units a remaining-life counter is reported in. Closed: the panel renders
56716
- * each. */
56717
- var ConsumableRemainingUnitSchema = _enum([
56718
- "days",
56719
- "hours",
56720
- "cycles"
56721
- ]);
56722
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
56723
- * days). `value` null = unknown (D393). */
57475
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57476
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57477
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
56724
57478
  var ConsumableRemainingSchema = object({
56725
- value: number().min(0).nullable(),
56726
- unit: ConsumableRemainingUnitSchema
57479
+ value: number().min(0).nullable().meta(readingMeta("device")),
57480
+ kind: ConsumableRemainingKindSchema
56727
57481
  });
56728
57482
  var ConsumablesStatusSchema = object({
56729
57483
  items: array(object({
@@ -56732,7 +57486,7 @@ var ConsumablesStatusSchema = object({
56732
57486
  /** Display name. */
56733
57487
  label: string().min(1),
56734
57488
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
56735
- level: number().min(0).max(100).nullable(),
57489
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
56736
57490
  /** The provider's discrete verdict when known (binary mode). */
56737
57491
  status: ConsumableStatusSchema.nullable(),
56738
57492
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -56749,7 +57503,7 @@ var ConsumablesStatusSchema = object({
56749
57503
  })),
56750
57504
  lastChangedAt: number()
56751
57505
  });
56752
- Object.values(DeviceType), [...ConsumableRemainingUnitSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57506
+ Object.values(DeviceType), [...ConsumableRemainingKindSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
56753
57507
  Object.values(DeviceType), method(object({
56754
57508
  deviceId: number().int().nonnegative(),
56755
57509
  key: string().min(1)
@@ -56774,39 +57528,17 @@ object({
56774
57528
  /** Current value. Numeric kind → number; select/text/datetime → string.
56775
57529
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
56776
57530
  * or time-only — HA accepts all three forms). */
56777
- value: union([number(), string()]),
57531
+ value: union([number().meta(readingMeta("device")), string()]),
56778
57532
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
56779
57533
  options: array(string()),
56780
57534
  /** Ms epoch when the slice was last updated. */
56781
57535
  lastChangedAt: number(),
56782
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
56783
- * Populated live from HA `attributes.unit_of_measurement` on each push.
56784
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
56785
- unit: string().optional(),
56786
- /** Minimum allowed value. Populated live from HA `attributes.min`.
56787
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
56788
- min: number().optional(),
56789
- /** Maximum allowed value. Populated live from HA `attributes.max`.
56790
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
56791
- max: number().optional(),
56792
- /** Step increment. Populated live from HA `attributes.step`.
56793
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
56794
- step: number().optional(),
56795
- /** Suggested decimal places for numeric display. Populated live from HA
56796
- * `attributes.suggested_display_precision`, or derived from `step` when the
56797
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
56798
- * other kinds. Falls back to auto-formatting when absent. */
56799
- precision: number().int().min(0).max(10).optional(),
56800
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
56801
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
56802
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
56803
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
56804
- * `has_date`/`has_time` attributes on each push. */
56805
- format: _enum([
56806
- "date",
56807
- "time",
56808
- "datetime"
56809
- ]).optional()
57536
+ /** Minimum allowed value, in the device quantity's base unit. */
57537
+ min: number().optional().meta(readingMeta("device")),
57538
+ /** Maximum allowed value, in the device quantity's base unit. */
57539
+ max: number().optional().meta(readingMeta("device")),
57540
+ /** Step increment, in the device quantity's base unit. */
57541
+ step: number().optional().meta(readingMeta("device"))
56810
57542
  });
56811
57543
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
56812
57544
  object({
@@ -56844,9 +57576,9 @@ object({
56844
57576
  ]),
56845
57577
  /** 0 = fully closed, 100 = fully open. Null when the device has
56846
57578
  * no intermediate position surface. */
56847
- position: number().min(0).max(100).nullable(),
57579
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
56848
57580
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
56849
- tiltPosition: number().min(0).max(100).nullable(),
57581
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
56850
57582
  /** Ms epoch when the slice was last updated. */
56851
57583
  lastChangedAt: number()
56852
57584
  });
@@ -56904,9 +57636,9 @@ var NormalizedRangeSchema$1 = object({
56904
57636
  object({
56905
57637
  mode: DayNightModeSchema,
56906
57638
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
56907
- sensitivity: number().optional(),
57639
+ sensitivity: number().optional().meta(readingMeta("none")),
56908
57640
  /** Delay before the IR-cut filter flips, in seconds. */
56909
- switchDelaySec: number().optional(),
57641
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
56910
57642
  lastFetchedAt: number()
56911
57643
  });
56912
57644
  /**
@@ -57061,11 +57793,11 @@ object({
57061
57793
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
57062
57794
  availablePrograms: array(string()),
57063
57795
  /** Seconds remaining on the active cycle, or null when not running / not known. */
57064
- remainingSeconds: number().int().nonnegative().nullable(),
57796
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57065
57797
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
57066
- delayedStartSeconds: number().int().nonnegative().nullable(),
57798
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57067
57799
  /** Cycle completion 0..100, or null when not applicable. */
57068
- progressPercent: number().min(0).max(100).nullable(),
57800
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57069
57801
  /** True when the door is reported open; null when the door state is not known. */
57070
57802
  doorOpen: boolean().nullable(),
57071
57803
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -57089,7 +57821,7 @@ object({
57089
57821
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57090
57822
  machineCareDue: boolean().nullable(),
57091
57823
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57092
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
57824
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
57093
57825
  /** Option states: null when the appliance does not expose the option or has not said. */
57094
57826
  halfLoad: boolean().nullable(),
57095
57827
  hygienePlus: boolean().nullable(),
@@ -57104,7 +57836,7 @@ object({
57104
57836
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
57105
57837
  activeEvents: array(string()),
57106
57838
  /** Numeric error code when the appliance reports one; null otherwise. */
57107
- errorCode: number().nullable(),
57839
+ errorCode: number().nullable().meta(readingMeta("none")),
57108
57840
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
57109
57841
  errorLabel: string().nullable(),
57110
57842
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -57250,24 +57982,14 @@ object({
57250
57982
  /** Ms epoch of the last press. null = never observed since this provider started. */
57251
57983
  lastPressedAt: number().nullable(),
57252
57984
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
57253
- pressCountSinceStart: number()
57985
+ pressCountSinceStart: number().meta(readingMeta("count"))
57254
57986
  });
57255
57987
  object({
57256
57988
  deviceId: number(),
57257
57989
  timestamp: number()
57258
57990
  });
57259
57991
  DeviceType.Button, DeviceType.Camera;
57260
- /**
57261
- * Enum-state sensor — a string value picked from a finite option set.
57262
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
57263
- * states, weather conditions, contact-source channels, mode strings).
57264
- * The option set is stable across observations; it is persisted in the
57265
- * device config blob (via the control slice at adoption) and is NOT
57266
- * stored in `sourceInfo`.
57267
- */
57268
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
57269
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
57270
- var EnumSensorDateTimeFormatSchema = _enum([
57992
+ _enum([
57271
57993
  "date",
57272
57994
  "time",
57273
57995
  "datetime"
@@ -57275,12 +57997,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
57275
57997
  object({
57276
57998
  value: string(),
57277
57999
  /**
57278
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
57279
- * as a locale date/time/datetime; absent for plain enum sensors. HA
57280
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
57281
- * a time-only sensor → `'time'`.
58000
+ * Set when `value` is a timestamp rather than an enum state, so history
58001
+ * does not learn ISO strings as states. The display shape (date / time /
58002
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
57282
58003
  */
57283
- format: EnumSensorDateTimeFormatSchema.optional(),
58004
+ timestamp: boolean().optional(),
57284
58005
  /** Ms epoch when the slice was last updated. */
57285
58006
  lastFetchedAt: number()
57286
58007
  });
@@ -57299,13 +58020,13 @@ var EventFireSchema = object({
57299
58020
  deviceId: number(),
57300
58021
  eventType: string(),
57301
58022
  data: record(string(), unknown()).nullable(),
57302
- timestamp: number(),
57303
- seq: number()
58023
+ timestamp: number().meta(readingMeta("none")),
58024
+ seq: number().meta(readingMeta("count"))
57304
58025
  });
57305
58026
  object({
57306
58027
  eventTypes: array(string()),
57307
58028
  lastEvent: EventFireSchema.nullable(),
57308
- eventCountSinceStart: number()
58029
+ eventCountSinceStart: number().meta(readingMeta("count"))
57309
58030
  });
57310
58031
  DeviceType.EventEmitter;
57311
58032
  var IdentitySchema = object({
@@ -57694,10 +58415,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
57694
58415
  object({
57695
58416
  /** Active speed as 0..100 inclusive. Null when the device has no
57696
58417
  * speed surface (single-speed fan). */
57697
- percentage: number().min(0).max(100).nullable(),
58418
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57698
58419
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
57699
58420
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
57700
- percentageStep: number().positive().optional(),
58421
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
57701
58422
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
57702
58423
  * — empty string when no preset is active or supported. */
57703
58424
  preset: string(),
@@ -57779,7 +58500,7 @@ var FeatureProbeStatusSchema = object({
57779
58500
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
57780
58501
  model: string().nullable(),
57781
58502
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
57782
- channelCount: number().nullable(),
58503
+ channelCount: number().nullable().meta(readingMeta("count")),
57783
58504
  /**
57784
58505
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
57785
58506
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -57811,9 +58532,45 @@ object({
57811
58532
  lastChangedAt: number()
57812
58533
  });
57813
58534
  DeviceType.Sensor;
58535
+ object({
58536
+ /** One row per member that has been commanded. A later command replaces that member's row. */
58537
+ results: array(object({
58538
+ deviceId: number().int().nonnegative(),
58539
+ cap: string().min(1),
58540
+ method: string().min(1),
58541
+ ok: boolean(),
58542
+ error: string().optional(),
58543
+ /** Ms epoch of the attempt. */
58544
+ at: number()
58545
+ })) });
58546
+ DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
58547
+ object({
58548
+ active: boolean().meta({ label: "Active" }),
58549
+ /** Ms epoch when the slice was last updated. */
58550
+ lastChangedAt: number()
58551
+ });
58552
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Climate, DeviceType.Thermostat;
58553
+ object({
58554
+ /** Members whose current value entered the aggregate. */
58555
+ included: number().int().nonnegative().meta(readingMeta("count", {
58556
+ precision: 0,
58557
+ label: "Reporting"
58558
+ })),
58559
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
58560
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
58561
+ precision: 0,
58562
+ label: "Excluded"
58563
+ })),
58564
+ /** `included` + `excluded`. */
58565
+ total: number().int().nonnegative().meta(readingMeta("count", {
58566
+ precision: 0,
58567
+ label: "Members"
58568
+ }))
58569
+ });
58570
+ DeviceType.Sensor, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
57814
58571
  object({
57815
58572
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
57816
- position: number().min(0).max(100).nullable(),
58573
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57817
58574
  /** Ms epoch when the slice was last updated (push or poll). */
57818
58575
  lastFetchedAt: number()
57819
58576
  });
@@ -57822,9 +58579,9 @@ object({
57822
58579
  /** Whether the humidifier is currently on. */
57823
58580
  on: boolean(),
57824
58581
  /** Current measured relative humidity (0..100). Null when not reported. */
57825
- currentHumidity: number().min(0).max(100).nullable(),
58582
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57826
58583
  /** Target relative humidity (0..100). Null when no setpoint surface. */
57827
- targetHumidity: number().min(0).max(100).nullable(),
58584
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57828
58585
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
57829
58586
  * device has no mode surface. */
57830
58587
  mode: string().nullable(),
@@ -57834,9 +58591,9 @@ object({
57834
58591
  * `idle` / `off`). Null when the device doesn't report it. */
57835
58592
  action: string().nullable(),
57836
58593
  /** HA `min_humidity` attribute. Null → UI uses 0. */
57837
- minHumidity: number().nullable(),
58594
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
57838
58595
  /** HA `max_humidity` attribute. Null → UI uses 100. */
57839
- maxHumidity: number().nullable(),
58596
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
57840
58597
  /** Ms epoch when the slice was last updated. */
57841
58598
  lastChangedAt: number()
57842
58599
  });
@@ -57860,22 +58617,13 @@ DeviceType.Humidifier, method(object({
57860
58617
  auth: "protected"
57861
58618
  });
57862
58619
  object({
57863
- /** Current relative humidity, 0..100. */
57864
- percent: number().min(0).max(100).meta({
57865
- unit: "%",
58620
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
58621
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
58622
+ precision: 0,
57866
58623
  label: "Humidity"
57867
- }),
58624
+ })),
57868
58625
  /** Ms epoch when the slice was last updated. */
57869
- lastFetchedAt: number(),
57870
- /** Live display unit from the upstream source (e.g. HA
57871
- * `attributes.unit_of_measurement`). The UI prefers this over the
57872
- * role's canonical unit. Absent → fall back to the canonical unit. */
57873
- unit: string().optional(),
57874
- /** Suggested decimal places for numeric display.
57875
- * Populated live from the upstream source when provided (e.g. HA
57876
- * `attributes.suggested_display_precision`). Falls back to
57877
- * auto-formatting when absent. */
57878
- precision: number().int().min(0).max(10).optional()
58626
+ lastFetchedAt: number()
57879
58627
  });
57880
58628
  DeviceType.Sensor, DeviceType.Thermostat;
57881
58629
  object({
@@ -57883,7 +58631,7 @@ object({
57883
58631
  * entity exposes no `entity_picture` (yet). */
57884
58632
  url: string().nullable(),
57885
58633
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
57886
- lastUpdated: number().nullable()
58634
+ lastUpdated: number().nullable().meta(readingMeta("none"))
57887
58635
  });
57888
58636
  DeviceType.Image;
57889
58637
  /**
@@ -57939,19 +58687,19 @@ var NormalizedRangeSchema = object({
57939
58687
  });
57940
58688
  object({
57941
58689
  /** Normalized 0–100. */
57942
- brightness: number().optional(),
58690
+ brightness: number().optional().meta(readingMeta("none")),
57943
58691
  /** Normalized 0–100. */
57944
- contrast: number().optional(),
58692
+ contrast: number().optional().meta(readingMeta("none")),
57945
58693
  /** Normalized 0–100. */
57946
- saturation: number().optional(),
58694
+ saturation: number().optional().meta(readingMeta("none")),
57947
58695
  /** Normalized 0–100. */
57948
- sharpness: number().optional(),
58696
+ sharpness: number().optional().meta(readingMeta("none")),
57949
58697
  mirror: boolean().optional(),
57950
58698
  flip: boolean().optional(),
57951
58699
  rotate: ImageRotateSchema.optional(),
57952
58700
  whiteBalance: WhiteBalanceModeSchema.optional(),
57953
58701
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
57954
- warmth: number().optional(),
58702
+ warmth: number().optional().meta(readingMeta("none")),
57955
58703
  exposureMode: ExposureModeSchema.optional(),
57956
58704
  backlightMode: BacklightModeSchema.optional(),
57957
58705
  lastFetchedAt: number()
@@ -58196,10 +58944,10 @@ var IntercomAbilitySchema = object({
58196
58944
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
58197
58945
  codecs: array(string()),
58198
58946
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
58199
- sampleRate: number(),
58947
+ sampleRate: number().meta(readingMeta("none")),
58200
58948
  duplex: _enum(["half", "full"]),
58201
58949
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
58202
- maxBacklogMs: number()
58950
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
58203
58951
  });
58204
58952
  var IntercomStatusSchema = object({
58205
58953
  /** True while a talk session is open. */
@@ -58310,13 +59058,13 @@ object({
58310
59058
  activity: LawnMowerActivitySchema,
58311
59059
  /** 0..100 battery percentage. Null when the device has no battery
58312
59060
  * reading. */
58313
- batteryLevel: number().min(0).max(100).nullable(),
59061
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58314
59062
  /** 0..100 mowing-completion percentage of the current task, or null when no
58315
59063
  * task is active / progress is unavailable. */
58316
- progressPercent: number().min(0).max(100).nullable(),
59064
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58317
59065
  /** Current device/event code (dynamic — mostly status, sometimes an error),
58318
59066
  * or null when unknown. */
58319
- currentCode: number().nullable(),
59067
+ currentCode: number().nullable().meta(readingMeta("none")),
58320
59068
  /** Human label for {@link currentCode}, or null when undecodable. */
58321
59069
  currentCodeLabel: string().nullable(),
58322
59070
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -58726,7 +59474,14 @@ DeviceType.Lock, method(object({
58726
59474
  auth: "protected"
58727
59475
  }), method(object({
58728
59476
  deviceId: number().int().nonnegative(),
58729
- code: string().min(1).optional()
59477
+ code: string().min(1).optional(),
59478
+ /**
59479
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
59480
+ * with one call is refused unless the caller says it was confirmed. A
59481
+ * single lock ignores it. The refusal lives in the callee, so no
59482
+ * automation or API caller reaches the doors by skipping a dialog.
59483
+ */
59484
+ confirm: boolean().optional()
58730
59485
  }), _void(), {
58731
59486
  kind: "mutation",
58732
59487
  auth: "protected"
@@ -58734,6 +59489,11 @@ DeviceType.Lock, method(object({
58734
59489
  kind: "mutation",
58735
59490
  auth: "protected"
58736
59491
  });
59492
+ object({
59493
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
59494
+ lastFetchedAt: number()
59495
+ });
59496
+ DeviceType.Sensor;
58737
59497
  /**
58738
59498
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
58739
59499
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
@@ -58778,7 +59538,7 @@ object({
58778
59538
  state: MediaPlayerStateSchema,
58779
59539
  /** Volume as 0..100 inclusive. Null when the device has no volume
58780
59540
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
58781
- volumeLevel: number().min(0).max(100).nullable(),
59541
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58782
59542
  /** Mute toggle distinct from volume=0. Null when no mute surface.
58783
59543
  * Pair with `DeviceFeature.MediaPlayerMute`. */
58784
59544
  isMuted: boolean().nullable(),
@@ -58791,10 +59551,10 @@ object({
58791
59551
  currentMedia: MediaInfoSchema.nullable(),
58792
59552
  /** Current playback position in ms. Null when not seekable or
58793
59553
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
58794
- positionMs: number().int().nonnegative().nullable(),
59554
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
58795
59555
  /** Total duration of the current media in ms. Null when unknown
58796
59556
  * (live stream) or nothing is playing. */
58797
- durationMs: number().int().nonnegative().nullable(),
59557
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
58798
59558
  /** Shuffle toggle. Null when no shuffle surface.
58799
59559
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
58800
59560
  shuffle: boolean().nullable(),
@@ -59094,7 +59854,7 @@ object({
59094
59854
  * it, every provider seeded it at boot, and every session left it
59095
59855
  * `detected: true` after its detach.
59096
59856
  */
59097
- autoClearAfterMs: number().nullable()
59857
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
59098
59858
  });
59099
59859
  /**
59100
59860
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -59144,7 +59904,7 @@ var MotionZoneRegionSchema = object({
59144
59904
  });
59145
59905
  object({
59146
59906
  enabled: boolean(),
59147
- sensitivity: number(),
59907
+ sensitivity: number().meta(readingMeta("none")),
59148
59908
  /** Grid region(s). Today exactly one `grid` shape. */
59149
59909
  regions: array(MotionZoneRegionSchema),
59150
59910
  lastFetchedAt: number()
@@ -59448,7 +60208,7 @@ var NavigationStatusSchema = object({
59448
60208
  following: boolean(),
59449
60209
  flash: boolean(),
59450
60210
  lightMode: NavigationLightModeSchema,
59451
- lightLevel: number().min(40).max(100),
60211
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
59452
60212
  /** Ms epoch when the slice was last updated. */
59453
60213
  lastChangedAt: number()
59454
60214
  });
@@ -59494,13 +60254,13 @@ var NetworkLinkStatusSchema = object({
59494
60254
  * one whose reading has not landed must not be drawn at 0 %. Consumers
59495
60255
  * SKIP a null rather than coerce it.
59496
60256
  */
59497
- signalPercent: number().min(0).max(100).nullable(),
60257
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59498
60258
  /** Raw received signal strength in dBm, when the firmware reports one. */
59499
- rssiDbm: number().optional(),
60259
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
59500
60260
  /** Network name of a wireless link, when the firmware reports it. */
59501
60261
  ssid: string().optional(),
59502
60262
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
59503
- lastUpdated: number()
60263
+ lastUpdated: number().meta(readingMeta("none"))
59504
60264
  });
59505
60265
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
59506
60266
  deviceId: number(),
@@ -59819,7 +60579,7 @@ object({
59819
60579
  lastError: string().nullable(),
59820
60580
  /** Number of deliveries currently buffered server-side (rate-limited
59821
60581
  * notifiers may queue). 0 when the provider sends synchronously. */
59822
- queueDepth: number().int().nonnegative(),
60582
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
59823
60583
  /** Per-feature capability matrix — authoritative source for the
59824
60584
  * compose-form field gating. */
59825
60585
  supports: NotifierSupportsSchema
@@ -59869,15 +60629,8 @@ DeviceType.Notifier, method(NotifierSendInputSchema, NotifierSendResultSchema, {
59869
60629
  acceptedAt: number()
59870
60630
  });
59871
60631
  object({
59872
- value: number(),
59873
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
59874
- * Populated live from the upstream source on each state push.
59875
- * Absent for unitless sensors. */
59876
- unit: string().optional(),
59877
- /** Suggested decimal places for numeric display.
59878
- * Populated live from the upstream source when provided.
59879
- * Falls back to auto-formatting when absent. */
59880
- precision: number().int().min(0).max(10).optional(),
60632
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
60633
+ value: number().nullable().meta(readingMeta("device")),
59881
60634
  /** Ms epoch when the slice was last updated. */
59882
60635
  lastFetchedAt: number()
59883
60636
  });
@@ -60167,23 +60920,26 @@ object({
60167
60920
  /** Food currently in the bowl (grams). Null when the device has not
60168
60921
  * reported a reading yet. On dual-hopper models this is the combined
60169
60922
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
60170
- foodLevel: number().nullable(),
60923
+ foodLevel: number().nullable().meta(readingMeta("none")),
60171
60924
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
60172
60925
  * single-hopper models. */
60173
- food1: number().nullable(),
60926
+ food1: number().nullable().meta(readingMeta("none")),
60174
60927
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
60175
60928
  * single-hopper models. */
60176
- food2: number().nullable(),
60929
+ food2: number().nullable().meta(readingMeta("none")),
60177
60930
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
60178
60931
  * (`device_class: problem`, on = low). True when the bowl is empty /
60179
60932
  * below the feeder's low threshold. */
60180
60933
  lowFood: boolean(),
60181
60934
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
60182
60935
  * device has no battery reading. */
60183
- batteryPower: number().min(0).max(100).nullable(),
60184
- /** Days of desiccant life remaining. Null when the model has no
60185
- * desiccant sensor. */
60186
- desiccantLeftDays: number().nullable(),
60936
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60937
+ /**
60938
+ * Desiccant life remaining, stored in seconds (duration base, D944).
60939
+ * Providers convert from the days the feeder reports. Null when the
60940
+ * model has no desiccant sensor.
60941
+ */
60942
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
60187
60943
  /** True while a feed is in progress. */
60188
60944
  feeding: boolean(),
60189
60945
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -60194,7 +60950,7 @@ object({
60194
60950
  * with `errorCode` for consumers that want the raw integer. */
60195
60951
  error: string().nullable(),
60196
60952
  /** Raw device error code (0 / null = no error). */
60197
- errorCode: number().nullable(),
60953
+ errorCode: number().nullable().meta(readingMeta("none")),
60198
60954
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
60199
60955
  isDualHopper: boolean(),
60200
60956
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -60204,7 +60960,7 @@ object({
60204
60960
  /** Play a chime when dispensing. */
60205
60961
  feedSound: boolean(),
60206
60962
  /** Speaker / prompt volume level (device-scaled integer). */
60207
- volume: number(),
60963
+ volume: number().meta(readingMeta("none")),
60208
60964
  /** Ms epoch when the slice was last refreshed from the cloud. */
60209
60965
  lastFetchedAt: number()
60210
60966
  });
@@ -60641,38 +61397,37 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
60641
61397
  });
60642
61398
  object({
60643
61399
  /** Instantaneous power draw in watts. */
60644
- watts: number().optional().meta({
60645
- unit: "W",
60646
- label: "Power"
60647
- }),
61400
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
60648
61401
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
60649
- kwhTotal: number().optional().meta({
60650
- unit: "kWh",
60651
- label: "Energy"
60652
- }),
61402
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
60653
61403
  /** Voltage in volts. */
60654
- volts: number().optional(),
61404
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
60655
61405
  /** Current in amperes. */
60656
- amps: number().optional(),
60657
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
60658
- va: number().optional(),
61406
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
61407
+ /** Apparent power in volt-amperes. */
61408
+ va: number().optional().meta(readingMeta("apparent-power")),
60659
61409
  /** Power factor (0..1) if reported. */
60660
- powerFactor: number().min(0).max(1).optional(),
61410
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
60661
61411
  /** Ms epoch when the slice was last updated. */
60662
- lastFetchedAt: number(),
60663
- /** Live display unit of the single metric this slice carries (e.g. HA
60664
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
60665
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
60666
- * unit per slice is unambiguous. The UI prefers this over the role's
60667
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
60668
- unit: string().optional(),
60669
- /** Suggested decimal places for numeric display.
60670
- * Populated live from the upstream source when provided (e.g. HA
60671
- * `attributes.suggested_display_precision`). Falls back to
60672
- * auto-formatting when absent. */
60673
- precision: number().int().min(0).max(10).optional()
61412
+ lastFetchedAt: number()
60674
61413
  });
60675
- DeviceType.Sensor;
61414
+ [
61415
+ DeviceType.Sensor,
61416
+ DeviceType.Switch,
61417
+ DeviceType.Light,
61418
+ DeviceType.Fan,
61419
+ DeviceType.Climate,
61420
+ DeviceType.Thermostat,
61421
+ DeviceType.Humidifier,
61422
+ DeviceType.WaterHeater,
61423
+ DeviceType.MediaPlayer,
61424
+ DeviceType.Washer,
61425
+ DeviceType.Dryer,
61426
+ DeviceType.Dishwasher,
61427
+ DeviceType.Vacuum,
61428
+ DeviceType.LawnMower,
61429
+ DeviceType.PetFeeder
61430
+ ].filter((type) => type !== DeviceType.Sensor);
60676
61431
  /**
60677
61432
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
60678
61433
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -60691,11 +61446,11 @@ DeviceType.Sensor;
60691
61446
  */
60692
61447
  var GpsLocationSchema = object({
60693
61448
  /** Latitude in decimal degrees, -90..90. */
60694
- latitude: number().min(-90).max(90),
61449
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
60695
61450
  /** Longitude in decimal degrees, -180..180. */
60696
- longitude: number().min(-180).max(180),
61451
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
60697
61452
  /** Reported accuracy in meters (lower = better). */
60698
- accuracyMeters: number().nonnegative()
61453
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
60699
61454
  });
60700
61455
  object({
60701
61456
  /** `home` / `not_home` / any user-defined zone name. */
@@ -60707,25 +61462,16 @@ object({
60707
61462
  gps: GpsLocationSchema.nullable(),
60708
61463
  /** Optional battery level of the tracking device (0..100). Null
60709
61464
  * when not reported. */
60710
- batteryPercent: number().min(0).max(100).nullable(),
61465
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60711
61466
  /** Ms epoch when the slice was last updated. */
60712
61467
  lastChangedAt: number()
60713
61468
  });
60714
61469
  DeviceType.Presence;
60715
61470
  object({
60716
- /** Current pressure in hPa. */
60717
- hpa: number(),
61471
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
61472
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
60718
61473
  /** Ms epoch when the slice was last updated. */
60719
- lastFetchedAt: number(),
60720
- /** Live display unit from the upstream source (e.g. HA
60721
- * `attributes.unit_of_measurement`). The UI prefers this over the
60722
- * role's canonical unit. Absent → fall back to the canonical unit. */
60723
- unit: string().optional(),
60724
- /** Suggested decimal places for numeric display.
60725
- * Populated live from the upstream source when provided (e.g. HA
60726
- * `attributes.suggested_display_precision`). Falls back to
60727
- * auto-formatting when absent. */
60728
- precision: number().int().min(0).max(10).optional()
61474
+ lastFetchedAt: number()
60729
61475
  });
60730
61476
  DeviceType.Sensor;
60731
61477
  /**
@@ -61058,8 +61804,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
61058
61804
  }), _void(), { kind: "mutation" });
61059
61805
  var PtzAutotrackSettingsSchema = object({
61060
61806
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
61061
- stopDelaySeconds: number().int().min(0).max(300),
61062
- disappearDelaySeconds: number().int().min(0).max(300)
61807
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
61808
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
61063
61809
  });
61064
61810
  /**
61065
61811
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -61116,7 +61862,7 @@ object({
61116
61862
  /** null = unknown — never coerced to false (D393). */
61117
61863
  running: boolean().nullable(),
61118
61864
  /** Epoch ms the current boolean began; null while `running` is null. */
61119
- since: number().nullable(),
61865
+ since: number().nullable().meta(readingMeta("none")),
61120
61866
  source: RunningStateSourceSchema.meta({ label: "Source" }),
61121
61867
  phase: string().nullable(),
61122
61868
  /** null until a reading has been published (D393). */
@@ -61128,7 +61874,7 @@ object({
61128
61874
  phase: string().nullable()
61129
61875
  });
61130
61876
  Object.values(DeviceType);
61131
- DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
61877
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
61132
61878
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
61133
61879
  kind: "mutation",
61134
61880
  auth: "admin"
@@ -62314,9 +63060,9 @@ var RecordTriggerSchema = _enum([
62314
63060
  */
62315
63061
  var RecordWindowSchema = object({
62316
63062
  trigger: RecordTriggerSchema,
62317
- day: number().int().min(0).max(6),
62318
- startMinute: number().int().min(0).max(1439),
62319
- endMinute: number().int().min(1).max(1440)
63063
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
63064
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
63065
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
62320
63066
  });
62321
63067
  /** Status of one physical volume, as the camera itself describes it. */
62322
63068
  var OnboardStorageVolumeSchema = object({
@@ -62385,10 +63131,10 @@ object({
62385
63131
  /** Master "record to the card at all" switch. */
62386
63132
  enabled: boolean().nullable(),
62387
63133
  overwriteWhenFull: boolean().nullable(),
62388
- preRecordSec: number().nullable(),
62389
- postRecordSec: number().nullable(),
63134
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
63135
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
62390
63136
  /** Length of one recorded file, in minutes. */
62391
- segmentMinutes: number().nullable(),
63137
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
62392
63138
  /** The primary track's weekly windows, flattened. */
62393
63139
  windows: array(RecordWindowSchema),
62394
63140
  /**
@@ -62404,7 +63150,7 @@ object({
62404
63150
  * provider reporting a non-zero count also reports the schedule as not
62405
63151
  * writable.
62406
63152
  */
62407
- unreadableWindows: number(),
63153
+ unreadableWindows: number().meta(readingMeta("count")),
62408
63154
  /**
62409
63155
  * The camera is scheduled to record and has NO usable storage.
62410
63156
  *
@@ -63041,7 +63787,7 @@ var CamStreamDescriptorSchema = object({
63041
63787
  url: string().optional(),
63042
63788
  codec: string().optional(),
63043
63789
  resolution: CamStreamResolutionSchema.optional(),
63044
- fps: number().positive().optional(),
63790
+ fps: number().positive().optional().meta(readingMeta("rate")),
63045
63791
  label: string().optional(),
63046
63792
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
63047
63793
  deviceFeatures: array(string()).optional(),
@@ -63071,18 +63817,18 @@ var StreamProfileSchema = _enum([
63071
63817
  ]);
63072
63818
  /** The current encoding config of one stream profile. */
63073
63819
  var StreamProfileConfigSchema = object({
63074
- width: number(),
63075
- height: number(),
63820
+ width: number().meta(readingMeta("none")),
63821
+ height: number().meta(readingMeta("none")),
63076
63822
  codec: _enum(["h264", "h265"]),
63077
- framerate: number(),
63078
- bitrate: number(),
63823
+ framerate: number().meta(readingMeta("rate")),
63824
+ bitrate: number().meta(readingMeta("bitrate")),
63079
63825
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
63080
63826
  encoderProfile: _enum([
63081
63827
  "high",
63082
63828
  "main",
63083
63829
  "baseline"
63084
63830
  ]).optional(),
63085
- gop: number().optional(),
63831
+ gop: number().optional().meta(readingMeta("none")),
63086
63832
  /**
63087
63833
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
63088
63834
  *
@@ -63167,7 +63913,7 @@ object({
63167
63913
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
63168
63914
  lastChangedAt: number()
63169
63915
  });
63170
- DeviceType.Switch, DeviceType.Siren, DeviceType.Light, method(object({
63916
+ DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, method(object({
63171
63917
  deviceId: number(),
63172
63918
  on: boolean()
63173
63919
  }), _void(), {
@@ -63725,23 +64471,13 @@ object({
63725
64471
  });
63726
64472
  DeviceType.Sensor;
63727
64473
  object({
63728
- /** Current temperature in Celsius. */
63729
- celsius: number().meta({
63730
- unit: "°C",
64474
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
64475
+ celsius: number().nullable().meta(readingMeta("temperature", {
64476
+ precision: 1,
63731
64477
  label: "Temperature"
63732
- }),
64478
+ })),
63733
64479
  /** Ms epoch when the slice was last updated (push or poll). */
63734
- lastFetchedAt: number(),
63735
- /** Live display unit from the upstream source (e.g. HA
63736
- * `attributes.unit_of_measurement`). The UI prefers this over the
63737
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
63738
- * Absent → the UI falls back to the role's canonical unit. */
63739
- unit: string().optional(),
63740
- /** Suggested decimal places for numeric display.
63741
- * Populated live from the upstream source when provided (e.g. HA
63742
- * `attributes.suggested_display_precision`). Falls back to
63743
- * auto-formatting when absent. */
63744
- precision: number().int().min(0).max(10).optional()
64480
+ lastFetchedAt: number()
63745
64481
  });
63746
64482
  DeviceType.Sensor, DeviceType.Thermostat;
63747
64483
  /**
@@ -63993,16 +64729,16 @@ object({
63993
64729
  ]),
63994
64730
  /** 0..100 battery percentage. Null when the device has no battery
63995
64731
  * reading. */
63996
- batteryLevel: number().min(0).max(100).nullable(),
64732
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63997
64733
  /** Current fan-speed token (provider-verbatim). Null when unknown or
63998
64734
  * the vacuum has no speed control. */
63999
64735
  fanSpeed: string().nullable(),
64000
64736
  /** Speed tokens the hardware accepts — drives the UI selector. */
64001
64737
  availableFanSpeeds: array(string()),
64002
64738
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64003
- progressPercent: number().min(0).max(100).nullable(),
64739
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64004
64740
  /** Current error code (0 / null = no error). */
64005
- errorCode: number().nullable(),
64741
+ errorCode: number().nullable().meta(readingMeta("none")),
64006
64742
  /** Human label for {@link errorCode}, or null when none / undecodable. */
64007
64743
  errorLabel: string().nullable(),
64008
64744
  /** Ms epoch when the slice was last updated. */
@@ -64041,7 +64777,7 @@ object({
64041
64777
  ]),
64042
64778
  /** 0 = fully closed, 100 = fully open. Null when the device has no
64043
64779
  * intermediate position surface. */
64044
- position: number().min(0).max(100).nullable(),
64780
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64045
64781
  /** Ms epoch when the slice was last updated. */
64046
64782
  lastChangedAt: number()
64047
64783
  });
@@ -64069,9 +64805,9 @@ object({
64069
64805
  DeviceType.Sensor;
64070
64806
  object({
64071
64807
  /** Current measured temperature. Null when not reported. */
64072
- currentTemp: number().nullable(),
64808
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
64073
64809
  /** Target temperature setpoint. Null when no setpoint surface. */
64074
- targetTemp: number().nullable(),
64810
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
64075
64811
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
64076
64812
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
64077
64813
  * device reports an unknown state. */
@@ -64082,9 +64818,9 @@ object({
64082
64818
  * has no away surface. */
64083
64819
  away: boolean().nullable(),
64084
64820
  /** HA `min_temp` attribute. Null when not reported. */
64085
- minTemp: number().nullable(),
64821
+ minTemp: number().nullable().meta(readingMeta("temperature")),
64086
64822
  /** HA `max_temp` attribute. Null when not reported. */
64087
- maxTemp: number().nullable(),
64823
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
64088
64824
  /** Ms epoch when the slice was last updated. */
64089
64825
  lastChangedAt: number()
64090
64826
  });
@@ -64108,25 +64844,18 @@ DeviceType.WaterHeater, method(object({
64108
64844
  auth: "protected"
64109
64845
  });
64110
64846
  object({
64111
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
64112
- * when no condition has been reported yet. */
64847
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
64113
64848
  condition: string().nullable(),
64114
- /** Current temperature in the reported unit. Null when not provided. */
64115
- temperature: number().nullable(),
64116
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
64117
- temperatureUnit: string().nullable(),
64849
+ /** Current temperature in Celsius. Null when not provided or refused. */
64850
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
64118
64851
  /** Relative humidity (0..100). Null when not provided. */
64119
- humidity: number().min(0).max(100).nullable(),
64120
- /** Barometric pressure in the reported unit. Null when not provided. */
64121
- pressure: number().nullable(),
64122
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
64123
- pressureUnit: string().nullable(),
64124
- /** Wind speed in the reported unit. Null when not provided. */
64125
- windSpeed: number().nullable(),
64126
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
64127
- windSpeedUnit: string().nullable(),
64852
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
64853
+ /** Barometric pressure in hPa. Null when not provided or refused. */
64854
+ pressure: number().nullable().meta(readingMeta("pressure")),
64855
+ /** Wind speed in metres per second. Null when not provided or refused. */
64856
+ windSpeed: number().nullable().meta(readingMeta("speed")),
64128
64857
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
64129
- windBearing: number().nullable(),
64858
+ windBearing: number().nullable().meta(readingMeta("angle")),
64130
64859
  /** Ms epoch when the slice was last updated. */
64131
64860
  lastFetchedAt: number()
64132
64861
  });
@@ -64876,8 +65605,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
64876
65605
  kind: "mutation",
64877
65606
  auth: "admin"
64878
65607
  });
65608
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
64879
65609
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
64880
- 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;
65610
+ 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;
64881
65611
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
64882
65612
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
64883
65613
  new Set(CAP_NAMES);
@@ -64930,7 +65660,7 @@ var AccessoryKind = {
64930
65660
  PrivacyMask: DeviceRole.PrivacyMask
64931
65661
  };
64932
65662
  AccessoryKind.Siren, AccessoryKind.Floodlight, AccessoryKind.Spotlight, AccessoryKind.PirSensor, AccessoryKind.Chime, AccessoryKind.Autotrack, AccessoryKind.Nightvision, AccessoryKind.PrivacyMask;
64933
- 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;
65663
+ 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;
64934
65664
  new Set(Object.values(DeviceType));
64935
65665
  Object.freeze({
64936
65666
  "accessories.getStatus": {
@@ -65038,6 +65768,13 @@ Object.freeze({
65038
65768
  access: "view",
65039
65769
  role: "admin"
65040
65770
  },
65771
+ "addons.getPackageDocs": {
65772
+ capName: "addons",
65773
+ capScope: "system",
65774
+ addonId: null,
65775
+ access: "view",
65776
+ role: "admin"
65777
+ },
65041
65778
  "addons.getVersions": {
65042
65779
  capName: "addons",
65043
65780
  capScope: "system",
@@ -68146,6 +68883,27 @@ Object.freeze({
68146
68883
  access: "view",
68147
68884
  role: "viewer"
68148
68885
  },
68886
+ "groupActive.getStatus": {
68887
+ capName: "group-active",
68888
+ capScope: "device",
68889
+ addonId: null,
68890
+ access: "view",
68891
+ role: "viewer"
68892
+ },
68893
+ "groupCommands.getStatus": {
68894
+ capName: "group-commands",
68895
+ capScope: "device",
68896
+ addonId: null,
68897
+ access: "view",
68898
+ role: "viewer"
68899
+ },
68900
+ "groupStatus.getStatus": {
68901
+ capName: "group-status",
68902
+ capScope: "device",
68903
+ addonId: null,
68904
+ access: "view",
68905
+ role: "viewer"
68906
+ },
68149
68907
  "heatingValve.getStatus": {
68150
68908
  capName: "heating-valve",
68151
68909
  capScope: "device",
@@ -69098,6 +69856,13 @@ Object.freeze({
69098
69856
  access: "create",
69099
69857
  role: "admin"
69100
69858
  },
69859
+ "meter.getStatus": {
69860
+ capName: "meter",
69861
+ capScope: "device",
69862
+ addonId: null,
69863
+ access: "view",
69864
+ role: "viewer"
69865
+ },
69101
69866
  "metricsProvider.collectSnapshot": {
69102
69867
  capName: "metrics-provider",
69103
69868
  capScope: "system",
@@ -77196,7 +77961,39 @@ var APPLIANCE_CAP_VOCABULARY = {
77196
77961
  phrase: t("When {device} finishes", "Quando {device} finisce"),
77197
77962
  icon: "square"
77198
77963
  }
77199
- }
77964
+ },
77965
+ overrides: { byType: {
77966
+ [DeviceType.Washer]: {
77967
+ state: { running: {
77968
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
77969
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
77970
+ } },
77971
+ events: {
77972
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
77973
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
77974
+ }
77975
+ },
77976
+ [DeviceType.Dryer]: {
77977
+ state: { running: {
77978
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
77979
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
77980
+ } },
77981
+ events: {
77982
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
77983
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
77984
+ }
77985
+ },
77986
+ [DeviceType.Dishwasher]: {
77987
+ state: { running: {
77988
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
77989
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
77990
+ } },
77991
+ events: {
77992
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
77993
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
77994
+ }
77995
+ }
77996
+ } }
77200
77997
  },
77201
77998
  update: {
77202
77999
  scope: "device",
@@ -77968,6 +78765,41 @@ var SENSOR_CAP_VOCABULARY = {
77968
78765
  icon: "flame"
77969
78766
  }]
77970
78767
  },
78768
+ "group-active": {
78769
+ scope: "device",
78770
+ noun: t("Group state", "Stato del gruppo")
78771
+ },
78772
+ "group-commands": {
78773
+ scope: "device",
78774
+ noun: t("Group command", "Comando di gruppo")
78775
+ },
78776
+ "group-status": {
78777
+ scope: "device",
78778
+ noun: t("Group", "Gruppo"),
78779
+ state: [
78780
+ {
78781
+ kind: "numeric",
78782
+ field: "included",
78783
+ quantity: t("the members reporting", "i membri che rispondono"),
78784
+ label: t("Reporting", "Rispondono"),
78785
+ icon: "users"
78786
+ },
78787
+ {
78788
+ kind: "numeric",
78789
+ field: "excluded",
78790
+ quantity: t("the members left out", "i membri esclusi"),
78791
+ label: t("Excluded", "Esclusi"),
78792
+ icon: "users"
78793
+ },
78794
+ {
78795
+ kind: "numeric",
78796
+ field: "total",
78797
+ quantity: t("the member count", "il numero di membri"),
78798
+ label: t("Members", "Membri"),
78799
+ icon: "sigma"
78800
+ }
78801
+ ]
78802
+ },
77971
78803
  "humidity-sensor": {
77972
78804
  scope: "device",
77973
78805
  noun: t("Humidity", "Umidità"),
@@ -77993,6 +78825,17 @@ var SENSOR_CAP_VOCABULARY = {
77993
78825
  }],
77994
78826
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
77995
78827
  },
78828
+ meter: {
78829
+ scope: "device",
78830
+ noun: t("Meter", "Contatore"),
78831
+ state: [{
78832
+ kind: "numeric",
78833
+ field: "value",
78834
+ quantity: t("the total", "il totale"),
78835
+ label: t("Total", "Totale"),
78836
+ icon: "gauge"
78837
+ }]
78838
+ },
77996
78839
  "numeric-sensor": {
77997
78840
  scope: "device",
77998
78841
  noun: t("Sensor", "Sensore"),
@@ -78001,7 +78844,6 @@ var SENSOR_CAP_VOCABULARY = {
78001
78844
  field: "value",
78002
78845
  quantity: t("the reading", "il valore"),
78003
78846
  label: t("Reading", "Valore"),
78004
- unitField: "unit",
78005
78847
  icon: "gauge"
78006
78848
  }]
78007
78849
  },
@@ -78137,7 +78979,7 @@ var SENSOR_CAP_VOCABULARY = {
78137
78979
  field: "temperature",
78138
78980
  quantity: t("the outdoor temperature", "la temperatura esterna"),
78139
78981
  label: t("Temperature", "Temperatura"),
78140
- unitField: "temperatureUnit",
78982
+ unit: "°C",
78141
78983
  icon: "thermometer"
78142
78984
  },
78143
78985
  {
@@ -78153,7 +78995,7 @@ var SENSOR_CAP_VOCABULARY = {
78153
78995
  field: "windSpeed",
78154
78996
  quantity: t("the wind speed", "la velocità del vento"),
78155
78997
  label: t("Wind", "Vento"),
78156
- unitField: "windSpeedUnit",
78998
+ unit: "m/s",
78157
78999
  icon: "wind"
78158
79000
  },
78159
79001
  {
@@ -78161,7 +79003,7 @@ var SENSOR_CAP_VOCABULARY = {
78161
79003
  field: "pressure",
78162
79004
  quantity: t("the air pressure", "la pressione atmosferica"),
78163
79005
  label: t("Pressure", "Pressione"),
78164
- unitField: "pressureUnit",
79006
+ unit: "hPa",
78165
79007
  icon: "gauge"
78166
79008
  }
78167
79009
  ]
@@ -78466,8 +79308,47 @@ function projectAction(cap, action) {
78466
79308
  };
78467
79309
  }
78468
79310
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
78469
- 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;
79311
+ 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;
78470
79312
  DeviceFeature.BatteryOperated;
79313
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
79314
+ 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.";
79315
+ 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.";
79316
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
79317
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
79318
+ 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.";
79319
+ function withheld(reason) {
79320
+ return {
79321
+ [DeviceType.Washer]: {
79322
+ exported: false,
79323
+ reason
79324
+ },
79325
+ [DeviceType.Dryer]: {
79326
+ exported: false,
79327
+ reason
79328
+ },
79329
+ [DeviceType.Dishwasher]: {
79330
+ exported: false,
79331
+ reason
79332
+ }
79333
+ };
79334
+ }
79335
+ function offered(received) {
79336
+ return {
79337
+ [DeviceType.Washer]: {
79338
+ exported: true,
79339
+ as: received
79340
+ },
79341
+ [DeviceType.Dryer]: {
79342
+ exported: true,
79343
+ as: received
79344
+ },
79345
+ [DeviceType.Dishwasher]: {
79346
+ exported: true,
79347
+ as: received
79348
+ }
79349
+ };
79350
+ }
79351
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
78471
79352
  Object.freeze({
78472
79353
  "accessories.getStatus": [{
78473
79354
  name: "deviceId",
@@ -79434,6 +80315,21 @@ Object.freeze({
79434
80315
  form: "single",
79435
80316
  optional: false
79436
80317
  }],
80318
+ "groupActive.getStatus": [{
80319
+ name: "deviceId",
80320
+ form: "single",
80321
+ optional: false
80322
+ }],
80323
+ "groupCommands.getStatus": [{
80324
+ name: "deviceId",
80325
+ form: "single",
80326
+ optional: false
80327
+ }],
80328
+ "groupStatus.getStatus": [{
80329
+ name: "deviceId",
80330
+ form: "single",
80331
+ optional: false
80332
+ }],
79437
80333
  "heatingValve.getStatus": [{
79438
80334
  name: "deviceId",
79439
80335
  form: "single",
@@ -79634,6 +80530,11 @@ Object.freeze({
79634
80530
  form: "single",
79635
80531
  optional: false
79636
80532
  }],
80533
+ "meter.getStatus": [{
80534
+ name: "deviceId",
80535
+ form: "single",
80536
+ optional: false
80537
+ }],
79637
80538
  "motion.getStatus": [{
79638
80539
  name: "deviceId",
79639
80540
  form: "single",
@@ -81241,7 +82142,6 @@ Object.freeze({
81241
82142
  "network-access": "ingress",
81242
82143
  "smtp-provider": "email"
81243
82144
  });
81244
- DeviceType.Camera;
81245
82145
  /**
81246
82146
  * A saved Customize assignment (D936). The schema is the type: callers use
81247
82147
  * `z.infer`, they do not restate the shape.
@@ -81418,6 +82318,8 @@ new Map(Object.entries({
81418
82318
  "detection-blind": "device-events",
81419
82319
  "motion-noisy": "device-events",
81420
82320
  "motion-calm": "device-events",
82321
+ "package-llm-silent": "device-events",
82322
+ "package-llm-answered": "device-events",
81421
82323
  "recording-stalled": "device-events",
81422
82324
  "recording-healthy": "device-events",
81423
82325
  "recording-degraded": "device-events",
@@ -81501,6 +82403,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
81501
82403
  label: "Motion without a subject",
81502
82404
  kinds: ["motion-noisy", "motion-calm"]
81503
82405
  },
82406
+ {
82407
+ id: "package-llm",
82408
+ family: "device-events",
82409
+ label: "Package confirm",
82410
+ kinds: ["package-llm-silent", "package-llm-answered"]
82411
+ },
81504
82412
  {
81505
82413
  id: "recording",
81506
82414
  family: "device-events",
@@ -81682,6 +82590,8 @@ var DEVICE_EVENTS = [
81682
82590
  "detection-blind",
81683
82591
  "motion-noisy",
81684
82592
  "motion-calm",
82593
+ "package-llm-silent",
82594
+ "package-llm-answered",
81685
82595
  "recording-stalled",
81686
82596
  "recording-healthy",
81687
82597
  "recording-degraded",
@@ -81950,6 +82860,15 @@ new Map([
81950
82860
  snapshotLabel: "Cameras with constant motion now",
81951
82861
  snapshotSection: null
81952
82862
  },
82863
+ {
82864
+ id: "package-llm-confirm",
82865
+ family: "device-events",
82866
+ label: "Package confirm (not answering / answering again)",
82867
+ bad: ["package-llm-silent"],
82868
+ recovery: ["package-llm-answered"],
82869
+ snapshotLabel: "Cameras whose package confirm is not answering",
82870
+ snapshotSection: null
82871
+ },
81953
82872
  {
81954
82873
  id: "recording-health",
81955
82874
  family: "device-events",