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