@camstack/addon-provider-onvif 1.2.148 → 1.2.150

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.
package/dist/addon.mjs CHANGED
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
4
4
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
5
  //#endregion
6
- //#region ../types/dist/event-category-DHXxB8t9.mjs
6
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
7
7
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
8
8
  EventCategory["SystemBoot"] = "system.boot";
9
9
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -329,6 +329,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
329
329
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
330
330
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
331
331
  /**
332
+ * A camera's package LLM confirm has failed closed three times in a row
333
+ * (D931, 2026-10-26). One edge per camera until
334
+ * {@link PipelinePackageLlmAnswered}.
335
+ */
336
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
337
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
338
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
339
+ /**
332
340
  * Per-camera pipeline config was mutated by the orchestrator
333
341
  * (a settings change via `setAgentInferenceDevices` /
334
342
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5438,7 +5446,7 @@ var ZodIssueCode = {
5438
5446
  var ZodFirstPartyTypeKind;
5439
5447
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5440
5448
  //#endregion
5441
- //#region ../types/dist/sleep-BErAxhul.mjs
5449
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5442
5450
  /**
5443
5451
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5444
5452
  * window to float samples (D455).
@@ -7925,6 +7933,99 @@ var RecordKindDeclarationSchema = object({
7925
7933
  }).strict();
7926
7934
  /** The kind names: lower-case, dash separated, short enough for an index name. */
7927
7935
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7936
+ var READING_QUANTITIES = [
7937
+ "temperature",
7938
+ "pressure",
7939
+ "speed",
7940
+ "precipitation",
7941
+ "precipitation-rate",
7942
+ "length",
7943
+ "area",
7944
+ "illuminance",
7945
+ "irradiance",
7946
+ "power",
7947
+ "energy",
7948
+ "voltage",
7949
+ "current",
7950
+ "apparent-power",
7951
+ "humidity",
7952
+ "percentage",
7953
+ "concentration-volume",
7954
+ "concentration-mass",
7955
+ "concentration-ppb",
7956
+ "volume",
7957
+ "duration",
7958
+ "duration-ms",
7959
+ "information",
7960
+ "sound-level",
7961
+ "signal-level",
7962
+ "colour-temperature",
7963
+ "angle",
7964
+ "bitrate",
7965
+ "rate",
7966
+ "mass",
7967
+ "index",
7968
+ "ratio",
7969
+ "count",
7970
+ "none",
7971
+ "device"
7972
+ ];
7973
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
7974
+ new Set(READING_QUANTITIES);
7975
+ /**
7976
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
7977
+ * (or an index / ratio / count). `device` means the cap has no single unit.
7978
+ */
7979
+ var QUANTITY_BASE = {
7980
+ temperature: "°C",
7981
+ pressure: "hPa",
7982
+ speed: "m/s",
7983
+ precipitation: "mm",
7984
+ "precipitation-rate": "mm/h",
7985
+ length: "m",
7986
+ area: "m²",
7987
+ illuminance: "lx",
7988
+ irradiance: "W/m²",
7989
+ power: "W",
7990
+ energy: "kWh",
7991
+ voltage: "V",
7992
+ current: "A",
7993
+ "apparent-power": "VA",
7994
+ humidity: "%",
7995
+ percentage: "%",
7996
+ "concentration-volume": "ppm",
7997
+ "concentration-mass": "µg/m³",
7998
+ "concentration-ppb": "ppb",
7999
+ volume: "L",
8000
+ duration: "s",
8001
+ "duration-ms": "ms",
8002
+ information: "B",
8003
+ "sound-level": "dBFS",
8004
+ "signal-level": "dBm",
8005
+ "colour-temperature": "mired",
8006
+ angle: "°",
8007
+ bitrate: "kbps",
8008
+ rate: "fps",
8009
+ mass: "kg",
8010
+ index: "none",
8011
+ ratio: "none",
8012
+ count: "none",
8013
+ none: "none",
8014
+ device: "device"
8015
+ };
8016
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8017
+ function baseUnitOf(quantity) {
8018
+ const symbol = QUANTITY_BASE[quantity];
8019
+ if (symbol === "none" || symbol === "device") return void 0;
8020
+ return symbol;
8021
+ }
8022
+ function readingMeta(quantity, extras) {
8023
+ return {
8024
+ quantity,
8025
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8026
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8027
+ };
8028
+ }
7928
8029
  /** Shared Zod schemas used across streaming capabilities. */
7929
8030
  var CamProfileSchema = _enum([
7930
8031
  "high",
@@ -7952,8 +8053,8 @@ var CamStreamKindSchema = _enum([
7952
8053
  "derived"
7953
8054
  ]);
7954
8055
  var CamStreamResolutionSchema = object({
7955
- width: number().int().positive(),
7956
- height: number().int().positive()
8056
+ width: number().int().positive().meta(readingMeta("none")),
8057
+ height: number().int().positive().meta(readingMeta("none"))
7957
8058
  });
7958
8059
  var CameraStreamSchema = object({
7959
8060
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8467,6 +8568,9 @@ var CAP_NAMES = Object.freeze([
8467
8568
  "filesystem-browse",
8468
8569
  "flood",
8469
8570
  "gas",
8571
+ "group-active",
8572
+ "group-commands",
8573
+ "group-status",
8470
8574
  "heating-valve",
8471
8575
  "humidifier",
8472
8576
  "humidity-sensor",
@@ -8487,6 +8591,7 @@ var CAP_NAMES = Object.freeze([
8487
8591
  "login-method",
8488
8592
  "media-player",
8489
8593
  "mesh-network",
8594
+ "meter",
8490
8595
  "metrics-provider",
8491
8596
  "model-convert",
8492
8597
  "model-distributor",
@@ -8596,14 +8701,6 @@ Object.freeze({
8596
8701
  {
8597
8702
  "path": "aqi",
8598
8703
  "valueType": "number"
8599
- },
8600
- {
8601
- "path": "unit",
8602
- "valueType": "string"
8603
- },
8604
- {
8605
- "path": "precision",
8606
- "valueType": "number"
8607
8704
  }
8608
8705
  ],
8609
8706
  "alarm-panel": [
@@ -8639,20 +8736,10 @@ Object.freeze({
8639
8736
  "valueType": "boolean"
8640
8737
  }
8641
8738
  ],
8642
- "ambient-light-sensor": [
8643
- {
8644
- "path": "lux",
8645
- "valueType": "number"
8646
- },
8647
- {
8648
- "path": "unit",
8649
- "valueType": "string"
8650
- },
8651
- {
8652
- "path": "precision",
8653
- "valueType": "number"
8654
- }
8655
- ],
8739
+ "ambient-light-sensor": [{
8740
+ "path": "lux",
8741
+ "valueType": "number"
8742
+ }],
8656
8743
  "audio-metrics": [
8657
8744
  {
8658
8745
  "path": "ts",
@@ -8732,6 +8819,10 @@ Object.freeze({
8732
8819
  "path": "lastUpdated",
8733
8820
  "valueType": "number"
8734
8821
  },
8822
+ {
8823
+ "path": "volts",
8824
+ "valueType": "number"
8825
+ },
8735
8826
  {
8736
8827
  "path": "lastContactAt",
8737
8828
  "valueType": "number"
@@ -8974,10 +9065,6 @@ Object.freeze({
8974
9065
  "datetime"
8975
9066
  ]
8976
9067
  },
8977
- {
8978
- "path": "unit",
8979
- "valueType": "string"
8980
- },
8981
9068
  {
8982
9069
  "path": "min",
8983
9070
  "valueType": "number"
@@ -8989,19 +9076,6 @@ Object.freeze({
8989
9076
  {
8990
9077
  "path": "step",
8991
9078
  "valueType": "number"
8992
- },
8993
- {
8994
- "path": "precision",
8995
- "valueType": "number"
8996
- },
8997
- {
8998
- "path": "format",
8999
- "valueType": "enum",
9000
- "enumValues": [
9001
- "date",
9002
- "time",
9003
- "datetime"
9004
- ]
9005
9079
  }
9006
9080
  ],
9007
9081
  "cover": [
@@ -9242,13 +9316,8 @@ Object.freeze({
9242
9316
  "path": "value",
9243
9317
  "valueType": "string"
9244
9318
  }, {
9245
- "path": "format",
9246
- "valueType": "enum",
9247
- "enumValues": [
9248
- "date",
9249
- "time",
9250
- "datetime"
9251
- ]
9319
+ "path": "timestamp",
9320
+ "valueType": "boolean"
9252
9321
  }],
9253
9322
  "event-emitter": [
9254
9323
  {
@@ -9321,6 +9390,24 @@ Object.freeze({
9321
9390
  "path": "detected",
9322
9391
  "valueType": "boolean"
9323
9392
  }],
9393
+ "group-active": [{
9394
+ "path": "active",
9395
+ "valueType": "boolean"
9396
+ }],
9397
+ "group-status": [
9398
+ {
9399
+ "path": "included",
9400
+ "valueType": "number"
9401
+ },
9402
+ {
9403
+ "path": "excluded",
9404
+ "valueType": "number"
9405
+ },
9406
+ {
9407
+ "path": "total",
9408
+ "valueType": "number"
9409
+ }
9410
+ ],
9324
9411
  "heating-valve": [{
9325
9412
  "path": "position",
9326
9413
  "valueType": "number"
@@ -9355,20 +9442,10 @@ Object.freeze({
9355
9442
  "valueType": "number"
9356
9443
  }
9357
9444
  ],
9358
- "humidity-sensor": [
9359
- {
9360
- "path": "percent",
9361
- "valueType": "number"
9362
- },
9363
- {
9364
- "path": "unit",
9365
- "valueType": "string"
9366
- },
9367
- {
9368
- "path": "precision",
9369
- "valueType": "number"
9370
- }
9371
- ],
9445
+ "humidity-sensor": [{
9446
+ "path": "percent",
9447
+ "valueType": "number"
9448
+ }],
9372
9449
  "image": [{
9373
9450
  "path": "url",
9374
9451
  "valueType": "string"
@@ -9576,6 +9653,10 @@ Object.freeze({
9576
9653
  ]
9577
9654
  }
9578
9655
  ],
9656
+ "meter": [{
9657
+ "path": "value",
9658
+ "valueType": "number"
9659
+ }],
9579
9660
  "motion": [
9580
9661
  {
9581
9662
  "path": "detected",
@@ -9707,20 +9788,10 @@ Object.freeze({
9707
9788
  "valueType": "boolean"
9708
9789
  }
9709
9790
  ],
9710
- "numeric-sensor": [
9711
- {
9712
- "path": "value",
9713
- "valueType": "number"
9714
- },
9715
- {
9716
- "path": "unit",
9717
- "valueType": "string"
9718
- },
9719
- {
9720
- "path": "precision",
9721
- "valueType": "number"
9722
- }
9723
- ],
9791
+ "numeric-sensor": [{
9792
+ "path": "value",
9793
+ "valueType": "number"
9794
+ }],
9724
9795
  "pet-feeder": [
9725
9796
  {
9726
9797
  "path": "foodLevel",
@@ -9812,14 +9883,6 @@ Object.freeze({
9812
9883
  {
9813
9884
  "path": "powerFactor",
9814
9885
  "valueType": "number"
9815
- },
9816
- {
9817
- "path": "unit",
9818
- "valueType": "string"
9819
- },
9820
- {
9821
- "path": "precision",
9822
- "valueType": "number"
9823
9886
  }
9824
9887
  ],
9825
9888
  "presence": [
@@ -9848,20 +9911,10 @@ Object.freeze({
9848
9911
  "valueType": "number"
9849
9912
  }
9850
9913
  ],
9851
- "pressure-sensor": [
9852
- {
9853
- "path": "hpa",
9854
- "valueType": "number"
9855
- },
9856
- {
9857
- "path": "unit",
9858
- "valueType": "string"
9859
- },
9860
- {
9861
- "path": "precision",
9862
- "valueType": "number"
9863
- }
9864
- ],
9914
+ "pressure-sensor": [{
9915
+ "path": "hpa",
9916
+ "valueType": "number"
9917
+ }],
9865
9918
  "privacy-mask": [{
9866
9919
  "path": "enabled",
9867
9920
  "valueType": "boolean"
@@ -10172,20 +10225,10 @@ Object.freeze({
10172
10225
  "path": "tampered",
10173
10226
  "valueType": "boolean"
10174
10227
  }],
10175
- "temperature-sensor": [
10176
- {
10177
- "path": "celsius",
10178
- "valueType": "number"
10179
- },
10180
- {
10181
- "path": "unit",
10182
- "valueType": "string"
10183
- },
10184
- {
10185
- "path": "precision",
10186
- "valueType": "number"
10187
- }
10188
- ],
10228
+ "temperature-sensor": [{
10229
+ "path": "celsius",
10230
+ "valueType": "number"
10231
+ }],
10189
10232
  "update": [
10190
10233
  {
10191
10234
  "path": "currentVersion",
@@ -10296,10 +10339,6 @@ Object.freeze({
10296
10339
  "path": "temperature",
10297
10340
  "valueType": "number"
10298
10341
  },
10299
- {
10300
- "path": "temperatureUnit",
10301
- "valueType": "string"
10302
- },
10303
10342
  {
10304
10343
  "path": "humidity",
10305
10344
  "valueType": "number"
@@ -10308,18 +10347,10 @@ Object.freeze({
10308
10347
  "path": "pressure",
10309
10348
  "valueType": "number"
10310
10349
  },
10311
- {
10312
- "path": "pressureUnit",
10313
- "valueType": "string"
10314
- },
10315
10350
  {
10316
10351
  "path": "windSpeed",
10317
10352
  "valueType": "number"
10318
10353
  },
10319
- {
10320
- "path": "windSpeedUnit",
10321
- "valueType": "string"
10322
- },
10323
10354
  {
10324
10355
  "path": "windBearing",
10325
10356
  "valueType": "number"
@@ -10369,7 +10400,7 @@ object({
10369
10400
  params: record(string(), union([string(), number()])).optional()
10370
10401
  });
10371
10402
  //#endregion
10372
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10403
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10373
10404
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10374
10405
  DeviceType["Camera"] = "camera";
10375
10406
  DeviceType["Hub"] = "hub";
@@ -10463,8 +10494,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10463
10494
  DeviceType["PetFeeder"] = "pet-feeder";
10464
10495
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10465
10496
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10466
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10497
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10498
+ * A plug-built dishwasher carries running-state and power instead. */
10467
10499
  DeviceType["Dishwasher"] = "dishwasher";
10500
+ /** Washing machine. A plug-built one is running while its power stays
10501
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10502
+ DeviceType["Washer"] = "washer";
10503
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10504
+ DeviceType["Dryer"] = "dryer";
10468
10505
  return DeviceType;
10469
10506
  }({});
10470
10507
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -10722,7 +10759,7 @@ function errMsg(err) {
10722
10759
  return String(err);
10723
10760
  }
10724
10761
  //#endregion
10725
- //#region ../types/dist/composition-kEl9mevu.mjs
10762
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
10726
10763
  /**
10727
10764
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10728
10765
  *
@@ -11037,6 +11074,13 @@ var table = {
11037
11074
  apply: () => {
11038
11075
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11039
11076
  }
11077
+ },
11078
+ schmitt: {
11079
+ minArgs: 3,
11080
+ maxArgs: 5,
11081
+ apply: () => {
11082
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11083
+ }
11040
11084
  }
11041
11085
  };
11042
11086
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11477,15 +11521,232 @@ function validateExpressionSource(src) {
11477
11521
  }
11478
11522
  return null;
11479
11523
  }
11480
- /** A composed device's stableId is this prefix plus the block id. */
11481
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11482
- /** Longest snippet a `code` source may carry (slice 4). */
11483
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11524
+ /**
11525
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11526
+ *
11527
+ * Standby of the control board is under 5 W; a motor or a heater is far
11528
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11529
+ * a cycle, so running starts only after 30 s above the line.
11530
+ *
11531
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11532
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11533
+ * report "finished" in the middle of the program, so the stop dwell is
11534
+ * 10 minutes on every type. The number is stored per type so one machine
11535
+ * can move later without moving the others.
11536
+ */
11537
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11538
+ var APPLIANCE_KINDS = [
11539
+ "washer",
11540
+ "dryer",
11541
+ "dishwasher"
11542
+ ];
11543
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11544
+ /**
11545
+ * Statistics and integrals over a history read (D944).
11546
+ *
11547
+ * Pure: the same points always give the same answer, so a restart recomputes
11548
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11549
+ * never the mean of the samples. A hole with no held value is not integrated
11550
+ * as zero. A truncated read is not published.
11551
+ */
11552
+ var STATISTIC_STATS = [
11553
+ "min",
11554
+ "max",
11555
+ "mean",
11556
+ "sum",
11557
+ "count",
11558
+ "change"
11559
+ ];
11560
+ /**
11561
+ * The range a statistics or integral helper asks history for (D944).
11562
+ *
11563
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11564
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11565
+ * is strictly longer than the longest that range can be. Forever qualifies.
11566
+ * Equal to a tier's retention is a refusal, not a short answer.
11567
+ */
11568
+ var MINUTE_MS = 6e4;
11569
+ var HOUR_MS = 60 * MINUTE_MS;
11570
+ var DAY_MS = 24 * HOUR_MS;
11571
+ var ROLLING_SPANS = [
11572
+ "1h",
11573
+ "24h",
11574
+ "2d",
11575
+ "7d",
11576
+ "30d",
11577
+ "custom"
11578
+ ];
11579
+ var CALENDAR_PERIODS = [
11580
+ "today",
11581
+ "yesterday",
11582
+ "this-week",
11583
+ "this-month"
11584
+ ];
11585
+ var RESET_PERIODS = [
11586
+ "never",
11587
+ "daily",
11588
+ "weekly",
11589
+ "monthly"
11590
+ ];
11591
+ 5 * MINUTE_MS;
11592
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11593
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11594
+ var rollingWindowObject = object({
11595
+ kind: literal("rolling"),
11596
+ span: _enum(ROLLING_SPANS),
11597
+ customMinutes: number().int().min(5).optional()
11598
+ }).check((payload) => {
11599
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11600
+ code: "custom",
11601
+ message: "a custom range needs a duration in minutes",
11602
+ path: ["customMinutes"],
11603
+ input: payload.value
11604
+ });
11605
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11606
+ code: "custom",
11607
+ message: "only a custom range carries a duration",
11608
+ path: ["customMinutes"],
11609
+ input: payload.value
11610
+ });
11611
+ });
11612
+ var RollingWindowSchema = rollingWindowObject;
11613
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
11614
+ kind: literal("calendar"),
11615
+ period: _enum(CALENDAR_PERIODS)
11616
+ })]);
11617
+ /**
11618
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11619
+ * id. Split out so helper schemas can name it without a cycle through
11620
+ * `composition.ts` (D659).
11621
+ */
11484
11622
  var CompositionSourceRefSchema = object({
11485
11623
  /** The owning addon; `stableId` is unique only within it. */
11486
11624
  addonId: string().min(1),
11487
11625
  stableId: string().min(1)
11488
11626
  });
11627
+ var GROUP_TEMPLATE_IDS = [
11628
+ "temperature",
11629
+ "humidity",
11630
+ "pressure",
11631
+ "illuminance",
11632
+ "power",
11633
+ "energy",
11634
+ "battery",
11635
+ "numeric",
11636
+ "motion",
11637
+ "contact",
11638
+ "leak",
11639
+ "smoke",
11640
+ "occupancy",
11641
+ "binary",
11642
+ "switch",
11643
+ "light",
11644
+ "fan",
11645
+ "cover",
11646
+ "valve",
11647
+ "lock",
11648
+ "media-player",
11649
+ "climate"
11650
+ ];
11651
+ var NUMERIC_AGGREGATES = [
11652
+ "mean",
11653
+ "median",
11654
+ "min",
11655
+ "max",
11656
+ "sum"
11657
+ ];
11658
+ var BINARY_AGGREGATES = ["any", "all"];
11659
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11660
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11661
+ var AGGREGATE_REDUCERS = [
11662
+ "mean",
11663
+ "median",
11664
+ "min",
11665
+ "max",
11666
+ "sum",
11667
+ "any",
11668
+ "all"
11669
+ ];
11670
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11671
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11672
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11673
+ /** One member. The field is stored so occupancy can name motion or binary. */
11674
+ var HelperMemberSchema = object({
11675
+ source: CompositionSourceRefSchema,
11676
+ cap: string().min(1),
11677
+ fieldPath: string().min(1)
11678
+ });
11679
+ var HelperSpecSchema = discriminatedUnion("kind", [
11680
+ object({
11681
+ kind: literal("group"),
11682
+ templateId: GroupTemplateIdSchema,
11683
+ aggregate: AggregateReducerSchema,
11684
+ members: array(HelperMemberSchema).min(1).max(64),
11685
+ /** Actuator groups only. Readings leave this unset. */
11686
+ active: ActiveCriterionSchema.optional()
11687
+ }),
11688
+ object({
11689
+ kind: literal("appliance"),
11690
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
11691
+ appliance: _enum(APPLIANCE_KINDS),
11692
+ thresholdWatts: number().positive(),
11693
+ startDwellMs: number().int().nonnegative(),
11694
+ stopDwellMs: number().int().nonnegative(),
11695
+ member: HelperMemberSchema
11696
+ }),
11697
+ object({
11698
+ kind: literal("statistic"),
11699
+ stat: _enum(STATISTIC_STATS),
11700
+ window: StatisticWindowSchema,
11701
+ source: HelperMemberSchema
11702
+ }),
11703
+ object({
11704
+ kind: literal("integral"),
11705
+ reset: _enum(RESET_PERIODS),
11706
+ output: _enum(["energy", "meter"]),
11707
+ source: HelperMemberSchema
11708
+ }),
11709
+ object({
11710
+ kind: literal("threshold"),
11711
+ source: HelperMemberSchema,
11712
+ high: number(),
11713
+ low: number(),
11714
+ onForMs: number().int().nonnegative(),
11715
+ offForMs: number().int().nonnegative()
11716
+ }),
11717
+ object({
11718
+ kind: literal("arithmetic"),
11719
+ op: _enum([
11720
+ "sum",
11721
+ "difference",
11722
+ "ratio"
11723
+ ]),
11724
+ inputs: array(HelperMemberSchema).min(2).max(64)
11725
+ }),
11726
+ object({
11727
+ kind: literal("expression"),
11728
+ expr: string().min(1),
11729
+ inputs: array(object({
11730
+ name: string().min(1),
11731
+ member: HelperMemberSchema
11732
+ })).min(1).max(64),
11733
+ output: _enum(["number", "boolean"])
11734
+ }),
11735
+ object({
11736
+ kind: literal("derivative"),
11737
+ window: RollingWindowSchema,
11738
+ per: _enum([
11739
+ "second",
11740
+ "minute",
11741
+ "hour"
11742
+ ]),
11743
+ source: HelperMemberSchema
11744
+ })
11745
+ ]);
11746
+ /** A composed device's stableId is this prefix plus the block id. */
11747
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11748
+ /** Longest snippet a `code` source may carry (slice 4). */
11749
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11489
11750
  /** One field of one capability of one source device. */
11490
11751
  var CompositionFieldReadSchema = object({
11491
11752
  source: CompositionSourceRefSchema,
@@ -11529,7 +11790,59 @@ var CompositionCodeSourceSchema = object({
11529
11790
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11530
11791
  CompositionFromSourceSchema,
11531
11792
  CompositionExpressionSourceSchema,
11532
- CompositionCodeSourceSchema
11793
+ CompositionCodeSourceSchema,
11794
+ object({
11795
+ kind: literal("aggregate"),
11796
+ reducer: _enum([
11797
+ "mean",
11798
+ "median",
11799
+ "min",
11800
+ "max",
11801
+ "sum",
11802
+ "any",
11803
+ "all"
11804
+ ]),
11805
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11806
+ /**
11807
+ * Set when the reads are an enum state, not a yes/no: the listed states
11808
+ * count as active (a cover is `open`). A state outside the list is inactive;
11809
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
11810
+ */
11811
+ activeWhen: array(string().min(1)).min(1).optional(),
11812
+ /** `value` is the reading; the others are the member counts. */
11813
+ publish: _enum([
11814
+ "value",
11815
+ "included",
11816
+ "excluded",
11817
+ "total"
11818
+ ]),
11819
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11820
+ empty: _enum([
11821
+ "null",
11822
+ "omit",
11823
+ "offline"
11824
+ ])
11825
+ }),
11826
+ object({
11827
+ kind: literal("series"),
11828
+ read: CompositionFieldReadSchema,
11829
+ /** `value` is the statistic or the integral. The others are the gap counts. */
11830
+ publish: _enum([
11831
+ "value",
11832
+ "included",
11833
+ "excluded",
11834
+ "total"
11835
+ ]),
11836
+ /**
11837
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
11838
+ * device unseeded until history answers, so a restart cannot publish 0.
11839
+ */
11840
+ empty: _enum([
11841
+ "null",
11842
+ "omit",
11843
+ "offline"
11844
+ ])
11845
+ })
11533
11846
  ]);
11534
11847
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11535
11848
  kind: literal("forward"),
@@ -11575,7 +11888,13 @@ var CompositionSchema = object({
11575
11888
  kind: literal("existing"),
11576
11889
  device: CompositionSourceRefSchema
11577
11890
  })]),
11578
- features: array(CompositionFeatureSchema).min(1).max(16)
11891
+ features: array(CompositionFeatureSchema).min(1).max(16),
11892
+ /**
11893
+ * Present when the block is a helper template. Absent on a free
11894
+ * composition (the room example, device 4603). The editor reads this,
11895
+ * never the compiled features.
11896
+ */
11897
+ helper: HelperSpecSchema.optional()
11579
11898
  }).superRefine((composition, ctx) => {
11580
11899
  const seen = /* @__PURE__ */ new Set();
11581
11900
  composition.features.forEach((feature, index) => {
@@ -16682,7 +17001,22 @@ var BrokerProviderInfoSchema = object({
16682
17001
  */
16683
17002
  interactiveConnectPath: string().min(1).optional(),
16684
17003
  /** Relative hub path to disconnect the linked account (same popup pattern). */
16685
- interactiveDisconnectPath: string().min(1).optional()
17004
+ interactiveDisconnectPath: string().min(1).optional(),
17005
+ /**
17006
+ * The kind signs in through a server-held interactive session instead of
17007
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17008
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17009
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17010
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17011
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17012
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17013
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17014
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17015
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17016
+ * offers it as the default path; the fields the session fills are
17017
+ * marked `filledBySignIn` in the kind's settings schema.
17018
+ */
17019
+ signIn: object({ basePath: string().min(1) }).optional()
16686
17020
  }))
16687
17021
  });
16688
17022
  var ListInputSchema = object({
@@ -17677,7 +18011,12 @@ var CompositionProblemSchema = object({
17677
18011
  "item-field-reserved",
17678
18012
  "target-missing",
17679
18013
  "target-unreadable",
17680
- "not-replaceable"
18014
+ "not-replaceable",
18015
+ "native-capability-replace",
18016
+ "mixed-quantity",
18017
+ "helper-mismatch",
18018
+ "member-constraint",
18019
+ "retention"
17681
18020
  ]),
17682
18021
  path: string(),
17683
18022
  message: string()
@@ -17698,7 +18037,8 @@ var CompositionUnavailableModeSchema = _enum([
17698
18037
  "null",
17699
18038
  "offline",
17700
18039
  "unknown",
17701
- "release"
18040
+ "release",
18041
+ "omit"
17702
18042
  ]);
17703
18043
  /**
17704
18044
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17735,9 +18075,11 @@ var CompositionFieldPlanSchema = object({
17735
18075
  reads: array(CompositionFieldReadSchema)
17736
18076
  });
17737
18077
  /**
17738
- * What a feature does to its target (D663): `new` builds a capability on a new
17739
- * device; on an existing device it `add`s a capability the device lacks, or
17740
- * `replace`s named fields of one it has natively.
18078
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18079
+ * new device; on an existing device it `add`s a capability the device lacks.
18080
+ * Replacing a capability the device already serves is refused
18081
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18082
+ * wire for a claim the hub already holds.
17741
18083
  */
17742
18084
  var CompositionFeatureModeSchema = _enum([
17743
18085
  "new",
@@ -17908,8 +18250,392 @@ Object.freeze({
17908
18250
  latchedAt: {
17909
18251
  kind: "fixed",
17910
18252
  result: ["number", "null"]
18253
+ },
18254
+ schmitt: {
18255
+ kind: "fixed",
18256
+ result: ["boolean", "null"]
17911
18257
  }
17912
18258
  });
18259
+ /**
18260
+ * One catalog row per base reading a group helper can build (D944 §3).
18261
+ * Data, not a form default written into the block.
18262
+ */
18263
+ var NOW$2 = {
18264
+ fieldPath: "lastFetchedAt",
18265
+ expr: "now"
18266
+ };
18267
+ var CHANGED = {
18268
+ fieldPath: "lastChangedAt",
18269
+ expr: "now"
18270
+ };
18271
+ function numeric(input) {
18272
+ return {
18273
+ ...input,
18274
+ family: "numeric",
18275
+ deviceType: DeviceType.Sensor,
18276
+ memberDeviceType: DeviceType.Sensor,
18277
+ memberDeviceTypes: [DeviceType.Sensor],
18278
+ activeWhen: void 0,
18279
+ commandCaps: [],
18280
+ baseUnit: baseUnitOf(input.quantity),
18281
+ aggregates: NUMERIC_AGGREGATES,
18282
+ accept: [{
18283
+ cap: input.outputCap,
18284
+ fieldPath: input.outputField
18285
+ }]
18286
+ };
18287
+ }
18288
+ function binary(input) {
18289
+ return {
18290
+ ...input,
18291
+ family: "binary",
18292
+ deviceType: DeviceType.Sensor,
18293
+ memberDeviceType: DeviceType.Sensor,
18294
+ memberDeviceTypes: [DeviceType.Sensor],
18295
+ activeWhen: void 0,
18296
+ commandCaps: [],
18297
+ quantity: "none",
18298
+ baseUnit: void 0,
18299
+ aggregates: BINARY_AGGREGATES,
18300
+ empty: "offline"
18301
+ };
18302
+ }
18303
+ var GROUP_TEMPLATES = [
18304
+ numeric({
18305
+ id: "temperature",
18306
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18307
+ role: DeviceRole.TemperatureSensor,
18308
+ outputCap: "temperature-sensor",
18309
+ outputField: "celsius",
18310
+ quantity: "temperature",
18311
+ defaultAggregate: "mean",
18312
+ empty: "null",
18313
+ companions: [NOW$2]
18314
+ }),
18315
+ numeric({
18316
+ id: "humidity",
18317
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18318
+ role: DeviceRole.HumiditySensor,
18319
+ outputCap: "humidity-sensor",
18320
+ outputField: "percent",
18321
+ quantity: "humidity",
18322
+ defaultAggregate: "mean",
18323
+ empty: "null",
18324
+ companions: [NOW$2]
18325
+ }),
18326
+ numeric({
18327
+ id: "pressure",
18328
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18329
+ role: DeviceRole.PressureSensor,
18330
+ outputCap: "pressure-sensor",
18331
+ outputField: "hpa",
18332
+ quantity: "pressure",
18333
+ defaultAggregate: "mean",
18334
+ empty: "null",
18335
+ companions: [NOW$2]
18336
+ }),
18337
+ numeric({
18338
+ id: "illuminance",
18339
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18340
+ role: DeviceRole.AmbientLightSensor,
18341
+ outputCap: "ambient-light-sensor",
18342
+ outputField: "lux",
18343
+ quantity: "illuminance",
18344
+ defaultAggregate: "mean",
18345
+ empty: "null",
18346
+ companions: [NOW$2]
18347
+ }),
18348
+ numeric({
18349
+ id: "power",
18350
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18351
+ role: DeviceRole.PowerSensor,
18352
+ outputCap: "power-meter",
18353
+ outputField: "watts",
18354
+ quantity: "power",
18355
+ defaultAggregate: "sum",
18356
+ empty: "omit",
18357
+ companions: [NOW$2]
18358
+ }),
18359
+ numeric({
18360
+ id: "energy",
18361
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18362
+ role: DeviceRole.EnergySensor,
18363
+ outputCap: "power-meter",
18364
+ outputField: "kwhTotal",
18365
+ quantity: "energy",
18366
+ defaultAggregate: "sum",
18367
+ empty: "omit",
18368
+ companions: [NOW$2]
18369
+ }),
18370
+ numeric({
18371
+ id: "battery",
18372
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18373
+ role: DeviceRole.BatterySensor,
18374
+ outputCap: "battery",
18375
+ outputField: "percentage",
18376
+ quantity: "percentage",
18377
+ defaultAggregate: "mean",
18378
+ empty: "null",
18379
+ companions: [
18380
+ {
18381
+ fieldPath: "charging",
18382
+ expr: "\"unknown\""
18383
+ },
18384
+ {
18385
+ fieldPath: "sleeping",
18386
+ expr: "false"
18387
+ },
18388
+ {
18389
+ fieldPath: "lastUpdated",
18390
+ expr: "now"
18391
+ }
18392
+ ]
18393
+ }),
18394
+ numeric({
18395
+ id: "numeric",
18396
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18397
+ role: DeviceRole.NumericSensor,
18398
+ outputCap: "numeric-sensor",
18399
+ outputField: "value",
18400
+ quantity: "device",
18401
+ defaultAggregate: "mean",
18402
+ empty: "null",
18403
+ companions: [NOW$2]
18404
+ }),
18405
+ binary({
18406
+ id: "motion",
18407
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18408
+ role: DeviceRole.MotionSensor,
18409
+ outputCap: "motion",
18410
+ outputField: "detected",
18411
+ defaultAggregate: "any",
18412
+ accept: [{
18413
+ cap: "motion",
18414
+ fieldPath: "detected"
18415
+ }],
18416
+ companions: [{
18417
+ fieldPath: "lastDetectedAt",
18418
+ expr: "null"
18419
+ }, {
18420
+ fieldPath: "autoClearAfterMs",
18421
+ expr: "null"
18422
+ }]
18423
+ }),
18424
+ binary({
18425
+ id: "contact",
18426
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18427
+ role: DeviceRole.ContactSensor,
18428
+ outputCap: "contact",
18429
+ outputField: "entryOpen",
18430
+ defaultAggregate: "any",
18431
+ accept: [{
18432
+ cap: "contact",
18433
+ fieldPath: "entryOpen"
18434
+ }],
18435
+ companions: [CHANGED]
18436
+ }),
18437
+ binary({
18438
+ id: "leak",
18439
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18440
+ role: DeviceRole.LeakSensor,
18441
+ outputCap: "flood",
18442
+ outputField: "flooded",
18443
+ defaultAggregate: "any",
18444
+ accept: [{
18445
+ cap: "flood",
18446
+ fieldPath: "flooded"
18447
+ }],
18448
+ companions: [CHANGED]
18449
+ }),
18450
+ binary({
18451
+ id: "smoke",
18452
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18453
+ role: DeviceRole.SmokeSensor,
18454
+ outputCap: "smoke",
18455
+ outputField: "detected",
18456
+ defaultAggregate: "any",
18457
+ accept: [{
18458
+ cap: "smoke",
18459
+ fieldPath: "detected"
18460
+ }],
18461
+ companions: [CHANGED]
18462
+ }),
18463
+ binary({
18464
+ id: "occupancy",
18465
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18466
+ role: DeviceRole.BinarySensor,
18467
+ outputCap: "binary",
18468
+ outputField: "on",
18469
+ defaultAggregate: "any",
18470
+ accept: [{
18471
+ cap: "motion",
18472
+ fieldPath: "detected"
18473
+ }, {
18474
+ cap: "binary",
18475
+ fieldPath: "on"
18476
+ }],
18477
+ companions: [CHANGED]
18478
+ }),
18479
+ binary({
18480
+ id: "binary",
18481
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18482
+ role: DeviceRole.BinarySensor,
18483
+ outputCap: "binary",
18484
+ outputField: "on",
18485
+ defaultAggregate: "any",
18486
+ accept: [{
18487
+ cap: "binary",
18488
+ fieldPath: "on"
18489
+ }],
18490
+ companions: [CHANGED]
18491
+ }),
18492
+ actuator({
18493
+ id: "switch",
18494
+ 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.",
18495
+ deviceType: DeviceType.Switch,
18496
+ outputCap: "switch",
18497
+ outputField: "on",
18498
+ member: {
18499
+ cap: "switch",
18500
+ fieldPath: "on"
18501
+ },
18502
+ commandCaps: ["switch"]
18503
+ }),
18504
+ actuator({
18505
+ id: "light",
18506
+ 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.",
18507
+ deviceType: DeviceType.Light,
18508
+ outputCap: "switch",
18509
+ outputField: "on",
18510
+ member: {
18511
+ cap: "switch",
18512
+ fieldPath: "on"
18513
+ },
18514
+ commandCaps: [
18515
+ "switch",
18516
+ "brightness",
18517
+ "color"
18518
+ ]
18519
+ }),
18520
+ actuator({
18521
+ id: "fan",
18522
+ 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.",
18523
+ deviceType: DeviceType.Fan,
18524
+ outputCap: "switch",
18525
+ outputField: "on",
18526
+ member: {
18527
+ cap: "switch",
18528
+ fieldPath: "on"
18529
+ },
18530
+ commandCaps: ["switch", "fan-control"]
18531
+ }),
18532
+ actuator({
18533
+ id: "cover",
18534
+ 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.",
18535
+ deviceType: DeviceType.Cover,
18536
+ outputCap: "group-active",
18537
+ outputField: "active",
18538
+ member: {
18539
+ cap: "cover",
18540
+ fieldPath: "state"
18541
+ },
18542
+ activeWhen: [
18543
+ "open",
18544
+ "opening",
18545
+ "stopped"
18546
+ ],
18547
+ commandCaps: ["cover"]
18548
+ }),
18549
+ actuator({
18550
+ id: "valve",
18551
+ 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.",
18552
+ deviceType: DeviceType.Valve,
18553
+ outputCap: "group-active",
18554
+ outputField: "active",
18555
+ member: {
18556
+ cap: "valve",
18557
+ fieldPath: "state"
18558
+ },
18559
+ activeWhen: [
18560
+ "open",
18561
+ "opening",
18562
+ "stopped"
18563
+ ],
18564
+ commandCaps: ["valve"]
18565
+ }),
18566
+ actuator({
18567
+ id: "lock",
18568
+ 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.",
18569
+ deviceType: DeviceType.Lock,
18570
+ outputCap: "group-active",
18571
+ outputField: "active",
18572
+ member: {
18573
+ cap: "lock-control",
18574
+ fieldPath: "state"
18575
+ },
18576
+ activeWhen: ["locked"],
18577
+ commandCaps: ["lock-control"]
18578
+ }),
18579
+ actuator({
18580
+ id: "media-player",
18581
+ 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.",
18582
+ deviceType: DeviceType.MediaPlayer,
18583
+ outputCap: "group-active",
18584
+ outputField: "active",
18585
+ member: {
18586
+ cap: "media-player",
18587
+ fieldPath: "state"
18588
+ },
18589
+ activeWhen: ["playing", "buffering"],
18590
+ commandCaps: ["media-player"]
18591
+ }),
18592
+ actuator({
18593
+ id: "climate",
18594
+ 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.",
18595
+ deviceType: DeviceType.Thermostat,
18596
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
18597
+ outputCap: "group-active",
18598
+ outputField: "active",
18599
+ member: {
18600
+ cap: "climate-control",
18601
+ fieldPath: "mode"
18602
+ },
18603
+ activeWhen: [
18604
+ "heat",
18605
+ "cool",
18606
+ "auto",
18607
+ "heat_cool",
18608
+ "fan_only",
18609
+ "dry"
18610
+ ],
18611
+ commandCaps: ["climate-control"]
18612
+ })
18613
+ ];
18614
+ function actuator(input) {
18615
+ return {
18616
+ id: input.id,
18617
+ summary: input.summary,
18618
+ deviceType: input.deviceType,
18619
+ outputCap: input.outputCap,
18620
+ outputField: input.outputField,
18621
+ commandCaps: input.commandCaps,
18622
+ family: "actuator",
18623
+ role: void 0,
18624
+ memberDeviceType: input.deviceType,
18625
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
18626
+ activeWhen: input.activeWhen,
18627
+ quantity: "none",
18628
+ baseUnit: void 0,
18629
+ aggregates: BINARY_AGGREGATES,
18630
+ defaultAggregate: "any",
18631
+ empty: "offline",
18632
+ accept: [input.member],
18633
+ companions: [CHANGED]
18634
+ };
18635
+ }
18636
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18637
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18638
+ DeviceType.Camera;
17913
18639
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
17914
18640
  /**
17915
18641
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19736,12 +20462,12 @@ function buildDeviceAdminUrl(parts) {
19736
20462
  * providers detect renames by matching `uniqueId` and patch `id`
19737
20463
  * in-place so dispatch keeps working.
19738
20464
  *
19739
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19740
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19741
- * state push and are the single source of truth. The `device_class` tag is
19742
- * an HA-specific input that is consumed at the discovery boundary
19743
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19744
- * stored after that point.
20465
+ * **Display unit and precision are NOT stored here.** They are a read-time
20466
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20467
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20468
+ * neither is repeated on each sample. The `device_class` tag is an
20469
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20470
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19745
20471
  *
19746
20472
  * The `raw` escape hatch carries provider-specific attributes that don't
19747
20473
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19755,6 +20481,14 @@ var SourceInfoSchema = object({
19755
20481
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19756
20482
  * Used to detect rename when `id` changes. */
19757
20483
  uniqueId: string().optional(),
20484
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20485
+ quantity: ReadingQuantitySchema.optional(),
20486
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20487
+ dateTimeShape: _enum([
20488
+ "date",
20489
+ "time",
20490
+ "datetime"
20491
+ ]).optional(),
19758
20492
  /** Provider-specific extras that don't fit the structured slots.
19759
20493
  * Whitelist what goes here per provider — do NOT dump entire HA
19760
20494
  * attribute blobs (would explode the meta JSON column). */
@@ -40748,7 +41482,8 @@ method(object({
40748
41482
  "enum"
40749
41483
  ]),
40750
41484
  enumValues: array(string()).optional(),
40751
- item: boolean().optional()
41485
+ item: boolean().optional(),
41486
+ quantity: ReadingQuantitySchema.optional()
40752
41487
  })).readonly(),
40753
41488
  itemArray: object({
40754
41489
  path: string(),
@@ -43109,7 +43844,7 @@ var NetworkAccessStatusSchema = object({
43109
43844
  });
43110
43845
  /**
43111
43846
  * Optional, richer endpoint shape returned by providers that expose
43112
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
43847
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43113
43848
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43114
43849
  * the originating provider config (mode + sourcePort) so the
43115
43850
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44548,8 +45283,8 @@ var MaskPolygonShapeSchema = object({
44548
45283
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44549
45284
  var MaskGridShapeSchema = object({
44550
45285
  kind: literal("grid"),
44551
- gridWidth: number(),
44552
- gridHeight: number(),
45286
+ gridWidth: number().meta(readingMeta("none")),
45287
+ gridHeight: number().meta(readingMeta("none")),
44553
45288
  cells: array(boolean())
44554
45289
  });
44555
45290
  discriminatedUnion("kind", [
@@ -44661,6 +45396,8 @@ var NcSystemEventKindSchema = _enum([
44661
45396
  "detection-blind",
44662
45397
  "motion-noisy",
44663
45398
  "motion-calm",
45399
+ "package-llm-silent",
45400
+ "package-llm-answered",
44664
45401
  "recording-stalled",
44665
45402
  "recording-healthy",
44666
45403
  "recording-degraded",
@@ -45750,7 +46487,20 @@ var NcRuleInputSchema = object({
45750
46487
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
45751
46488
  * path does not run Zod defaults.
45752
46489
  */
45753
- groupingSec: number().int().min(5).max(60).optional()
46490
+ groupingSec: number().int().min(5).max(60).optional(),
46491
+ /**
46492
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46493
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46494
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46495
+ * same mark the Library writes, and records whose notification it was and
46496
+ * when. A channel with no buttons gets the same two choices as links.
46497
+ *
46498
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46499
+ * cap path): absent and `false` both mean off, which is what every rule
46500
+ * authored before this field meant. An editor turns it off by writing
46501
+ * `false`, so it needs no `clear` key.
46502
+ */
46503
+ askVerdict: boolean().optional()
45754
46504
  });
45755
46505
  /**
45756
46506
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -55779,6 +56529,28 @@ var PackageVersionInfoSchema = object({
55779
56529
  deprecated: string().optional(),
55780
56530
  distTags: array(string()).readonly()
55781
56531
  });
56532
+ /**
56533
+ * One published document. `ok` is markdown from the registry. `missing` means
56534
+ * the registry answered and the file was not there. `error` is a registry or
56535
+ * tarball failure — never an empty string standing in for either.
56536
+ */
56537
+ var PackageDocBodySchema = discriminatedUnion("status", [
56538
+ object({
56539
+ status: literal("ok"),
56540
+ markdown: string()
56541
+ }),
56542
+ object({ status: literal("missing") }),
56543
+ object({
56544
+ status: literal("error"),
56545
+ message: string()
56546
+ })
56547
+ ]);
56548
+ var PackageDocsSchema = object({
56549
+ name: string(),
56550
+ version: string(),
56551
+ readme: PackageDocBodySchema,
56552
+ changelog: PackageDocBodySchema
56553
+ });
55782
56554
  var SearchResultSchema = object({
55783
56555
  name: string(),
55784
56556
  version: string(),
@@ -55997,7 +56769,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
55997
56769
  }), object({ success: literal(true) }), {
55998
56770
  kind: "mutation",
55999
56771
  auth: "admin"
56000
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
56772
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
56773
+ name: string().min(1),
56774
+ version: string().min(1).optional()
56775
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56001
56776
  kind: "mutation",
56002
56777
  auth: "admin"
56003
56778
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56047,46 +56822,27 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56047
56822
  }), LogStreamEntrySchema, { kind: "subscription" });
56048
56823
  object({
56049
56824
  /** Carbon dioxide concentration in ppm. */
56050
- co2Ppm: number().min(0).optional(),
56825
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56051
56826
  /** Total volatile organic compounds in ppb. */
56052
- vocPpb: number().min(0).optional(),
56827
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56053
56828
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56054
- pm25: number().min(0).optional(),
56829
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56055
56830
  /** Particulate matter ≤ 10 μm in µg/m³. */
56056
- pm10: number().min(0).optional(),
56831
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56057
56832
  /** Composite AQI value (typically 0..500). */
56058
- aqi: number().optional(),
56833
+ aqi: number().optional().meta(readingMeta("index")),
56059
56834
  /** Ms epoch when the slice was last updated. */
56060
- lastFetchedAt: number(),
56061
- /** Live display unit of the single metric this slice carries (e.g. HA
56062
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56063
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56064
- * per slice is unambiguous. */
56065
- unit: string().optional(),
56066
- /** Suggested decimal places for numeric display.
56067
- * Populated live from the upstream source when provided (e.g. HA
56068
- * `attributes.suggested_display_precision`). Falls back to
56069
- * auto-formatting when absent. */
56070
- precision: number().int().min(0).max(10).optional()
56835
+ lastFetchedAt: number()
56071
56836
  });
56072
56837
  DeviceType.Sensor;
56073
56838
  object({
56074
- /** Current illuminance in lux (lx). */
56075
- lux: number().min(0).meta({
56076
- unit: "lx",
56839
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56840
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56841
+ precision: 0,
56077
56842
  label: "Illuminance"
56078
- }),
56843
+ })),
56079
56844
  /** Ms epoch when the slice was last updated. */
56080
- lastFetchedAt: number(),
56081
- /** Live display unit from the upstream source (e.g. HA
56082
- * `attributes.unit_of_measurement`). The UI prefers this over the
56083
- * role's canonical unit. Absent → fall back to the canonical unit. */
56084
- unit: string().optional(),
56085
- /** Suggested decimal places for numeric display.
56086
- * Populated live from the upstream source when provided (e.g. HA
56087
- * `attributes.suggested_display_precision`). Falls back to
56088
- * auto-formatting when absent. */
56089
- precision: number().int().min(0).max(10).optional()
56845
+ lastFetchedAt: number()
56090
56846
  });
56091
56847
  DeviceType.Sensor;
56092
56848
  /**
@@ -56231,10 +56987,7 @@ var BatteryStatusSchema = object({
56231
56987
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56232
56988
  * declines to band a non-finite reading, which is the correct shape.
56233
56989
  */
56234
- percentage: number().min(0).max(100).nullable().meta({
56235
- unit: "%",
56236
- label: "Charge"
56237
- }),
56990
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56238
56991
  /**
56239
56992
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56240
56993
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56261,7 +57014,14 @@ var BatteryStatusSchema = object({
56261
57014
  */
56262
57015
  sleeping: boolean().meta({ label: "Sleeping" }),
56263
57016
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56264
- lastUpdated: number(),
57017
+ lastUpdated: number().meta(readingMeta("none")),
57018
+ /**
57019
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57020
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57021
+ * that only report charge. A second reading on this cap, not a second
57022
+ * `numeric-sensor` on the same device.
57023
+ */
57024
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56265
57025
  /**
56266
57026
  * Ms epoch of the last time the device PROVED it was reachable — a
56267
57027
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56301,7 +57061,7 @@ var BatteryStatusSchema = object({
56301
57061
  */
56302
57062
  binary: boolean().optional().meta({ label: "Binary indicator" })
56303
57063
  });
56304
- DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat;
57064
+ DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.PetFeeder, DeviceType.Container, DeviceType.Thermostat, DeviceType.EventEmitter;
56305
57065
  method(object({
56306
57066
  deviceId: number(),
56307
57067
  /** Bound on the wait. Sensible range 3000–10000ms. */
@@ -56321,7 +57081,7 @@ object({
56321
57081
  DeviceType.Sensor;
56322
57082
  object({
56323
57083
  /** Current level as 0..100 inclusive. Firmware-reported. */
56324
- percentage: number().min(0).max(100),
57084
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56325
57085
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56326
57086
  lastChangedAt: number()
56327
57087
  });
@@ -56706,21 +57466,21 @@ object({
56706
57466
  /** Single setpoint in Celsius. Used by single-target modes
56707
57467
  * (heat / cool / dry). Null when the device is in a range mode
56708
57468
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
56709
- target: number().nullable(),
57469
+ target: number().nullable().meta(readingMeta("temperature")),
56710
57470
  /** Upper bound of the dual setpoint in Celsius. Populated only
56711
57471
  * in `heat_cool` mode. */
56712
- targetHigh: number().nullable(),
57472
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
56713
57473
  /** Lower bound of the dual setpoint in Celsius. Populated only
56714
57474
  * in `heat_cool` mode. */
56715
- targetLow: number().nullable(),
57475
+ targetLow: number().nullable().meta(readingMeta("temperature")),
56716
57476
  /** Target relative humidity (0..100). Null when the device has no
56717
57477
  * humidity control. */
56718
- targetHumidity: number().min(0).max(100).nullable(),
57478
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56719
57479
  /** Current measured temperature in Celsius. Read-only — pushed by
56720
57480
  * the device's internal sensor. */
56721
- currentTemp: number().nullable(),
57481
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
56722
57482
  /** Current measured relative humidity (0..100). Read-only. */
56723
- currentHumidity: number().min(0).max(100).nullable(),
57483
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56724
57484
  /** Vertical louver swing on/off. Null when the device has no vertical
56725
57485
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
56726
57486
  swingVertical: boolean().nullable(),
@@ -56728,11 +57488,11 @@ object({
56728
57488
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
56729
57489
  swingHorizontal: boolean().nullable(),
56730
57490
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
56731
- minTemp: number().nullable().optional(),
57491
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
56732
57492
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
56733
- maxTemp: number().nullable().optional(),
57493
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
56734
57494
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
56735
- tempStep: number().positive().nullable().optional(),
57495
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
56736
57496
  /** Ms epoch when the slice was last updated (push or command). */
56737
57497
  lastFetchedAt: number()
56738
57498
  });
@@ -56811,17 +57571,17 @@ DeviceType.Thermostat, DeviceType.Climate, method(object({
56811
57571
  * modes to expose in the picker without a slice fetch.
56812
57572
  */
56813
57573
  var RgbTripletSchema = object({
56814
- r: number().int().min(0).max(255),
56815
- g: number().int().min(0).max(255),
56816
- b: number().int().min(0).max(255)
57574
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57575
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57576
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
56817
57577
  });
56818
57578
  var HsvTripletSchema = object({
56819
57579
  /** Hue in degrees, 0..360. */
56820
- h: number().min(0).max(360),
57580
+ h: number().min(0).max(360).meta(readingMeta("none")),
56821
57581
  /** Saturation as 0..100 inclusive. */
56822
- s: number().min(0).max(100),
57582
+ s: number().min(0).max(100).meta(readingMeta("none")),
56823
57583
  /** Value/brightness as 0..100 inclusive. */
56824
- v: number().min(0).max(100)
57584
+ v: number().min(0).max(100).meta(readingMeta("none"))
56825
57585
  });
56826
57586
  var ColorInputSchema = discriminatedUnion("mode", [
56827
57587
  object({
@@ -56850,7 +57610,7 @@ object({
56850
57610
  /** Populated when `mode === 'hsv'`. */
56851
57611
  hsv: HsvTripletSchema.optional(),
56852
57612
  /** Populated when `mode === 'mired'`. */
56853
- mireds: number().int().optional(),
57613
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
56854
57614
  /** Ms epoch of the last operator-driven change. */
56855
57615
  lastChangedAt: number()
56856
57616
  });
@@ -56974,18 +57734,12 @@ _enum([
56974
57734
  "empty",
56975
57735
  "full"
56976
57736
  ]);
56977
- /** Units a remaining-life counter is reported in. Closed: the panel renders
56978
- * each. */
56979
- var ConsumableRemainingUnitSchema = _enum([
56980
- "days",
56981
- "hours",
56982
- "cycles"
56983
- ]);
56984
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
56985
- * days). `value` null = unknown (D393). */
57737
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57738
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57739
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
56986
57740
  var ConsumableRemainingSchema = object({
56987
- value: number().min(0).nullable(),
56988
- unit: ConsumableRemainingUnitSchema
57741
+ value: number().min(0).nullable().meta(readingMeta("device")),
57742
+ kind: ConsumableRemainingKindSchema
56989
57743
  });
56990
57744
  var ConsumablesStatusSchema = object({
56991
57745
  items: array(object({
@@ -56994,7 +57748,7 @@ var ConsumablesStatusSchema = object({
56994
57748
  /** Display name. */
56995
57749
  label: string().min(1),
56996
57750
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
56997
- level: number().min(0).max(100).nullable(),
57751
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
56998
57752
  /** The provider's discrete verdict when known (binary mode). */
56999
57753
  status: ConsumableStatusSchema.nullable(),
57000
57754
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57011,7 +57765,7 @@ var ConsumablesStatusSchema = object({
57011
57765
  })),
57012
57766
  lastChangedAt: number()
57013
57767
  });
57014
- Object.values(DeviceType), [...ConsumableRemainingUnitSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57768
+ Object.values(DeviceType), [...ConsumableRemainingKindSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57015
57769
  Object.values(DeviceType), method(object({
57016
57770
  deviceId: number().int().nonnegative(),
57017
57771
  key: string().min(1)
@@ -57036,39 +57790,17 @@ object({
57036
57790
  /** Current value. Numeric kind → number; select/text/datetime → string.
57037
57791
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
57038
57792
  * or time-only — HA accepts all three forms). */
57039
- value: union([number(), string()]),
57793
+ value: union([number().meta(readingMeta("device")), string()]),
57040
57794
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
57041
57795
  options: array(string()),
57042
57796
  /** Ms epoch when the slice was last updated. */
57043
57797
  lastChangedAt: number(),
57044
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
57045
- * Populated live from HA `attributes.unit_of_measurement` on each push.
57046
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57047
- unit: string().optional(),
57048
- /** Minimum allowed value. Populated live from HA `attributes.min`.
57049
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57050
- min: number().optional(),
57051
- /** Maximum allowed value. Populated live from HA `attributes.max`.
57052
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57053
- max: number().optional(),
57054
- /** Step increment. Populated live from HA `attributes.step`.
57055
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57056
- step: number().optional(),
57057
- /** Suggested decimal places for numeric display. Populated live from HA
57058
- * `attributes.suggested_display_precision`, or derived from `step` when the
57059
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
57060
- * other kinds. Falls back to auto-formatting when absent. */
57061
- precision: number().int().min(0).max(10).optional(),
57062
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
57063
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
57064
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
57065
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
57066
- * `has_date`/`has_time` attributes on each push. */
57067
- format: _enum([
57068
- "date",
57069
- "time",
57070
- "datetime"
57071
- ]).optional()
57798
+ /** Minimum allowed value, in the device quantity's base unit. */
57799
+ min: number().optional().meta(readingMeta("device")),
57800
+ /** Maximum allowed value, in the device quantity's base unit. */
57801
+ max: number().optional().meta(readingMeta("device")),
57802
+ /** Step increment, in the device quantity's base unit. */
57803
+ step: number().optional().meta(readingMeta("device"))
57072
57804
  });
57073
57805
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
57074
57806
  object({
@@ -57106,9 +57838,9 @@ object({
57106
57838
  ]),
57107
57839
  /** 0 = fully closed, 100 = fully open. Null when the device has
57108
57840
  * no intermediate position surface. */
57109
- position: number().min(0).max(100).nullable(),
57841
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57110
57842
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
57111
- tiltPosition: number().min(0).max(100).nullable(),
57843
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57112
57844
  /** Ms epoch when the slice was last updated. */
57113
57845
  lastChangedAt: number()
57114
57846
  });
@@ -57166,9 +57898,9 @@ var NormalizedRangeSchema$1 = object({
57166
57898
  object({
57167
57899
  mode: DayNightModeSchema,
57168
57900
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
57169
- sensitivity: number().optional(),
57901
+ sensitivity: number().optional().meta(readingMeta("none")),
57170
57902
  /** Delay before the IR-cut filter flips, in seconds. */
57171
- switchDelaySec: number().optional(),
57903
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
57172
57904
  lastFetchedAt: number()
57173
57905
  });
57174
57906
  /**
@@ -57323,11 +58055,11 @@ object({
57323
58055
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
57324
58056
  availablePrograms: array(string()),
57325
58057
  /** Seconds remaining on the active cycle, or null when not running / not known. */
57326
- remainingSeconds: number().int().nonnegative().nullable(),
58058
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57327
58059
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
57328
- delayedStartSeconds: number().int().nonnegative().nullable(),
58060
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57329
58061
  /** Cycle completion 0..100, or null when not applicable. */
57330
- progressPercent: number().min(0).max(100).nullable(),
58062
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57331
58063
  /** True when the door is reported open; null when the door state is not known. */
57332
58064
  doorOpen: boolean().nullable(),
57333
58065
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -57351,7 +58083,7 @@ object({
57351
58083
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57352
58084
  machineCareDue: boolean().nullable(),
57353
58085
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57354
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
58086
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
57355
58087
  /** Option states: null when the appliance does not expose the option or has not said. */
57356
58088
  halfLoad: boolean().nullable(),
57357
58089
  hygienePlus: boolean().nullable(),
@@ -57366,7 +58098,7 @@ object({
57366
58098
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
57367
58099
  activeEvents: array(string()),
57368
58100
  /** Numeric error code when the appliance reports one; null otherwise. */
57369
- errorCode: number().nullable(),
58101
+ errorCode: number().nullable().meta(readingMeta("none")),
57370
58102
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
57371
58103
  errorLabel: string().nullable(),
57372
58104
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -57512,24 +58244,14 @@ object({
57512
58244
  /** Ms epoch of the last press. null = never observed since this provider started. */
57513
58245
  lastPressedAt: number().nullable(),
57514
58246
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
57515
- pressCountSinceStart: number()
58247
+ pressCountSinceStart: number().meta(readingMeta("count"))
57516
58248
  });
57517
58249
  object({
57518
58250
  deviceId: number(),
57519
58251
  timestamp: number()
57520
58252
  });
57521
58253
  DeviceType.Button, DeviceType.Camera;
57522
- /**
57523
- * Enum-state sensor — a string value picked from a finite option set.
57524
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
57525
- * states, weather conditions, contact-source channels, mode strings).
57526
- * The option set is stable across observations; it is persisted in the
57527
- * device config blob (via the control slice at adoption) and is NOT
57528
- * stored in `sourceInfo`.
57529
- */
57530
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
57531
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
57532
- var EnumSensorDateTimeFormatSchema = _enum([
58254
+ _enum([
57533
58255
  "date",
57534
58256
  "time",
57535
58257
  "datetime"
@@ -57537,12 +58259,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
57537
58259
  object({
57538
58260
  value: string(),
57539
58261
  /**
57540
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
57541
- * as a locale date/time/datetime; absent for plain enum sensors. HA
57542
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
57543
- * a time-only sensor → `'time'`.
58262
+ * Set when `value` is a timestamp rather than an enum state, so history
58263
+ * does not learn ISO strings as states. The display shape (date / time /
58264
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
57544
58265
  */
57545
- format: EnumSensorDateTimeFormatSchema.optional(),
58266
+ timestamp: boolean().optional(),
57546
58267
  /** Ms epoch when the slice was last updated. */
57547
58268
  lastFetchedAt: number()
57548
58269
  });
@@ -57561,13 +58282,13 @@ var EventFireSchema = object({
57561
58282
  deviceId: number(),
57562
58283
  eventType: string(),
57563
58284
  data: record(string(), unknown()).nullable(),
57564
- timestamp: number(),
57565
- seq: number()
58285
+ timestamp: number().meta(readingMeta("none")),
58286
+ seq: number().meta(readingMeta("count"))
57566
58287
  });
57567
58288
  object({
57568
58289
  eventTypes: array(string()),
57569
58290
  lastEvent: EventFireSchema.nullable(),
57570
- eventCountSinceStart: number()
58291
+ eventCountSinceStart: number().meta(readingMeta("count"))
57571
58292
  });
57572
58293
  DeviceType.EventEmitter;
57573
58294
  var IdentitySchema = object({
@@ -57956,10 +58677,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
57956
58677
  object({
57957
58678
  /** Active speed as 0..100 inclusive. Null when the device has no
57958
58679
  * speed surface (single-speed fan). */
57959
- percentage: number().min(0).max(100).nullable(),
58680
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57960
58681
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
57961
58682
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
57962
- percentageStep: number().positive().optional(),
58683
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
57963
58684
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
57964
58685
  * — empty string when no preset is active or supported. */
57965
58686
  preset: string(),
@@ -58041,7 +58762,7 @@ var FeatureProbeStatusSchema = object({
58041
58762
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
58042
58763
  model: string().nullable(),
58043
58764
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
58044
- channelCount: number().nullable(),
58765
+ channelCount: number().nullable().meta(readingMeta("count")),
58045
58766
  /**
58046
58767
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
58047
58768
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -58073,9 +58794,45 @@ object({
58073
58794
  lastChangedAt: number()
58074
58795
  });
58075
58796
  DeviceType.Sensor;
58797
+ object({
58798
+ /** One row per member that has been commanded. A later command replaces that member's row. */
58799
+ results: array(object({
58800
+ deviceId: number().int().nonnegative(),
58801
+ cap: string().min(1),
58802
+ method: string().min(1),
58803
+ ok: boolean(),
58804
+ error: string().optional(),
58805
+ /** Ms epoch of the attempt. */
58806
+ at: number()
58807
+ })) });
58808
+ DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
58809
+ object({
58810
+ active: boolean().meta({ label: "Active" }),
58811
+ /** Ms epoch when the slice was last updated. */
58812
+ lastChangedAt: number()
58813
+ });
58814
+ DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Climate, DeviceType.Thermostat;
58815
+ object({
58816
+ /** Members whose current value entered the aggregate. */
58817
+ included: number().int().nonnegative().meta(readingMeta("count", {
58818
+ precision: 0,
58819
+ label: "Reporting"
58820
+ })),
58821
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
58822
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
58823
+ precision: 0,
58824
+ label: "Excluded"
58825
+ })),
58826
+ /** `included` + `excluded`. */
58827
+ total: number().int().nonnegative().meta(readingMeta("count", {
58828
+ precision: 0,
58829
+ label: "Members"
58830
+ }))
58831
+ });
58832
+ DeviceType.Sensor, DeviceType.Switch, DeviceType.Light, DeviceType.Fan, DeviceType.Cover, DeviceType.Valve, DeviceType.Lock, DeviceType.MediaPlayer, DeviceType.Thermostat, DeviceType.Climate;
58076
58833
  object({
58077
58834
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
58078
- position: number().min(0).max(100).nullable(),
58835
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58079
58836
  /** Ms epoch when the slice was last updated (push or poll). */
58080
58837
  lastFetchedAt: number()
58081
58838
  });
@@ -58084,9 +58841,9 @@ object({
58084
58841
  /** Whether the humidifier is currently on. */
58085
58842
  on: boolean(),
58086
58843
  /** Current measured relative humidity (0..100). Null when not reported. */
58087
- currentHumidity: number().min(0).max(100).nullable(),
58844
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58088
58845
  /** Target relative humidity (0..100). Null when no setpoint surface. */
58089
- targetHumidity: number().min(0).max(100).nullable(),
58846
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58090
58847
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
58091
58848
  * device has no mode surface. */
58092
58849
  mode: string().nullable(),
@@ -58096,9 +58853,9 @@ object({
58096
58853
  * `idle` / `off`). Null when the device doesn't report it. */
58097
58854
  action: string().nullable(),
58098
58855
  /** HA `min_humidity` attribute. Null → UI uses 0. */
58099
- minHumidity: number().nullable(),
58856
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
58100
58857
  /** HA `max_humidity` attribute. Null → UI uses 100. */
58101
- maxHumidity: number().nullable(),
58858
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
58102
58859
  /** Ms epoch when the slice was last updated. */
58103
58860
  lastChangedAt: number()
58104
58861
  });
@@ -58122,22 +58879,13 @@ DeviceType.Humidifier, method(object({
58122
58879
  auth: "protected"
58123
58880
  });
58124
58881
  object({
58125
- /** Current relative humidity, 0..100. */
58126
- percent: number().min(0).max(100).meta({
58127
- unit: "%",
58882
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
58883
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
58884
+ precision: 0,
58128
58885
  label: "Humidity"
58129
- }),
58886
+ })),
58130
58887
  /** Ms epoch when the slice was last updated. */
58131
- lastFetchedAt: number(),
58132
- /** Live display unit from the upstream source (e.g. HA
58133
- * `attributes.unit_of_measurement`). The UI prefers this over the
58134
- * role's canonical unit. Absent → fall back to the canonical unit. */
58135
- unit: string().optional(),
58136
- /** Suggested decimal places for numeric display.
58137
- * Populated live from the upstream source when provided (e.g. HA
58138
- * `attributes.suggested_display_precision`). Falls back to
58139
- * auto-formatting when absent. */
58140
- precision: number().int().min(0).max(10).optional()
58888
+ lastFetchedAt: number()
58141
58889
  });
58142
58890
  DeviceType.Sensor, DeviceType.Thermostat;
58143
58891
  object({
@@ -58145,7 +58893,7 @@ object({
58145
58893
  * entity exposes no `entity_picture` (yet). */
58146
58894
  url: string().nullable(),
58147
58895
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
58148
- lastUpdated: number().nullable()
58896
+ lastUpdated: number().nullable().meta(readingMeta("none"))
58149
58897
  });
58150
58898
  DeviceType.Image;
58151
58899
  /**
@@ -58201,19 +58949,19 @@ var NormalizedRangeSchema = object({
58201
58949
  });
58202
58950
  object({
58203
58951
  /** Normalized 0–100. */
58204
- brightness: number().optional(),
58952
+ brightness: number().optional().meta(readingMeta("none")),
58205
58953
  /** Normalized 0–100. */
58206
- contrast: number().optional(),
58954
+ contrast: number().optional().meta(readingMeta("none")),
58207
58955
  /** Normalized 0–100. */
58208
- saturation: number().optional(),
58956
+ saturation: number().optional().meta(readingMeta("none")),
58209
58957
  /** Normalized 0–100. */
58210
- sharpness: number().optional(),
58958
+ sharpness: number().optional().meta(readingMeta("none")),
58211
58959
  mirror: boolean().optional(),
58212
58960
  flip: boolean().optional(),
58213
58961
  rotate: ImageRotateSchema.optional(),
58214
58962
  whiteBalance: WhiteBalanceModeSchema.optional(),
58215
58963
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
58216
- warmth: number().optional(),
58964
+ warmth: number().optional().meta(readingMeta("none")),
58217
58965
  exposureMode: ExposureModeSchema.optional(),
58218
58966
  backlightMode: BacklightModeSchema.optional(),
58219
58967
  lastFetchedAt: number()
@@ -58458,10 +59206,10 @@ var IntercomAbilitySchema = object({
58458
59206
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
58459
59207
  codecs: array(string()),
58460
59208
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
58461
- sampleRate: number(),
59209
+ sampleRate: number().meta(readingMeta("none")),
58462
59210
  duplex: _enum(["half", "full"]),
58463
59211
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
58464
- maxBacklogMs: number()
59212
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
58465
59213
  });
58466
59214
  var IntercomStatusSchema = object({
58467
59215
  /** True while a talk session is open. */
@@ -58572,13 +59320,13 @@ object({
58572
59320
  activity: LawnMowerActivitySchema,
58573
59321
  /** 0..100 battery percentage. Null when the device has no battery
58574
59322
  * reading. */
58575
- batteryLevel: number().min(0).max(100).nullable(),
59323
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58576
59324
  /** 0..100 mowing-completion percentage of the current task, or null when no
58577
59325
  * task is active / progress is unavailable. */
58578
- progressPercent: number().min(0).max(100).nullable(),
59326
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58579
59327
  /** Current device/event code (dynamic — mostly status, sometimes an error),
58580
59328
  * or null when unknown. */
58581
- currentCode: number().nullable(),
59329
+ currentCode: number().nullable().meta(readingMeta("none")),
58582
59330
  /** Human label for {@link currentCode}, or null when undecodable. */
58583
59331
  currentCodeLabel: string().nullable(),
58584
59332
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -58988,7 +59736,14 @@ DeviceType.Lock, method(object({
58988
59736
  auth: "protected"
58989
59737
  }), method(object({
58990
59738
  deviceId: number().int().nonnegative(),
58991
- code: string().min(1).optional()
59739
+ code: string().min(1).optional(),
59740
+ /**
59741
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
59742
+ * with one call is refused unless the caller says it was confirmed. A
59743
+ * single lock ignores it. The refusal lives in the callee, so no
59744
+ * automation or API caller reaches the doors by skipping a dialog.
59745
+ */
59746
+ confirm: boolean().optional()
58992
59747
  }), _void(), {
58993
59748
  kind: "mutation",
58994
59749
  auth: "protected"
@@ -58996,6 +59751,11 @@ DeviceType.Lock, method(object({
58996
59751
  kind: "mutation",
58997
59752
  auth: "protected"
58998
59753
  });
59754
+ object({
59755
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
59756
+ lastFetchedAt: number()
59757
+ });
59758
+ DeviceType.Sensor;
58999
59759
  /**
59000
59760
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
59001
59761
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
@@ -59040,7 +59800,7 @@ object({
59040
59800
  state: MediaPlayerStateSchema,
59041
59801
  /** Volume as 0..100 inclusive. Null when the device has no volume
59042
59802
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
59043
- volumeLevel: number().min(0).max(100).nullable(),
59803
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59044
59804
  /** Mute toggle distinct from volume=0. Null when no mute surface.
59045
59805
  * Pair with `DeviceFeature.MediaPlayerMute`. */
59046
59806
  isMuted: boolean().nullable(),
@@ -59053,10 +59813,10 @@ object({
59053
59813
  currentMedia: MediaInfoSchema.nullable(),
59054
59814
  /** Current playback position in ms. Null when not seekable or
59055
59815
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
59056
- positionMs: number().int().nonnegative().nullable(),
59816
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59057
59817
  /** Total duration of the current media in ms. Null when unknown
59058
59818
  * (live stream) or nothing is playing. */
59059
- durationMs: number().int().nonnegative().nullable(),
59819
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59060
59820
  /** Shuffle toggle. Null when no shuffle surface.
59061
59821
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
59062
59822
  shuffle: boolean().nullable(),
@@ -59356,7 +60116,7 @@ object({
59356
60116
  * it, every provider seeded it at boot, and every session left it
59357
60117
  * `detected: true` after its detach.
59358
60118
  */
59359
- autoClearAfterMs: number().nullable()
60119
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
59360
60120
  });
59361
60121
  /**
59362
60122
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -59406,7 +60166,7 @@ var MotionZoneRegionSchema = object({
59406
60166
  });
59407
60167
  object({
59408
60168
  enabled: boolean(),
59409
- sensitivity: number(),
60169
+ sensitivity: number().meta(readingMeta("none")),
59410
60170
  /** Grid region(s). Today exactly one `grid` shape. */
59411
60171
  regions: array(MotionZoneRegionSchema),
59412
60172
  lastFetchedAt: number()
@@ -59710,7 +60470,7 @@ var NavigationStatusSchema = object({
59710
60470
  following: boolean(),
59711
60471
  flash: boolean(),
59712
60472
  lightMode: NavigationLightModeSchema,
59713
- lightLevel: number().min(40).max(100),
60473
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
59714
60474
  /** Ms epoch when the slice was last updated. */
59715
60475
  lastChangedAt: number()
59716
60476
  });
@@ -59756,13 +60516,13 @@ var NetworkLinkStatusSchema = object({
59756
60516
  * one whose reading has not landed must not be drawn at 0 %. Consumers
59757
60517
  * SKIP a null rather than coerce it.
59758
60518
  */
59759
- signalPercent: number().min(0).max(100).nullable(),
60519
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59760
60520
  /** Raw received signal strength in dBm, when the firmware reports one. */
59761
- rssiDbm: number().optional(),
60521
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
59762
60522
  /** Network name of a wireless link, when the firmware reports it. */
59763
60523
  ssid: string().optional(),
59764
60524
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
59765
- lastUpdated: number()
60525
+ lastUpdated: number().meta(readingMeta("none"))
59766
60526
  });
59767
60527
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
59768
60528
  deviceId: number(),
@@ -60081,7 +60841,7 @@ object({
60081
60841
  lastError: string().nullable(),
60082
60842
  /** Number of deliveries currently buffered server-side (rate-limited
60083
60843
  * notifiers may queue). 0 when the provider sends synchronously. */
60084
- queueDepth: number().int().nonnegative(),
60844
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
60085
60845
  /** Per-feature capability matrix — authoritative source for the
60086
60846
  * compose-form field gating. */
60087
60847
  supports: NotifierSupportsSchema
@@ -60131,15 +60891,8 @@ DeviceType.Notifier, method(NotifierSendInputSchema, NotifierSendResultSchema, {
60131
60891
  acceptedAt: number()
60132
60892
  });
60133
60893
  object({
60134
- value: number(),
60135
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
60136
- * Populated live from the upstream source on each state push.
60137
- * Absent for unitless sensors. */
60138
- unit: string().optional(),
60139
- /** Suggested decimal places for numeric display.
60140
- * Populated live from the upstream source when provided.
60141
- * Falls back to auto-formatting when absent. */
60142
- precision: number().int().min(0).max(10).optional(),
60894
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
60895
+ value: number().nullable().meta(readingMeta("device")),
60143
60896
  /** Ms epoch when the slice was last updated. */
60144
60897
  lastFetchedAt: number()
60145
60898
  });
@@ -60429,23 +61182,26 @@ object({
60429
61182
  /** Food currently in the bowl (grams). Null when the device has not
60430
61183
  * reported a reading yet. On dual-hopper models this is the combined
60431
61184
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
60432
- foodLevel: number().nullable(),
61185
+ foodLevel: number().nullable().meta(readingMeta("none")),
60433
61186
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
60434
61187
  * single-hopper models. */
60435
- food1: number().nullable(),
61188
+ food1: number().nullable().meta(readingMeta("none")),
60436
61189
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
60437
61190
  * single-hopper models. */
60438
- food2: number().nullable(),
61191
+ food2: number().nullable().meta(readingMeta("none")),
60439
61192
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
60440
61193
  * (`device_class: problem`, on = low). True when the bowl is empty /
60441
61194
  * below the feeder's low threshold. */
60442
61195
  lowFood: boolean(),
60443
61196
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
60444
61197
  * device has no battery reading. */
60445
- batteryPower: number().min(0).max(100).nullable(),
60446
- /** Days of desiccant life remaining. Null when the model has no
60447
- * desiccant sensor. */
60448
- desiccantLeftDays: number().nullable(),
61198
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61199
+ /**
61200
+ * Desiccant life remaining, stored in seconds (duration base, D944).
61201
+ * Providers convert from the days the feeder reports. Null when the
61202
+ * model has no desiccant sensor.
61203
+ */
61204
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
60449
61205
  /** True while a feed is in progress. */
60450
61206
  feeding: boolean(),
60451
61207
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -60456,7 +61212,7 @@ object({
60456
61212
  * with `errorCode` for consumers that want the raw integer. */
60457
61213
  error: string().nullable(),
60458
61214
  /** Raw device error code (0 / null = no error). */
60459
- errorCode: number().nullable(),
61215
+ errorCode: number().nullable().meta(readingMeta("none")),
60460
61216
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
60461
61217
  isDualHopper: boolean(),
60462
61218
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -60466,7 +61222,7 @@ object({
60466
61222
  /** Play a chime when dispensing. */
60467
61223
  feedSound: boolean(),
60468
61224
  /** Speaker / prompt volume level (device-scaled integer). */
60469
- volume: number(),
61225
+ volume: number().meta(readingMeta("none")),
60470
61226
  /** Ms epoch when the slice was last refreshed from the cloud. */
60471
61227
  lastFetchedAt: number()
60472
61228
  });
@@ -60903,38 +61659,37 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
60903
61659
  });
60904
61660
  object({
60905
61661
  /** Instantaneous power draw in watts. */
60906
- watts: number().optional().meta({
60907
- unit: "W",
60908
- label: "Power"
60909
- }),
61662
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
60910
61663
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
60911
- kwhTotal: number().optional().meta({
60912
- unit: "kWh",
60913
- label: "Energy"
60914
- }),
61664
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
60915
61665
  /** Voltage in volts. */
60916
- volts: number().optional(),
61666
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
60917
61667
  /** Current in amperes. */
60918
- amps: number().optional(),
60919
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
60920
- va: number().optional(),
61668
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
61669
+ /** Apparent power in volt-amperes. */
61670
+ va: number().optional().meta(readingMeta("apparent-power")),
60921
61671
  /** Power factor (0..1) if reported. */
60922
- powerFactor: number().min(0).max(1).optional(),
61672
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
60923
61673
  /** Ms epoch when the slice was last updated. */
60924
- lastFetchedAt: number(),
60925
- /** Live display unit of the single metric this slice carries (e.g. HA
60926
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
60927
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
60928
- * unit per slice is unambiguous. The UI prefers this over the role's
60929
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
60930
- unit: string().optional(),
60931
- /** Suggested decimal places for numeric display.
60932
- * Populated live from the upstream source when provided (e.g. HA
60933
- * `attributes.suggested_display_precision`). Falls back to
60934
- * auto-formatting when absent. */
60935
- precision: number().int().min(0).max(10).optional()
61674
+ lastFetchedAt: number()
60936
61675
  });
60937
- DeviceType.Sensor;
61676
+ [
61677
+ DeviceType.Sensor,
61678
+ DeviceType.Switch,
61679
+ DeviceType.Light,
61680
+ DeviceType.Fan,
61681
+ DeviceType.Climate,
61682
+ DeviceType.Thermostat,
61683
+ DeviceType.Humidifier,
61684
+ DeviceType.WaterHeater,
61685
+ DeviceType.MediaPlayer,
61686
+ DeviceType.Washer,
61687
+ DeviceType.Dryer,
61688
+ DeviceType.Dishwasher,
61689
+ DeviceType.Vacuum,
61690
+ DeviceType.LawnMower,
61691
+ DeviceType.PetFeeder
61692
+ ].filter((type) => type !== DeviceType.Sensor);
60938
61693
  /**
60939
61694
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
60940
61695
  * on `DeviceType.Presence`. Read-only — no setters: presence is
@@ -60953,11 +61708,11 @@ DeviceType.Sensor;
60953
61708
  */
60954
61709
  var GpsLocationSchema = object({
60955
61710
  /** Latitude in decimal degrees, -90..90. */
60956
- latitude: number().min(-90).max(90),
61711
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
60957
61712
  /** Longitude in decimal degrees, -180..180. */
60958
- longitude: number().min(-180).max(180),
61713
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
60959
61714
  /** Reported accuracy in meters (lower = better). */
60960
- accuracyMeters: number().nonnegative()
61715
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
60961
61716
  });
60962
61717
  object({
60963
61718
  /** `home` / `not_home` / any user-defined zone name. */
@@ -60969,25 +61724,16 @@ object({
60969
61724
  gps: GpsLocationSchema.nullable(),
60970
61725
  /** Optional battery level of the tracking device (0..100). Null
60971
61726
  * when not reported. */
60972
- batteryPercent: number().min(0).max(100).nullable(),
61727
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60973
61728
  /** Ms epoch when the slice was last updated. */
60974
61729
  lastChangedAt: number()
60975
61730
  });
60976
61731
  DeviceType.Presence;
60977
61732
  object({
60978
- /** Current pressure in hPa. */
60979
- hpa: number(),
61733
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
61734
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
60980
61735
  /** Ms epoch when the slice was last updated. */
60981
- lastFetchedAt: number(),
60982
- /** Live display unit from the upstream source (e.g. HA
60983
- * `attributes.unit_of_measurement`). The UI prefers this over the
60984
- * role's canonical unit. Absent → fall back to the canonical unit. */
60985
- unit: string().optional(),
60986
- /** Suggested decimal places for numeric display.
60987
- * Populated live from the upstream source when provided (e.g. HA
60988
- * `attributes.suggested_display_precision`). Falls back to
60989
- * auto-formatting when absent. */
60990
- precision: number().int().min(0).max(10).optional()
61736
+ lastFetchedAt: number()
60991
61737
  });
60992
61738
  DeviceType.Sensor;
60993
61739
  /**
@@ -61412,8 +62158,8 @@ var ptzCapability = {
61412
62158
  };
61413
62159
  var PtzAutotrackSettingsSchema = object({
61414
62160
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
61415
- stopDelaySeconds: number().int().min(0).max(300),
61416
- disappearDelaySeconds: number().int().min(0).max(300)
62161
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
62162
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
61417
62163
  });
61418
62164
  /**
61419
62165
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -61470,7 +62216,7 @@ object({
61470
62216
  /** null = unknown — never coerced to false (D393). */
61471
62217
  running: boolean().nullable(),
61472
62218
  /** Epoch ms the current boolean began; null while `running` is null. */
61473
- since: number().nullable(),
62219
+ since: number().nullable().meta(readingMeta("none")),
61474
62220
  source: RunningStateSourceSchema.meta({ label: "Source" }),
61475
62221
  phase: string().nullable(),
61476
62222
  /** null until a reading has been published (D393). */
@@ -61482,7 +62228,7 @@ object({
61482
62228
  phase: string().nullable()
61483
62229
  });
61484
62230
  Object.values(DeviceType);
61485
- DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
62231
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher, DeviceType.Vacuum, DeviceType.LawnMower, DeviceType.MediaPlayer, DeviceType.WaterHeater, DeviceType.Humidifier, DeviceType.Fan, DeviceType.Climate, DeviceType.Thermostat, DeviceType.PetFeeder;
61486
62232
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Switch, method(object({ deviceId: number().int().nonnegative() }), object({ success: literal(true) }), {
61487
62233
  kind: "mutation",
61488
62234
  auth: "admin"
@@ -62668,9 +63414,9 @@ var RecordTriggerSchema = _enum([
62668
63414
  */
62669
63415
  var RecordWindowSchema = object({
62670
63416
  trigger: RecordTriggerSchema,
62671
- day: number().int().min(0).max(6),
62672
- startMinute: number().int().min(0).max(1439),
62673
- endMinute: number().int().min(1).max(1440)
63417
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
63418
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
63419
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
62674
63420
  });
62675
63421
  /** Status of one physical volume, as the camera itself describes it. */
62676
63422
  var OnboardStorageVolumeSchema = object({
@@ -62739,10 +63485,10 @@ object({
62739
63485
  /** Master "record to the card at all" switch. */
62740
63486
  enabled: boolean().nullable(),
62741
63487
  overwriteWhenFull: boolean().nullable(),
62742
- preRecordSec: number().nullable(),
62743
- postRecordSec: number().nullable(),
63488
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
63489
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
62744
63490
  /** Length of one recorded file, in minutes. */
62745
- segmentMinutes: number().nullable(),
63491
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
62746
63492
  /** The primary track's weekly windows, flattened. */
62747
63493
  windows: array(RecordWindowSchema),
62748
63494
  /**
@@ -62758,7 +63504,7 @@ object({
62758
63504
  * provider reporting a non-zero count also reports the schedule as not
62759
63505
  * writable.
62760
63506
  */
62761
- unreadableWindows: number(),
63507
+ unreadableWindows: number().meta(readingMeta("count")),
62762
63508
  /**
62763
63509
  * The camera is scheduled to record and has NO usable storage.
62764
63510
  *
@@ -63395,7 +64141,7 @@ var CamStreamDescriptorSchema = object({
63395
64141
  url: string().optional(),
63396
64142
  codec: string().optional(),
63397
64143
  resolution: CamStreamResolutionSchema.optional(),
63398
- fps: number().positive().optional(),
64144
+ fps: number().positive().optional().meta(readingMeta("rate")),
63399
64145
  label: string().optional(),
63400
64146
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
63401
64147
  deviceFeatures: array(string()).optional(),
@@ -63425,18 +64171,18 @@ var StreamProfileSchema = _enum([
63425
64171
  ]);
63426
64172
  /** The current encoding config of one stream profile. */
63427
64173
  var StreamProfileConfigSchema = object({
63428
- width: number(),
63429
- height: number(),
64174
+ width: number().meta(readingMeta("none")),
64175
+ height: number().meta(readingMeta("none")),
63430
64176
  codec: _enum(["h264", "h265"]),
63431
- framerate: number(),
63432
- bitrate: number(),
64177
+ framerate: number().meta(readingMeta("rate")),
64178
+ bitrate: number().meta(readingMeta("bitrate")),
63433
64179
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
63434
64180
  encoderProfile: _enum([
63435
64181
  "high",
63436
64182
  "main",
63437
64183
  "baseline"
63438
64184
  ]).optional(),
63439
- gop: number().optional(),
64185
+ gop: number().optional().meta(readingMeta("none")),
63440
64186
  /**
63441
64187
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
63442
64188
  *
@@ -63521,7 +64267,7 @@ object({
63521
64267
  /** Ms epoch of the last state change. Useful for UI "X minutes ago". */
63522
64268
  lastChangedAt: number()
63523
64269
  });
63524
- DeviceType.Switch, DeviceType.Siren, DeviceType.Light, method(object({
64270
+ DeviceType.Switch, DeviceType.Siren, DeviceType.Light, DeviceType.Fan, method(object({
63525
64271
  deviceId: number(),
63526
64272
  on: boolean()
63527
64273
  }), _void(), {
@@ -64079,23 +64825,13 @@ object({
64079
64825
  });
64080
64826
  DeviceType.Sensor;
64081
64827
  object({
64082
- /** Current temperature in Celsius. */
64083
- celsius: number().meta({
64084
- unit: "°C",
64828
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
64829
+ celsius: number().nullable().meta(readingMeta("temperature", {
64830
+ precision: 1,
64085
64831
  label: "Temperature"
64086
- }),
64832
+ })),
64087
64833
  /** Ms epoch when the slice was last updated (push or poll). */
64088
- lastFetchedAt: number(),
64089
- /** Live display unit from the upstream source (e.g. HA
64090
- * `attributes.unit_of_measurement`). The UI prefers this over the
64091
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
64092
- * Absent → the UI falls back to the role's canonical unit. */
64093
- unit: string().optional(),
64094
- /** Suggested decimal places for numeric display.
64095
- * Populated live from the upstream source when provided (e.g. HA
64096
- * `attributes.suggested_display_precision`). Falls back to
64097
- * auto-formatting when absent. */
64098
- precision: number().int().min(0).max(10).optional()
64834
+ lastFetchedAt: number()
64099
64835
  });
64100
64836
  DeviceType.Sensor, DeviceType.Thermostat;
64101
64837
  /**
@@ -64347,16 +65083,16 @@ object({
64347
65083
  ]),
64348
65084
  /** 0..100 battery percentage. Null when the device has no battery
64349
65085
  * reading. */
64350
- batteryLevel: number().min(0).max(100).nullable(),
65086
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64351
65087
  /** Current fan-speed token (provider-verbatim). Null when unknown or
64352
65088
  * the vacuum has no speed control. */
64353
65089
  fanSpeed: string().nullable(),
64354
65090
  /** Speed tokens the hardware accepts — drives the UI selector. */
64355
65091
  availableFanSpeeds: array(string()),
64356
65092
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64357
- progressPercent: number().min(0).max(100).nullable(),
65093
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64358
65094
  /** Current error code (0 / null = no error). */
64359
- errorCode: number().nullable(),
65095
+ errorCode: number().nullable().meta(readingMeta("none")),
64360
65096
  /** Human label for {@link errorCode}, or null when none / undecodable. */
64361
65097
  errorLabel: string().nullable(),
64362
65098
  /** Ms epoch when the slice was last updated. */
@@ -64395,7 +65131,7 @@ object({
64395
65131
  ]),
64396
65132
  /** 0 = fully closed, 100 = fully open. Null when the device has no
64397
65133
  * intermediate position surface. */
64398
- position: number().min(0).max(100).nullable(),
65134
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64399
65135
  /** Ms epoch when the slice was last updated. */
64400
65136
  lastChangedAt: number()
64401
65137
  });
@@ -64423,9 +65159,9 @@ object({
64423
65159
  DeviceType.Sensor;
64424
65160
  object({
64425
65161
  /** Current measured temperature. Null when not reported. */
64426
- currentTemp: number().nullable(),
65162
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
64427
65163
  /** Target temperature setpoint. Null when no setpoint surface. */
64428
- targetTemp: number().nullable(),
65164
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
64429
65165
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
64430
65166
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
64431
65167
  * device reports an unknown state. */
@@ -64436,9 +65172,9 @@ object({
64436
65172
  * has no away surface. */
64437
65173
  away: boolean().nullable(),
64438
65174
  /** HA `min_temp` attribute. Null when not reported. */
64439
- minTemp: number().nullable(),
65175
+ minTemp: number().nullable().meta(readingMeta("temperature")),
64440
65176
  /** HA `max_temp` attribute. Null when not reported. */
64441
- maxTemp: number().nullable(),
65177
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
64442
65178
  /** Ms epoch when the slice was last updated. */
64443
65179
  lastChangedAt: number()
64444
65180
  });
@@ -64462,25 +65198,18 @@ DeviceType.WaterHeater, method(object({
64462
65198
  auth: "protected"
64463
65199
  });
64464
65200
  object({
64465
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
64466
- * when no condition has been reported yet. */
65201
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
64467
65202
  condition: string().nullable(),
64468
- /** Current temperature in the reported unit. Null when not provided. */
64469
- temperature: number().nullable(),
64470
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
64471
- temperatureUnit: string().nullable(),
65203
+ /** Current temperature in Celsius. Null when not provided or refused. */
65204
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
64472
65205
  /** Relative humidity (0..100). Null when not provided. */
64473
- humidity: number().min(0).max(100).nullable(),
64474
- /** Barometric pressure in the reported unit. Null when not provided. */
64475
- pressure: number().nullable(),
64476
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
64477
- pressureUnit: string().nullable(),
64478
- /** Wind speed in the reported unit. Null when not provided. */
64479
- windSpeed: number().nullable(),
64480
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
64481
- windSpeedUnit: string().nullable(),
65206
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
65207
+ /** Barometric pressure in hPa. Null when not provided or refused. */
65208
+ pressure: number().nullable().meta(readingMeta("pressure")),
65209
+ /** Wind speed in metres per second. Null when not provided or refused. */
65210
+ windSpeed: number().nullable().meta(readingMeta("speed")),
64482
65211
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
64483
- windBearing: number().nullable(),
65212
+ windBearing: number().nullable().meta(readingMeta("angle")),
64484
65213
  /** Ms epoch when the slice was last updated. */
64485
65214
  lastFetchedAt: number()
64486
65215
  });
@@ -65230,8 +65959,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
65230
65959
  kind: "mutation",
65231
65960
  auth: "admin"
65232
65961
  });
65962
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
65233
65963
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
65234
- 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;
65964
+ 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;
65235
65965
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
65236
65966
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
65237
65967
  new Set(CAP_NAMES);
@@ -66109,7 +66839,7 @@ var BaseDeviceProvider = class extends BaseAddon {
66109
66839
  return toDeviceSummary(device, this.addonId);
66110
66840
  }
66111
66841
  };
66112
- 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;
66842
+ 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;
66113
66843
  new Set(Object.values(DeviceType));
66114
66844
  Object.freeze({
66115
66845
  "accessories.getStatus": {
@@ -66217,6 +66947,13 @@ Object.freeze({
66217
66947
  access: "view",
66218
66948
  role: "admin"
66219
66949
  },
66950
+ "addons.getPackageDocs": {
66951
+ capName: "addons",
66952
+ capScope: "system",
66953
+ addonId: null,
66954
+ access: "view",
66955
+ role: "admin"
66956
+ },
66220
66957
  "addons.getVersions": {
66221
66958
  capName: "addons",
66222
66959
  capScope: "system",
@@ -69325,6 +70062,27 @@ Object.freeze({
69325
70062
  access: "view",
69326
70063
  role: "viewer"
69327
70064
  },
70065
+ "groupActive.getStatus": {
70066
+ capName: "group-active",
70067
+ capScope: "device",
70068
+ addonId: null,
70069
+ access: "view",
70070
+ role: "viewer"
70071
+ },
70072
+ "groupCommands.getStatus": {
70073
+ capName: "group-commands",
70074
+ capScope: "device",
70075
+ addonId: null,
70076
+ access: "view",
70077
+ role: "viewer"
70078
+ },
70079
+ "groupStatus.getStatus": {
70080
+ capName: "group-status",
70081
+ capScope: "device",
70082
+ addonId: null,
70083
+ access: "view",
70084
+ role: "viewer"
70085
+ },
69328
70086
  "heatingValve.getStatus": {
69329
70087
  capName: "heating-valve",
69330
70088
  capScope: "device",
@@ -70277,6 +71035,13 @@ Object.freeze({
70277
71035
  access: "create",
70278
71036
  role: "admin"
70279
71037
  },
71038
+ "meter.getStatus": {
71039
+ capName: "meter",
71040
+ capScope: "device",
71041
+ addonId: null,
71042
+ access: "view",
71043
+ role: "viewer"
71044
+ },
70280
71045
  "metricsProvider.collectSnapshot": {
70281
71046
  capName: "metrics-provider",
70282
71047
  capScope: "system",
@@ -78375,7 +79140,39 @@ var APPLIANCE_CAP_VOCABULARY = {
78375
79140
  phrase: t("When {device} finishes", "Quando {device} finisce"),
78376
79141
  icon: "square"
78377
79142
  }
78378
- }
79143
+ },
79144
+ overrides: { byType: {
79145
+ [DeviceType.Washer]: {
79146
+ state: { running: {
79147
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
79148
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
79149
+ } },
79150
+ events: {
79151
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
79152
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
79153
+ }
79154
+ },
79155
+ [DeviceType.Dryer]: {
79156
+ state: { running: {
79157
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
79158
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
79159
+ } },
79160
+ events: {
79161
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
79162
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
79163
+ }
79164
+ },
79165
+ [DeviceType.Dishwasher]: {
79166
+ state: { running: {
79167
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
79168
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
79169
+ } },
79170
+ events: {
79171
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
79172
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
79173
+ }
79174
+ }
79175
+ } }
78379
79176
  },
78380
79177
  update: {
78381
79178
  scope: "device",
@@ -79147,6 +79944,41 @@ var SENSOR_CAP_VOCABULARY = {
79147
79944
  icon: "flame"
79148
79945
  }]
79149
79946
  },
79947
+ "group-active": {
79948
+ scope: "device",
79949
+ noun: t("Group state", "Stato del gruppo")
79950
+ },
79951
+ "group-commands": {
79952
+ scope: "device",
79953
+ noun: t("Group command", "Comando di gruppo")
79954
+ },
79955
+ "group-status": {
79956
+ scope: "device",
79957
+ noun: t("Group", "Gruppo"),
79958
+ state: [
79959
+ {
79960
+ kind: "numeric",
79961
+ field: "included",
79962
+ quantity: t("the members reporting", "i membri che rispondono"),
79963
+ label: t("Reporting", "Rispondono"),
79964
+ icon: "users"
79965
+ },
79966
+ {
79967
+ kind: "numeric",
79968
+ field: "excluded",
79969
+ quantity: t("the members left out", "i membri esclusi"),
79970
+ label: t("Excluded", "Esclusi"),
79971
+ icon: "users"
79972
+ },
79973
+ {
79974
+ kind: "numeric",
79975
+ field: "total",
79976
+ quantity: t("the member count", "il numero di membri"),
79977
+ label: t("Members", "Membri"),
79978
+ icon: "sigma"
79979
+ }
79980
+ ]
79981
+ },
79150
79982
  "humidity-sensor": {
79151
79983
  scope: "device",
79152
79984
  noun: t("Humidity", "Umidità"),
@@ -79172,6 +80004,17 @@ var SENSOR_CAP_VOCABULARY = {
79172
80004
  }],
79173
80005
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
79174
80006
  },
80007
+ meter: {
80008
+ scope: "device",
80009
+ noun: t("Meter", "Contatore"),
80010
+ state: [{
80011
+ kind: "numeric",
80012
+ field: "value",
80013
+ quantity: t("the total", "il totale"),
80014
+ label: t("Total", "Totale"),
80015
+ icon: "gauge"
80016
+ }]
80017
+ },
79175
80018
  "numeric-sensor": {
79176
80019
  scope: "device",
79177
80020
  noun: t("Sensor", "Sensore"),
@@ -79180,7 +80023,6 @@ var SENSOR_CAP_VOCABULARY = {
79180
80023
  field: "value",
79181
80024
  quantity: t("the reading", "il valore"),
79182
80025
  label: t("Reading", "Valore"),
79183
- unitField: "unit",
79184
80026
  icon: "gauge"
79185
80027
  }]
79186
80028
  },
@@ -79316,7 +80158,7 @@ var SENSOR_CAP_VOCABULARY = {
79316
80158
  field: "temperature",
79317
80159
  quantity: t("the outdoor temperature", "la temperatura esterna"),
79318
80160
  label: t("Temperature", "Temperatura"),
79319
- unitField: "temperatureUnit",
80161
+ unit: "°C",
79320
80162
  icon: "thermometer"
79321
80163
  },
79322
80164
  {
@@ -79332,7 +80174,7 @@ var SENSOR_CAP_VOCABULARY = {
79332
80174
  field: "windSpeed",
79333
80175
  quantity: t("the wind speed", "la velocità del vento"),
79334
80176
  label: t("Wind", "Vento"),
79335
- unitField: "windSpeedUnit",
80177
+ unit: "m/s",
79336
80178
  icon: "wind"
79337
80179
  },
79338
80180
  {
@@ -79340,7 +80182,7 @@ var SENSOR_CAP_VOCABULARY = {
79340
80182
  field: "pressure",
79341
80183
  quantity: t("the air pressure", "la pressione atmosferica"),
79342
80184
  label: t("Pressure", "Pressione"),
79343
- unitField: "pressureUnit",
80185
+ unit: "hPa",
79344
80186
  icon: "gauge"
79345
80187
  }
79346
80188
  ]
@@ -79645,8 +80487,47 @@ function projectAction(cap, action) {
79645
80487
  };
79646
80488
  }
79647
80489
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
79648
- 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;
80490
+ 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;
79649
80491
  DeviceFeature.BatteryOperated;
80492
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
80493
+ 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.";
80494
+ 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.";
80495
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
80496
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
80497
+ 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.";
80498
+ function withheld(reason) {
80499
+ return {
80500
+ [DeviceType.Washer]: {
80501
+ exported: false,
80502
+ reason
80503
+ },
80504
+ [DeviceType.Dryer]: {
80505
+ exported: false,
80506
+ reason
80507
+ },
80508
+ [DeviceType.Dishwasher]: {
80509
+ exported: false,
80510
+ reason
80511
+ }
80512
+ };
80513
+ }
80514
+ function offered(received) {
80515
+ return {
80516
+ [DeviceType.Washer]: {
80517
+ exported: true,
80518
+ as: received
80519
+ },
80520
+ [DeviceType.Dryer]: {
80521
+ exported: true,
80522
+ as: received
80523
+ },
80524
+ [DeviceType.Dishwasher]: {
80525
+ exported: true,
80526
+ as: received
80527
+ }
80528
+ };
80529
+ }
80530
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
79650
80531
  /** Reject after `ms`; always clears its own timer. */
79651
80532
  async function withTimeout$1(promise, ms, label) {
79652
80533
  let timer;
@@ -80679,6 +81560,21 @@ Object.freeze({
80679
81560
  form: "single",
80680
81561
  optional: false
80681
81562
  }],
81563
+ "groupActive.getStatus": [{
81564
+ name: "deviceId",
81565
+ form: "single",
81566
+ optional: false
81567
+ }],
81568
+ "groupCommands.getStatus": [{
81569
+ name: "deviceId",
81570
+ form: "single",
81571
+ optional: false
81572
+ }],
81573
+ "groupStatus.getStatus": [{
81574
+ name: "deviceId",
81575
+ form: "single",
81576
+ optional: false
81577
+ }],
80682
81578
  "heatingValve.getStatus": [{
80683
81579
  name: "deviceId",
80684
81580
  form: "single",
@@ -80879,6 +81775,11 @@ Object.freeze({
80879
81775
  form: "single",
80880
81776
  optional: false
80881
81777
  }],
81778
+ "meter.getStatus": [{
81779
+ name: "deviceId",
81780
+ form: "single",
81781
+ optional: false
81782
+ }],
80882
81783
  "motion.getStatus": [{
80883
81784
  name: "deviceId",
80884
81785
  form: "single",
@@ -82486,7 +83387,6 @@ Object.freeze({
82486
83387
  "network-access": "ingress",
82487
83388
  "smtp-provider": "email"
82488
83389
  });
82489
- DeviceType.Camera;
82490
83390
  /**
82491
83391
  * A saved Customize assignment (D936). The schema is the type: callers use
82492
83392
  * `z.infer`, they do not restate the shape.
@@ -82663,6 +83563,8 @@ new Map(Object.entries({
82663
83563
  "detection-blind": "device-events",
82664
83564
  "motion-noisy": "device-events",
82665
83565
  "motion-calm": "device-events",
83566
+ "package-llm-silent": "device-events",
83567
+ "package-llm-answered": "device-events",
82666
83568
  "recording-stalled": "device-events",
82667
83569
  "recording-healthy": "device-events",
82668
83570
  "recording-degraded": "device-events",
@@ -82746,6 +83648,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
82746
83648
  label: "Motion without a subject",
82747
83649
  kinds: ["motion-noisy", "motion-calm"]
82748
83650
  },
83651
+ {
83652
+ id: "package-llm",
83653
+ family: "device-events",
83654
+ label: "Package confirm",
83655
+ kinds: ["package-llm-silent", "package-llm-answered"]
83656
+ },
82749
83657
  {
82750
83658
  id: "recording",
82751
83659
  family: "device-events",
@@ -82927,6 +83835,8 @@ var DEVICE_EVENTS = [
82927
83835
  "detection-blind",
82928
83836
  "motion-noisy",
82929
83837
  "motion-calm",
83838
+ "package-llm-silent",
83839
+ "package-llm-answered",
82930
83840
  "recording-stalled",
82931
83841
  "recording-healthy",
82932
83842
  "recording-degraded",
@@ -83195,6 +84105,15 @@ new Map([
83195
84105
  snapshotLabel: "Cameras with constant motion now",
83196
84106
  snapshotSection: null
83197
84107
  },
84108
+ {
84109
+ id: "package-llm-confirm",
84110
+ family: "device-events",
84111
+ label: "Package confirm (not answering / answering again)",
84112
+ bad: ["package-llm-silent"],
84113
+ recovery: ["package-llm-answered"],
84114
+ snapshotLabel: "Cameras whose package confirm is not answering",
84115
+ snapshotSection: null
84116
+ },
83198
84117
  {
83199
84118
  id: "recording-health",
83200
84119
  family: "device-events",