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