@camstack/addon-provider-reolink 1.2.210 → 1.2.212

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.mjs CHANGED
@@ -28,7 +28,7 @@ import { URL as URL$1 } from "url";
28
28
  import { spawn as spawn$1 } from "node:child_process";
29
29
  import os, { tmpdir } from "node:os";
30
30
  import { createReadStream } from "node:fs";
31
- //#region ../types/dist/event-category-DHXxB8t9.mjs
31
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
32
32
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
33
33
  EventCategory["SystemBoot"] = "system.boot";
34
34
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -354,6 +354,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
354
354
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
355
355
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
356
356
  /**
357
+ * A camera's package LLM confirm has failed closed three times in a row
358
+ * (D931, 2026-10-26). One edge per camera until
359
+ * {@link PipelinePackageLlmAnswered}.
360
+ */
361
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
362
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
363
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
364
+ /**
357
365
  * Per-camera pipeline config was mutated by the orchestrator
358
366
  * (a settings change via `setAgentInferenceDevices` /
359
367
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5463,7 +5471,7 @@ var ZodIssueCode = {
5463
5471
  var ZodFirstPartyTypeKind;
5464
5472
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5465
5473
  //#endregion
5466
- //#region ../types/dist/sleep-BErAxhul.mjs
5474
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5467
5475
  /**
5468
5476
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5469
5477
  * window to float samples (D455).
@@ -8021,6 +8029,99 @@ var RecordKindDeclarationSchema = object({
8021
8029
  }).strict();
8022
8030
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8023
8031
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8032
+ var READING_QUANTITIES = [
8033
+ "temperature",
8034
+ "pressure",
8035
+ "speed",
8036
+ "precipitation",
8037
+ "precipitation-rate",
8038
+ "length",
8039
+ "area",
8040
+ "illuminance",
8041
+ "irradiance",
8042
+ "power",
8043
+ "energy",
8044
+ "voltage",
8045
+ "current",
8046
+ "apparent-power",
8047
+ "humidity",
8048
+ "percentage",
8049
+ "concentration-volume",
8050
+ "concentration-mass",
8051
+ "concentration-ppb",
8052
+ "volume",
8053
+ "duration",
8054
+ "duration-ms",
8055
+ "information",
8056
+ "sound-level",
8057
+ "signal-level",
8058
+ "colour-temperature",
8059
+ "angle",
8060
+ "bitrate",
8061
+ "rate",
8062
+ "mass",
8063
+ "index",
8064
+ "ratio",
8065
+ "count",
8066
+ "none",
8067
+ "device"
8068
+ ];
8069
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8070
+ new Set(READING_QUANTITIES);
8071
+ /**
8072
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8073
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8074
+ */
8075
+ var QUANTITY_BASE = {
8076
+ temperature: "°C",
8077
+ pressure: "hPa",
8078
+ speed: "m/s",
8079
+ precipitation: "mm",
8080
+ "precipitation-rate": "mm/h",
8081
+ length: "m",
8082
+ area: "m²",
8083
+ illuminance: "lx",
8084
+ irradiance: "W/m²",
8085
+ power: "W",
8086
+ energy: "kWh",
8087
+ voltage: "V",
8088
+ current: "A",
8089
+ "apparent-power": "VA",
8090
+ humidity: "%",
8091
+ percentage: "%",
8092
+ "concentration-volume": "ppm",
8093
+ "concentration-mass": "µg/m³",
8094
+ "concentration-ppb": "ppb",
8095
+ volume: "L",
8096
+ duration: "s",
8097
+ "duration-ms": "ms",
8098
+ information: "B",
8099
+ "sound-level": "dBFS",
8100
+ "signal-level": "dBm",
8101
+ "colour-temperature": "mired",
8102
+ angle: "°",
8103
+ bitrate: "kbps",
8104
+ rate: "fps",
8105
+ mass: "kg",
8106
+ index: "none",
8107
+ ratio: "none",
8108
+ count: "none",
8109
+ none: "none",
8110
+ device: "device"
8111
+ };
8112
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8113
+ function baseUnitOf(quantity) {
8114
+ const symbol = QUANTITY_BASE[quantity];
8115
+ if (symbol === "none" || symbol === "device") return void 0;
8116
+ return symbol;
8117
+ }
8118
+ function readingMeta(quantity, extras) {
8119
+ return {
8120
+ quantity,
8121
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8122
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8123
+ };
8124
+ }
8024
8125
  /** Shared Zod schemas used across streaming capabilities. */
8025
8126
  var CamProfileSchema = _enum([
8026
8127
  "high",
@@ -8048,8 +8149,8 @@ var CamStreamKindSchema = _enum([
8048
8149
  "derived"
8049
8150
  ]);
8050
8151
  var CamStreamResolutionSchema = object({
8051
- width: number().int().positive(),
8052
- height: number().int().positive()
8152
+ width: number().int().positive().meta(readingMeta("none")),
8153
+ height: number().int().positive().meta(readingMeta("none"))
8053
8154
  });
8054
8155
  var CameraStreamSchema = object({
8055
8156
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8601,6 +8702,9 @@ var CAP_NAMES = Object.freeze([
8601
8702
  "filesystem-browse",
8602
8703
  "flood",
8603
8704
  "gas",
8705
+ "group-active",
8706
+ "group-commands",
8707
+ "group-status",
8604
8708
  "heating-valve",
8605
8709
  "humidifier",
8606
8710
  "humidity-sensor",
@@ -8621,6 +8725,7 @@ var CAP_NAMES = Object.freeze([
8621
8725
  "login-method",
8622
8726
  "media-player",
8623
8727
  "mesh-network",
8728
+ "meter",
8624
8729
  "metrics-provider",
8625
8730
  "model-convert",
8626
8731
  "model-distributor",
@@ -8730,14 +8835,6 @@ Object.freeze({
8730
8835
  {
8731
8836
  "path": "aqi",
8732
8837
  "valueType": "number"
8733
- },
8734
- {
8735
- "path": "unit",
8736
- "valueType": "string"
8737
- },
8738
- {
8739
- "path": "precision",
8740
- "valueType": "number"
8741
8838
  }
8742
8839
  ],
8743
8840
  "alarm-panel": [
@@ -8773,20 +8870,10 @@ Object.freeze({
8773
8870
  "valueType": "boolean"
8774
8871
  }
8775
8872
  ],
8776
- "ambient-light-sensor": [
8777
- {
8778
- "path": "lux",
8779
- "valueType": "number"
8780
- },
8781
- {
8782
- "path": "unit",
8783
- "valueType": "string"
8784
- },
8785
- {
8786
- "path": "precision",
8787
- "valueType": "number"
8788
- }
8789
- ],
8873
+ "ambient-light-sensor": [{
8874
+ "path": "lux",
8875
+ "valueType": "number"
8876
+ }],
8790
8877
  "audio-metrics": [
8791
8878
  {
8792
8879
  "path": "ts",
@@ -8866,6 +8953,10 @@ Object.freeze({
8866
8953
  "path": "lastUpdated",
8867
8954
  "valueType": "number"
8868
8955
  },
8956
+ {
8957
+ "path": "volts",
8958
+ "valueType": "number"
8959
+ },
8869
8960
  {
8870
8961
  "path": "lastContactAt",
8871
8962
  "valueType": "number"
@@ -9108,10 +9199,6 @@ Object.freeze({
9108
9199
  "datetime"
9109
9200
  ]
9110
9201
  },
9111
- {
9112
- "path": "unit",
9113
- "valueType": "string"
9114
- },
9115
9202
  {
9116
9203
  "path": "min",
9117
9204
  "valueType": "number"
@@ -9123,19 +9210,6 @@ Object.freeze({
9123
9210
  {
9124
9211
  "path": "step",
9125
9212
  "valueType": "number"
9126
- },
9127
- {
9128
- "path": "precision",
9129
- "valueType": "number"
9130
- },
9131
- {
9132
- "path": "format",
9133
- "valueType": "enum",
9134
- "enumValues": [
9135
- "date",
9136
- "time",
9137
- "datetime"
9138
- ]
9139
9213
  }
9140
9214
  ],
9141
9215
  "cover": [
@@ -9376,13 +9450,8 @@ Object.freeze({
9376
9450
  "path": "value",
9377
9451
  "valueType": "string"
9378
9452
  }, {
9379
- "path": "format",
9380
- "valueType": "enum",
9381
- "enumValues": [
9382
- "date",
9383
- "time",
9384
- "datetime"
9385
- ]
9453
+ "path": "timestamp",
9454
+ "valueType": "boolean"
9386
9455
  }],
9387
9456
  "event-emitter": [
9388
9457
  {
@@ -9455,6 +9524,24 @@ Object.freeze({
9455
9524
  "path": "detected",
9456
9525
  "valueType": "boolean"
9457
9526
  }],
9527
+ "group-active": [{
9528
+ "path": "active",
9529
+ "valueType": "boolean"
9530
+ }],
9531
+ "group-status": [
9532
+ {
9533
+ "path": "included",
9534
+ "valueType": "number"
9535
+ },
9536
+ {
9537
+ "path": "excluded",
9538
+ "valueType": "number"
9539
+ },
9540
+ {
9541
+ "path": "total",
9542
+ "valueType": "number"
9543
+ }
9544
+ ],
9458
9545
  "heating-valve": [{
9459
9546
  "path": "position",
9460
9547
  "valueType": "number"
@@ -9489,20 +9576,10 @@ Object.freeze({
9489
9576
  "valueType": "number"
9490
9577
  }
9491
9578
  ],
9492
- "humidity-sensor": [
9493
- {
9494
- "path": "percent",
9495
- "valueType": "number"
9496
- },
9497
- {
9498
- "path": "unit",
9499
- "valueType": "string"
9500
- },
9501
- {
9502
- "path": "precision",
9503
- "valueType": "number"
9504
- }
9505
- ],
9579
+ "humidity-sensor": [{
9580
+ "path": "percent",
9581
+ "valueType": "number"
9582
+ }],
9506
9583
  "image": [{
9507
9584
  "path": "url",
9508
9585
  "valueType": "string"
@@ -9710,6 +9787,10 @@ Object.freeze({
9710
9787
  ]
9711
9788
  }
9712
9789
  ],
9790
+ "meter": [{
9791
+ "path": "value",
9792
+ "valueType": "number"
9793
+ }],
9713
9794
  "motion": [
9714
9795
  {
9715
9796
  "path": "detected",
@@ -9841,20 +9922,10 @@ Object.freeze({
9841
9922
  "valueType": "boolean"
9842
9923
  }
9843
9924
  ],
9844
- "numeric-sensor": [
9845
- {
9846
- "path": "value",
9847
- "valueType": "number"
9848
- },
9849
- {
9850
- "path": "unit",
9851
- "valueType": "string"
9852
- },
9853
- {
9854
- "path": "precision",
9855
- "valueType": "number"
9856
- }
9857
- ],
9925
+ "numeric-sensor": [{
9926
+ "path": "value",
9927
+ "valueType": "number"
9928
+ }],
9858
9929
  "pet-feeder": [
9859
9930
  {
9860
9931
  "path": "foodLevel",
@@ -9946,14 +10017,6 @@ Object.freeze({
9946
10017
  {
9947
10018
  "path": "powerFactor",
9948
10019
  "valueType": "number"
9949
- },
9950
- {
9951
- "path": "unit",
9952
- "valueType": "string"
9953
- },
9954
- {
9955
- "path": "precision",
9956
- "valueType": "number"
9957
10020
  }
9958
10021
  ],
9959
10022
  "presence": [
@@ -9982,20 +10045,10 @@ Object.freeze({
9982
10045
  "valueType": "number"
9983
10046
  }
9984
10047
  ],
9985
- "pressure-sensor": [
9986
- {
9987
- "path": "hpa",
9988
- "valueType": "number"
9989
- },
9990
- {
9991
- "path": "unit",
9992
- "valueType": "string"
9993
- },
9994
- {
9995
- "path": "precision",
9996
- "valueType": "number"
9997
- }
9998
- ],
10048
+ "pressure-sensor": [{
10049
+ "path": "hpa",
10050
+ "valueType": "number"
10051
+ }],
9999
10052
  "privacy-mask": [{
10000
10053
  "path": "enabled",
10001
10054
  "valueType": "boolean"
@@ -10306,20 +10359,10 @@ Object.freeze({
10306
10359
  "path": "tampered",
10307
10360
  "valueType": "boolean"
10308
10361
  }],
10309
- "temperature-sensor": [
10310
- {
10311
- "path": "celsius",
10312
- "valueType": "number"
10313
- },
10314
- {
10315
- "path": "unit",
10316
- "valueType": "string"
10317
- },
10318
- {
10319
- "path": "precision",
10320
- "valueType": "number"
10321
- }
10322
- ],
10362
+ "temperature-sensor": [{
10363
+ "path": "celsius",
10364
+ "valueType": "number"
10365
+ }],
10323
10366
  "update": [
10324
10367
  {
10325
10368
  "path": "currentVersion",
@@ -10430,10 +10473,6 @@ Object.freeze({
10430
10473
  "path": "temperature",
10431
10474
  "valueType": "number"
10432
10475
  },
10433
- {
10434
- "path": "temperatureUnit",
10435
- "valueType": "string"
10436
- },
10437
10476
  {
10438
10477
  "path": "humidity",
10439
10478
  "valueType": "number"
@@ -10442,18 +10481,10 @@ Object.freeze({
10442
10481
  "path": "pressure",
10443
10482
  "valueType": "number"
10444
10483
  },
10445
- {
10446
- "path": "pressureUnit",
10447
- "valueType": "string"
10448
- },
10449
10484
  {
10450
10485
  "path": "windSpeed",
10451
10486
  "valueType": "number"
10452
10487
  },
10453
- {
10454
- "path": "windSpeedUnit",
10455
- "valueType": "string"
10456
- },
10457
10488
  {
10458
10489
  "path": "windBearing",
10459
10490
  "valueType": "number"
@@ -10541,7 +10572,7 @@ function sleep$2(ms) {
10541
10572
  return new Promise((resolve) => setTimeout(resolve, Math.max(0, ms)));
10542
10573
  }
10543
10574
  //#endregion
10544
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10575
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10545
10576
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10546
10577
  DeviceType["Camera"] = "camera";
10547
10578
  DeviceType["Hub"] = "hub";
@@ -10635,8 +10666,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10635
10666
  DeviceType["PetFeeder"] = "pet-feeder";
10636
10667
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10637
10668
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10638
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10669
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10670
+ * A plug-built dishwasher carries running-state and power instead. */
10639
10671
  DeviceType["Dishwasher"] = "dishwasher";
10672
+ /** Washing machine. A plug-built one is running while its power stays
10673
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10674
+ DeviceType["Washer"] = "washer";
10675
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10676
+ DeviceType["Dryer"] = "dryer";
10640
10677
  return DeviceType;
10641
10678
  }({});
10642
10679
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -10884,7 +10921,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
10884
10921
  return DeviceRole;
10885
10922
  }({});
10886
10923
  //#endregion
10887
- //#region ../types/dist/composition-kEl9mevu.mjs
10924
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
10888
10925
  /**
10889
10926
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10890
10927
  *
@@ -11199,6 +11236,13 @@ var table$1 = {
11199
11236
  apply: () => {
11200
11237
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11201
11238
  }
11239
+ },
11240
+ schmitt: {
11241
+ minArgs: 3,
11242
+ maxArgs: 5,
11243
+ apply: () => {
11244
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11245
+ }
11202
11246
  }
11203
11247
  };
11204
11248
  Object.freeze(Object.assign(Object.create(null), table$1));
@@ -11639,15 +11683,232 @@ function validateExpressionSource(src) {
11639
11683
  }
11640
11684
  return null;
11641
11685
  }
11642
- /** A composed device's stableId is this prefix plus the block id. */
11643
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11644
- /** Longest snippet a `code` source may carry (slice 4). */
11645
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11686
+ /**
11687
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11688
+ *
11689
+ * Standby of the control board is under 5 W; a motor or a heater is far
11690
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11691
+ * a cycle, so running starts only after 30 s above the line.
11692
+ *
11693
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11694
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11695
+ * report "finished" in the middle of the program, so the stop dwell is
11696
+ * 10 minutes on every type. The number is stored per type so one machine
11697
+ * can move later without moving the others.
11698
+ */
11699
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11700
+ var APPLIANCE_KINDS = [
11701
+ "washer",
11702
+ "dryer",
11703
+ "dishwasher"
11704
+ ];
11705
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11706
+ /**
11707
+ * Statistics and integrals over a history read (D944).
11708
+ *
11709
+ * Pure: the same points always give the same answer, so a restart recomputes
11710
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11711
+ * never the mean of the samples. A hole with no held value is not integrated
11712
+ * as zero. A truncated read is not published.
11713
+ */
11714
+ var STATISTIC_STATS = [
11715
+ "min",
11716
+ "max",
11717
+ "mean",
11718
+ "sum",
11719
+ "count",
11720
+ "change"
11721
+ ];
11722
+ /**
11723
+ * The range a statistics or integral helper asks history for (D944).
11724
+ *
11725
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11726
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11727
+ * is strictly longer than the longest that range can be. Forever qualifies.
11728
+ * Equal to a tier's retention is a refusal, not a short answer.
11729
+ */
11730
+ var MINUTE_MS = 6e4;
11731
+ var HOUR_MS = 60 * MINUTE_MS;
11732
+ var DAY_MS = 24 * HOUR_MS;
11733
+ var ROLLING_SPANS = [
11734
+ "1h",
11735
+ "24h",
11736
+ "2d",
11737
+ "7d",
11738
+ "30d",
11739
+ "custom"
11740
+ ];
11741
+ var CALENDAR_PERIODS = [
11742
+ "today",
11743
+ "yesterday",
11744
+ "this-week",
11745
+ "this-month"
11746
+ ];
11747
+ var RESET_PERIODS = [
11748
+ "never",
11749
+ "daily",
11750
+ "weekly",
11751
+ "monthly"
11752
+ ];
11753
+ 5 * MINUTE_MS;
11754
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11755
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11756
+ var rollingWindowObject = object({
11757
+ kind: literal("rolling"),
11758
+ span: _enum(ROLLING_SPANS),
11759
+ customMinutes: number().int().min(5).optional()
11760
+ }).check((payload) => {
11761
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11762
+ code: "custom",
11763
+ message: "a custom range needs a duration in minutes",
11764
+ path: ["customMinutes"],
11765
+ input: payload.value
11766
+ });
11767
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11768
+ code: "custom",
11769
+ message: "only a custom range carries a duration",
11770
+ path: ["customMinutes"],
11771
+ input: payload.value
11772
+ });
11773
+ });
11774
+ var RollingWindowSchema = rollingWindowObject;
11775
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
11776
+ kind: literal("calendar"),
11777
+ period: _enum(CALENDAR_PERIODS)
11778
+ })]);
11779
+ /**
11780
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11781
+ * id. Split out so helper schemas can name it without a cycle through
11782
+ * `composition.ts` (D659).
11783
+ */
11646
11784
  var CompositionSourceRefSchema = object({
11647
11785
  /** The owning addon; `stableId` is unique only within it. */
11648
11786
  addonId: string().min(1),
11649
11787
  stableId: string().min(1)
11650
11788
  });
11789
+ var GROUP_TEMPLATE_IDS = [
11790
+ "temperature",
11791
+ "humidity",
11792
+ "pressure",
11793
+ "illuminance",
11794
+ "power",
11795
+ "energy",
11796
+ "battery",
11797
+ "numeric",
11798
+ "motion",
11799
+ "contact",
11800
+ "leak",
11801
+ "smoke",
11802
+ "occupancy",
11803
+ "binary",
11804
+ "switch",
11805
+ "light",
11806
+ "fan",
11807
+ "cover",
11808
+ "valve",
11809
+ "lock",
11810
+ "media-player",
11811
+ "climate"
11812
+ ];
11813
+ var NUMERIC_AGGREGATES = [
11814
+ "mean",
11815
+ "median",
11816
+ "min",
11817
+ "max",
11818
+ "sum"
11819
+ ];
11820
+ var BINARY_AGGREGATES = ["any", "all"];
11821
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11822
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11823
+ var AGGREGATE_REDUCERS = [
11824
+ "mean",
11825
+ "median",
11826
+ "min",
11827
+ "max",
11828
+ "sum",
11829
+ "any",
11830
+ "all"
11831
+ ];
11832
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11833
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11834
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11835
+ /** One member. The field is stored so occupancy can name motion or binary. */
11836
+ var HelperMemberSchema = object({
11837
+ source: CompositionSourceRefSchema,
11838
+ cap: string().min(1),
11839
+ fieldPath: string().min(1)
11840
+ });
11841
+ var HelperSpecSchema = discriminatedUnion("kind", [
11842
+ object({
11843
+ kind: literal("group"),
11844
+ templateId: GroupTemplateIdSchema,
11845
+ aggregate: AggregateReducerSchema,
11846
+ members: array(HelperMemberSchema).min(1).max(64),
11847
+ /** Actuator groups only. Readings leave this unset. */
11848
+ active: ActiveCriterionSchema.optional()
11849
+ }),
11850
+ object({
11851
+ kind: literal("appliance"),
11852
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
11853
+ appliance: _enum(APPLIANCE_KINDS),
11854
+ thresholdWatts: number().positive(),
11855
+ startDwellMs: number().int().nonnegative(),
11856
+ stopDwellMs: number().int().nonnegative(),
11857
+ member: HelperMemberSchema
11858
+ }),
11859
+ object({
11860
+ kind: literal("statistic"),
11861
+ stat: _enum(STATISTIC_STATS),
11862
+ window: StatisticWindowSchema,
11863
+ source: HelperMemberSchema
11864
+ }),
11865
+ object({
11866
+ kind: literal("integral"),
11867
+ reset: _enum(RESET_PERIODS),
11868
+ output: _enum(["energy", "meter"]),
11869
+ source: HelperMemberSchema
11870
+ }),
11871
+ object({
11872
+ kind: literal("threshold"),
11873
+ source: HelperMemberSchema,
11874
+ high: number(),
11875
+ low: number(),
11876
+ onForMs: number().int().nonnegative(),
11877
+ offForMs: number().int().nonnegative()
11878
+ }),
11879
+ object({
11880
+ kind: literal("arithmetic"),
11881
+ op: _enum([
11882
+ "sum",
11883
+ "difference",
11884
+ "ratio"
11885
+ ]),
11886
+ inputs: array(HelperMemberSchema).min(2).max(64)
11887
+ }),
11888
+ object({
11889
+ kind: literal("expression"),
11890
+ expr: string().min(1),
11891
+ inputs: array(object({
11892
+ name: string().min(1),
11893
+ member: HelperMemberSchema
11894
+ })).min(1).max(64),
11895
+ output: _enum(["number", "boolean"])
11896
+ }),
11897
+ object({
11898
+ kind: literal("derivative"),
11899
+ window: RollingWindowSchema,
11900
+ per: _enum([
11901
+ "second",
11902
+ "minute",
11903
+ "hour"
11904
+ ]),
11905
+ source: HelperMemberSchema
11906
+ })
11907
+ ]);
11908
+ /** A composed device's stableId is this prefix plus the block id. */
11909
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11910
+ /** Longest snippet a `code` source may carry (slice 4). */
11911
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11651
11912
  /** One field of one capability of one source device. */
11652
11913
  var CompositionFieldReadSchema = object({
11653
11914
  source: CompositionSourceRefSchema,
@@ -11691,7 +11952,59 @@ var CompositionCodeSourceSchema = object({
11691
11952
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11692
11953
  CompositionFromSourceSchema,
11693
11954
  CompositionExpressionSourceSchema,
11694
- CompositionCodeSourceSchema
11955
+ CompositionCodeSourceSchema,
11956
+ object({
11957
+ kind: literal("aggregate"),
11958
+ reducer: _enum([
11959
+ "mean",
11960
+ "median",
11961
+ "min",
11962
+ "max",
11963
+ "sum",
11964
+ "any",
11965
+ "all"
11966
+ ]),
11967
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11968
+ /**
11969
+ * Set when the reads are an enum state, not a yes/no: the listed states
11970
+ * count as active (a cover is `open`). A state outside the list is inactive;
11971
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
11972
+ */
11973
+ activeWhen: array(string().min(1)).min(1).optional(),
11974
+ /** `value` is the reading; the others are the member counts. */
11975
+ publish: _enum([
11976
+ "value",
11977
+ "included",
11978
+ "excluded",
11979
+ "total"
11980
+ ]),
11981
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11982
+ empty: _enum([
11983
+ "null",
11984
+ "omit",
11985
+ "offline"
11986
+ ])
11987
+ }),
11988
+ object({
11989
+ kind: literal("series"),
11990
+ read: CompositionFieldReadSchema,
11991
+ /** `value` is the statistic or the integral. The others are the gap counts. */
11992
+ publish: _enum([
11993
+ "value",
11994
+ "included",
11995
+ "excluded",
11996
+ "total"
11997
+ ]),
11998
+ /**
11999
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
12000
+ * device unseeded until history answers, so a restart cannot publish 0.
12001
+ */
12002
+ empty: _enum([
12003
+ "null",
12004
+ "omit",
12005
+ "offline"
12006
+ ])
12007
+ })
11695
12008
  ]);
11696
12009
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11697
12010
  kind: literal("forward"),
@@ -11737,7 +12050,13 @@ var CompositionSchema = object({
11737
12050
  kind: literal("existing"),
11738
12051
  device: CompositionSourceRefSchema
11739
12052
  })]),
11740
- features: array(CompositionFeatureSchema).min(1).max(16)
12053
+ features: array(CompositionFeatureSchema).min(1).max(16),
12054
+ /**
12055
+ * Present when the block is a helper template. Absent on a free
12056
+ * composition (the room example, device 4603). The editor reads this,
12057
+ * never the compiled features.
12058
+ */
12059
+ helper: HelperSpecSchema.optional()
11741
12060
  }).superRefine((composition, ctx) => {
11742
12061
  const seen = /* @__PURE__ */ new Set();
11743
12062
  composition.features.forEach((feature, index) => {
@@ -17414,7 +17733,22 @@ var BrokerProviderInfoSchema = object({
17414
17733
  */
17415
17734
  interactiveConnectPath: string().min(1).optional(),
17416
17735
  /** Relative hub path to disconnect the linked account (same popup pattern). */
17417
- interactiveDisconnectPath: string().min(1).optional()
17736
+ interactiveDisconnectPath: string().min(1).optional(),
17737
+ /**
17738
+ * The kind signs in through a server-held interactive session instead of
17739
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17740
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17741
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17742
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17743
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17744
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17745
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17746
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17747
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17748
+ * offers it as the default path; the fields the session fills are
17749
+ * marked `filledBySignIn` in the kind's settings schema.
17750
+ */
17751
+ signIn: object({ basePath: string().min(1) }).optional()
17418
17752
  }))
17419
17753
  });
17420
17754
  var ListInputSchema = object({
@@ -18515,7 +18849,12 @@ var CompositionProblemSchema = object({
18515
18849
  "item-field-reserved",
18516
18850
  "target-missing",
18517
18851
  "target-unreadable",
18518
- "not-replaceable"
18852
+ "not-replaceable",
18853
+ "native-capability-replace",
18854
+ "mixed-quantity",
18855
+ "helper-mismatch",
18856
+ "member-constraint",
18857
+ "retention"
18519
18858
  ]),
18520
18859
  path: string(),
18521
18860
  message: string()
@@ -18536,7 +18875,8 @@ var CompositionUnavailableModeSchema = _enum([
18536
18875
  "null",
18537
18876
  "offline",
18538
18877
  "unknown",
18539
- "release"
18878
+ "release",
18879
+ "omit"
18540
18880
  ]);
18541
18881
  /**
18542
18882
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18573,9 +18913,11 @@ var CompositionFieldPlanSchema = object({
18573
18913
  reads: array(CompositionFieldReadSchema)
18574
18914
  });
18575
18915
  /**
18576
- * What a feature does to its target (D663): `new` builds a capability on a new
18577
- * device; on an existing device it `add`s a capability the device lacks, or
18578
- * `replace`s named fields of one it has natively.
18916
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18917
+ * new device; on an existing device it `add`s a capability the device lacks.
18918
+ * Replacing a capability the device already serves is refused
18919
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18920
+ * wire for a claim the hub already holds.
18579
18921
  */
18580
18922
  var CompositionFeatureModeSchema = _enum([
18581
18923
  "new",
@@ -18746,8 +19088,392 @@ Object.freeze({
18746
19088
  latchedAt: {
18747
19089
  kind: "fixed",
18748
19090
  result: ["number", "null"]
19091
+ },
19092
+ schmitt: {
19093
+ kind: "fixed",
19094
+ result: ["boolean", "null"]
18749
19095
  }
18750
19096
  });
19097
+ /**
19098
+ * One catalog row per base reading a group helper can build (D944 §3).
19099
+ * Data, not a form default written into the block.
19100
+ */
19101
+ var NOW$2 = {
19102
+ fieldPath: "lastFetchedAt",
19103
+ expr: "now"
19104
+ };
19105
+ var CHANGED = {
19106
+ fieldPath: "lastChangedAt",
19107
+ expr: "now"
19108
+ };
19109
+ function numeric(input) {
19110
+ return {
19111
+ ...input,
19112
+ family: "numeric",
19113
+ deviceType: DeviceType.Sensor,
19114
+ memberDeviceType: DeviceType.Sensor,
19115
+ memberDeviceTypes: [DeviceType.Sensor],
19116
+ activeWhen: void 0,
19117
+ commandCaps: [],
19118
+ baseUnit: baseUnitOf(input.quantity),
19119
+ aggregates: NUMERIC_AGGREGATES,
19120
+ accept: [{
19121
+ cap: input.outputCap,
19122
+ fieldPath: input.outputField
19123
+ }]
19124
+ };
19125
+ }
19126
+ function binary(input) {
19127
+ return {
19128
+ ...input,
19129
+ family: "binary",
19130
+ deviceType: DeviceType.Sensor,
19131
+ memberDeviceType: DeviceType.Sensor,
19132
+ memberDeviceTypes: [DeviceType.Sensor],
19133
+ activeWhen: void 0,
19134
+ commandCaps: [],
19135
+ quantity: "none",
19136
+ baseUnit: void 0,
19137
+ aggregates: BINARY_AGGREGATES,
19138
+ empty: "offline"
19139
+ };
19140
+ }
19141
+ var GROUP_TEMPLATES = [
19142
+ numeric({
19143
+ id: "temperature",
19144
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
19145
+ role: DeviceRole.TemperatureSensor,
19146
+ outputCap: "temperature-sensor",
19147
+ outputField: "celsius",
19148
+ quantity: "temperature",
19149
+ defaultAggregate: "mean",
19150
+ empty: "null",
19151
+ companions: [NOW$2]
19152
+ }),
19153
+ numeric({
19154
+ id: "humidity",
19155
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
19156
+ role: DeviceRole.HumiditySensor,
19157
+ outputCap: "humidity-sensor",
19158
+ outputField: "percent",
19159
+ quantity: "humidity",
19160
+ defaultAggregate: "mean",
19161
+ empty: "null",
19162
+ companions: [NOW$2]
19163
+ }),
19164
+ numeric({
19165
+ id: "pressure",
19166
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
19167
+ role: DeviceRole.PressureSensor,
19168
+ outputCap: "pressure-sensor",
19169
+ outputField: "hpa",
19170
+ quantity: "pressure",
19171
+ defaultAggregate: "mean",
19172
+ empty: "null",
19173
+ companions: [NOW$2]
19174
+ }),
19175
+ numeric({
19176
+ id: "illuminance",
19177
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
19178
+ role: DeviceRole.AmbientLightSensor,
19179
+ outputCap: "ambient-light-sensor",
19180
+ outputField: "lux",
19181
+ quantity: "illuminance",
19182
+ defaultAggregate: "mean",
19183
+ empty: "null",
19184
+ companions: [NOW$2]
19185
+ }),
19186
+ numeric({
19187
+ id: "power",
19188
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
19189
+ role: DeviceRole.PowerSensor,
19190
+ outputCap: "power-meter",
19191
+ outputField: "watts",
19192
+ quantity: "power",
19193
+ defaultAggregate: "sum",
19194
+ empty: "omit",
19195
+ companions: [NOW$2]
19196
+ }),
19197
+ numeric({
19198
+ id: "energy",
19199
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
19200
+ role: DeviceRole.EnergySensor,
19201
+ outputCap: "power-meter",
19202
+ outputField: "kwhTotal",
19203
+ quantity: "energy",
19204
+ defaultAggregate: "sum",
19205
+ empty: "omit",
19206
+ companions: [NOW$2]
19207
+ }),
19208
+ numeric({
19209
+ id: "battery",
19210
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
19211
+ role: DeviceRole.BatterySensor,
19212
+ outputCap: "battery",
19213
+ outputField: "percentage",
19214
+ quantity: "percentage",
19215
+ defaultAggregate: "mean",
19216
+ empty: "null",
19217
+ companions: [
19218
+ {
19219
+ fieldPath: "charging",
19220
+ expr: "\"unknown\""
19221
+ },
19222
+ {
19223
+ fieldPath: "sleeping",
19224
+ expr: "false"
19225
+ },
19226
+ {
19227
+ fieldPath: "lastUpdated",
19228
+ expr: "now"
19229
+ }
19230
+ ]
19231
+ }),
19232
+ numeric({
19233
+ id: "numeric",
19234
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
19235
+ role: DeviceRole.NumericSensor,
19236
+ outputCap: "numeric-sensor",
19237
+ outputField: "value",
19238
+ quantity: "device",
19239
+ defaultAggregate: "mean",
19240
+ empty: "null",
19241
+ companions: [NOW$2]
19242
+ }),
19243
+ binary({
19244
+ id: "motion",
19245
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
19246
+ role: DeviceRole.MotionSensor,
19247
+ outputCap: "motion",
19248
+ outputField: "detected",
19249
+ defaultAggregate: "any",
19250
+ accept: [{
19251
+ cap: "motion",
19252
+ fieldPath: "detected"
19253
+ }],
19254
+ companions: [{
19255
+ fieldPath: "lastDetectedAt",
19256
+ expr: "null"
19257
+ }, {
19258
+ fieldPath: "autoClearAfterMs",
19259
+ expr: "null"
19260
+ }]
19261
+ }),
19262
+ binary({
19263
+ id: "contact",
19264
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
19265
+ role: DeviceRole.ContactSensor,
19266
+ outputCap: "contact",
19267
+ outputField: "entryOpen",
19268
+ defaultAggregate: "any",
19269
+ accept: [{
19270
+ cap: "contact",
19271
+ fieldPath: "entryOpen"
19272
+ }],
19273
+ companions: [CHANGED]
19274
+ }),
19275
+ binary({
19276
+ id: "leak",
19277
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
19278
+ role: DeviceRole.LeakSensor,
19279
+ outputCap: "flood",
19280
+ outputField: "flooded",
19281
+ defaultAggregate: "any",
19282
+ accept: [{
19283
+ cap: "flood",
19284
+ fieldPath: "flooded"
19285
+ }],
19286
+ companions: [CHANGED]
19287
+ }),
19288
+ binary({
19289
+ id: "smoke",
19290
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
19291
+ role: DeviceRole.SmokeSensor,
19292
+ outputCap: "smoke",
19293
+ outputField: "detected",
19294
+ defaultAggregate: "any",
19295
+ accept: [{
19296
+ cap: "smoke",
19297
+ fieldPath: "detected"
19298
+ }],
19299
+ companions: [CHANGED]
19300
+ }),
19301
+ binary({
19302
+ id: "occupancy",
19303
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
19304
+ role: DeviceRole.BinarySensor,
19305
+ outputCap: "binary",
19306
+ outputField: "on",
19307
+ defaultAggregate: "any",
19308
+ accept: [{
19309
+ cap: "motion",
19310
+ fieldPath: "detected"
19311
+ }, {
19312
+ cap: "binary",
19313
+ fieldPath: "on"
19314
+ }],
19315
+ companions: [CHANGED]
19316
+ }),
19317
+ binary({
19318
+ id: "binary",
19319
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
19320
+ role: DeviceRole.BinarySensor,
19321
+ outputCap: "binary",
19322
+ outputField: "on",
19323
+ defaultAggregate: "any",
19324
+ accept: [{
19325
+ cap: "binary",
19326
+ fieldPath: "on"
19327
+ }],
19328
+ companions: [CHANGED]
19329
+ }),
19330
+ actuator({
19331
+ id: "switch",
19332
+ 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.",
19333
+ deviceType: DeviceType.Switch,
19334
+ outputCap: "switch",
19335
+ outputField: "on",
19336
+ member: {
19337
+ cap: "switch",
19338
+ fieldPath: "on"
19339
+ },
19340
+ commandCaps: ["switch"]
19341
+ }),
19342
+ actuator({
19343
+ id: "light",
19344
+ 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.",
19345
+ deviceType: DeviceType.Light,
19346
+ outputCap: "switch",
19347
+ outputField: "on",
19348
+ member: {
19349
+ cap: "switch",
19350
+ fieldPath: "on"
19351
+ },
19352
+ commandCaps: [
19353
+ "switch",
19354
+ "brightness",
19355
+ "color"
19356
+ ]
19357
+ }),
19358
+ actuator({
19359
+ id: "fan",
19360
+ 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.",
19361
+ deviceType: DeviceType.Fan,
19362
+ outputCap: "switch",
19363
+ outputField: "on",
19364
+ member: {
19365
+ cap: "switch",
19366
+ fieldPath: "on"
19367
+ },
19368
+ commandCaps: ["switch", "fan-control"]
19369
+ }),
19370
+ actuator({
19371
+ id: "cover",
19372
+ 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.",
19373
+ deviceType: DeviceType.Cover,
19374
+ outputCap: "group-active",
19375
+ outputField: "active",
19376
+ member: {
19377
+ cap: "cover",
19378
+ fieldPath: "state"
19379
+ },
19380
+ activeWhen: [
19381
+ "open",
19382
+ "opening",
19383
+ "stopped"
19384
+ ],
19385
+ commandCaps: ["cover"]
19386
+ }),
19387
+ actuator({
19388
+ id: "valve",
19389
+ 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.",
19390
+ deviceType: DeviceType.Valve,
19391
+ outputCap: "group-active",
19392
+ outputField: "active",
19393
+ member: {
19394
+ cap: "valve",
19395
+ fieldPath: "state"
19396
+ },
19397
+ activeWhen: [
19398
+ "open",
19399
+ "opening",
19400
+ "stopped"
19401
+ ],
19402
+ commandCaps: ["valve"]
19403
+ }),
19404
+ actuator({
19405
+ id: "lock",
19406
+ 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.",
19407
+ deviceType: DeviceType.Lock,
19408
+ outputCap: "group-active",
19409
+ outputField: "active",
19410
+ member: {
19411
+ cap: "lock-control",
19412
+ fieldPath: "state"
19413
+ },
19414
+ activeWhen: ["locked"],
19415
+ commandCaps: ["lock-control"]
19416
+ }),
19417
+ actuator({
19418
+ id: "media-player",
19419
+ 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.",
19420
+ deviceType: DeviceType.MediaPlayer,
19421
+ outputCap: "group-active",
19422
+ outputField: "active",
19423
+ member: {
19424
+ cap: "media-player",
19425
+ fieldPath: "state"
19426
+ },
19427
+ activeWhen: ["playing", "buffering"],
19428
+ commandCaps: ["media-player"]
19429
+ }),
19430
+ actuator({
19431
+ id: "climate",
19432
+ 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.",
19433
+ deviceType: DeviceType.Thermostat,
19434
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
19435
+ outputCap: "group-active",
19436
+ outputField: "active",
19437
+ member: {
19438
+ cap: "climate-control",
19439
+ fieldPath: "mode"
19440
+ },
19441
+ activeWhen: [
19442
+ "heat",
19443
+ "cool",
19444
+ "auto",
19445
+ "heat_cool",
19446
+ "fan_only",
19447
+ "dry"
19448
+ ],
19449
+ commandCaps: ["climate-control"]
19450
+ })
19451
+ ];
19452
+ function actuator(input) {
19453
+ return {
19454
+ id: input.id,
19455
+ summary: input.summary,
19456
+ deviceType: input.deviceType,
19457
+ outputCap: input.outputCap,
19458
+ outputField: input.outputField,
19459
+ commandCaps: input.commandCaps,
19460
+ family: "actuator",
19461
+ role: void 0,
19462
+ memberDeviceType: input.deviceType,
19463
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
19464
+ activeWhen: input.activeWhen,
19465
+ quantity: "none",
19466
+ baseUnit: void 0,
19467
+ aggregates: BINARY_AGGREGATES,
19468
+ defaultAggregate: "any",
19469
+ empty: "offline",
19470
+ accept: [input.member],
19471
+ companions: [CHANGED]
19472
+ };
19473
+ }
19474
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19475
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19476
+ DeviceType.Camera;
18751
19477
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18752
19478
  /**
18753
19479
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20574,12 +21300,12 @@ function buildDeviceAdminUrl(parts) {
20574
21300
  * providers detect renames by matching `uniqueId` and patch `id`
20575
21301
  * in-place so dispatch keeps working.
20576
21302
  *
20577
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20578
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20579
- * state push and are the single source of truth. The `device_class` tag is
20580
- * an HA-specific input that is consumed at the discovery boundary
20581
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20582
- * stored after that point.
21303
+ * **Display unit and precision are NOT stored here.** They are a read-time
21304
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
21305
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
21306
+ * neither is repeated on each sample. The `device_class` tag is an
21307
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
21308
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20583
21309
  *
20584
21310
  * The `raw` escape hatch carries provider-specific attributes that don't
20585
21311
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20593,6 +21319,14 @@ var SourceInfoSchema = object({
20593
21319
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20594
21320
  * Used to detect rename when `id` changes. */
20595
21321
  uniqueId: string().optional(),
21322
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
21323
+ quantity: ReadingQuantitySchema.optional(),
21324
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
21325
+ dateTimeShape: _enum([
21326
+ "date",
21327
+ "time",
21328
+ "datetime"
21329
+ ]).optional(),
20596
21330
  /** Provider-specific extras that don't fit the structured slots.
20597
21331
  * Whitelist what goes here per provider — do NOT dump entire HA
20598
21332
  * attribute blobs (would explode the meta JSON column). */
@@ -41697,7 +42431,8 @@ method(object({
41697
42431
  "enum"
41698
42432
  ]),
41699
42433
  enumValues: array(string()).optional(),
41700
- item: boolean().optional()
42434
+ item: boolean().optional(),
42435
+ quantity: ReadingQuantitySchema.optional()
41701
42436
  })).readonly(),
41702
42437
  itemArray: object({
41703
42438
  path: string(),
@@ -44098,7 +44833,7 @@ var NetworkAccessStatusSchema = object({
44098
44833
  });
44099
44834
  /**
44100
44835
  * Optional, richer endpoint shape returned by providers that expose
44101
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44836
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
44102
44837
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
44103
44838
  * the originating provider config (mode + sourcePort) so the
44104
44839
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -45581,8 +46316,8 @@ var MaskPolygonShapeSchema = object({
45581
46316
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45582
46317
  var MaskGridShapeSchema = object({
45583
46318
  kind: literal("grid"),
45584
- gridWidth: number(),
45585
- gridHeight: number(),
46319
+ gridWidth: number().meta(readingMeta("none")),
46320
+ gridHeight: number().meta(readingMeta("none")),
45586
46321
  cells: array(boolean())
45587
46322
  });
45588
46323
  discriminatedUnion("kind", [
@@ -45694,6 +46429,8 @@ var NcSystemEventKindSchema = _enum([
45694
46429
  "detection-blind",
45695
46430
  "motion-noisy",
45696
46431
  "motion-calm",
46432
+ "package-llm-silent",
46433
+ "package-llm-answered",
45697
46434
  "recording-stalled",
45698
46435
  "recording-healthy",
45699
46436
  "recording-degraded",
@@ -46783,7 +47520,20 @@ var NcRuleInputSchema = object({
46783
47520
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46784
47521
  * path does not run Zod defaults.
46785
47522
  */
46786
- groupingSec: number().int().min(5).max(60).optional()
47523
+ groupingSec: number().int().min(5).max(60).optional(),
47524
+ /**
47525
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
47526
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
47527
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
47528
+ * same mark the Library writes, and records whose notification it was and
47529
+ * when. A channel with no buttons gets the same two choices as links.
47530
+ *
47531
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
47532
+ * cap path): absent and `false` both mean off, which is what every rule
47533
+ * authored before this field meant. An editor turns it off by writing
47534
+ * `false`, so it needs no `clear` key.
47535
+ */
47536
+ askVerdict: boolean().optional()
46787
47537
  });
46788
47538
  /**
46789
47539
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -57307,6 +58057,28 @@ var PackageVersionInfoSchema = object({
57307
58057
  deprecated: string().optional(),
57308
58058
  distTags: array(string()).readonly()
57309
58059
  });
58060
+ /**
58061
+ * One published document. `ok` is markdown from the registry. `missing` means
58062
+ * the registry answered and the file was not there. `error` is a registry or
58063
+ * tarball failure — never an empty string standing in for either.
58064
+ */
58065
+ var PackageDocBodySchema = discriminatedUnion("status", [
58066
+ object({
58067
+ status: literal("ok"),
58068
+ markdown: string()
58069
+ }),
58070
+ object({ status: literal("missing") }),
58071
+ object({
58072
+ status: literal("error"),
58073
+ message: string()
58074
+ })
58075
+ ]);
58076
+ var PackageDocsSchema = object({
58077
+ name: string(),
58078
+ version: string(),
58079
+ readme: PackageDocBodySchema,
58080
+ changelog: PackageDocBodySchema
58081
+ });
57310
58082
  var SearchResultSchema = object({
57311
58083
  name: string(),
57312
58084
  version: string(),
@@ -57525,7 +58297,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57525
58297
  }), object({ success: literal(true) }), {
57526
58298
  kind: "mutation",
57527
58299
  auth: "admin"
57528
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
58300
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
58301
+ name: string().min(1),
58302
+ version: string().min(1).optional()
58303
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
57529
58304
  kind: "mutation",
57530
58305
  auth: "admin"
57531
58306
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -57584,27 +58359,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57584
58359
  */
57585
58360
  var AirQualitySensorStatusSchema = object({
57586
58361
  /** Carbon dioxide concentration in ppm. */
57587
- co2Ppm: number().min(0).optional(),
58362
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
57588
58363
  /** Total volatile organic compounds in ppb. */
57589
- vocPpb: number().min(0).optional(),
58364
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
57590
58365
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
57591
- pm25: number().min(0).optional(),
58366
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
57592
58367
  /** Particulate matter ≤ 10 μm in µg/m³. */
57593
- pm10: number().min(0).optional(),
58368
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
57594
58369
  /** Composite AQI value (typically 0..500). */
57595
- aqi: number().optional(),
58370
+ aqi: number().optional().meta(readingMeta("index")),
57596
58371
  /** Ms epoch when the slice was last updated. */
57597
- lastFetchedAt: number(),
57598
- /** Live display unit of the single metric this slice carries (e.g. HA
57599
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
57600
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
57601
- * per slice is unambiguous. */
57602
- unit: string().optional(),
57603
- /** Suggested decimal places for numeric display.
57604
- * Populated live from the upstream source when provided (e.g. HA
57605
- * `attributes.suggested_display_precision`). Falls back to
57606
- * auto-formatting when absent. */
57607
- precision: number().int().min(0).max(10).optional()
58372
+ lastFetchedAt: number()
57608
58373
  });
57609
58374
  var airQualitySensorCapability = {
57610
58375
  name: "air-quality-sensor",
@@ -57641,12 +58406,12 @@ var airQualitySensorCapability = {
57641
58406
  },
57642
58407
  pm25: {
57643
58408
  kind: "gauge",
57644
- unit: "ug/m3",
58409
+ unit: "µg/m³",
57645
58410
  deadband: 1
57646
58411
  },
57647
58412
  pm10: {
57648
58413
  kind: "gauge",
57649
- unit: "ug/m3",
58414
+ unit: "µg/m³",
57650
58415
  deadband: 1
57651
58416
  },
57652
58417
  aqi: {
@@ -57664,22 +58429,13 @@ var airQualitySensorCapability = {
57664
58429
  * entries with `device_class: illuminance`.
57665
58430
  */
57666
58431
  var AmbientLightSensorStatusSchema = object({
57667
- /** Current illuminance in lux (lx). */
57668
- lux: number().min(0).meta({
57669
- unit: "lx",
58432
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
58433
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
58434
+ precision: 0,
57670
58435
  label: "Illuminance"
57671
- }),
58436
+ })),
57672
58437
  /** Ms epoch when the slice was last updated. */
57673
- lastFetchedAt: number(),
57674
- /** Live display unit from the upstream source (e.g. HA
57675
- * `attributes.unit_of_measurement`). The UI prefers this over the
57676
- * role's canonical unit. Absent → fall back to the canonical unit. */
57677
- unit: string().optional(),
57678
- /** Suggested decimal places for numeric display.
57679
- * Populated live from the upstream source when provided (e.g. HA
57680
- * `attributes.suggested_display_precision`). Falls back to
57681
- * auto-formatting when absent. */
57682
- precision: number().int().min(0).max(10).optional()
58438
+ lastFetchedAt: number()
57683
58439
  });
57684
58440
  var ambientLightSensorCapability = {
57685
58441
  name: "ambient-light-sensor",
@@ -57711,12 +58467,7 @@ var ambientLightSensorCapability = {
57711
58467
  } } },
57712
58468
  /** Clock fields: written, but excluded from the compare that decides
57713
58469
  * whether persisting is worth a SQLite commit. */
57714
- volatileStateFields: ["lastFetchedAt"],
57715
- customize: {
57716
- version: 1,
57717
- advanced: "derive",
57718
- targets: []
57719
- }
58470
+ volatileStateFields: ["lastFetchedAt"]
57720
58471
  };
57721
58472
  /**
57722
58473
  * Per-class audio metrics aggregated over a sliding window.
@@ -57943,10 +58694,7 @@ var BatteryStatusSchema = object({
57943
58694
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57944
58695
  * declines to band a non-finite reading, which is the correct shape.
57945
58696
  */
57946
- percentage: number().min(0).max(100).nullable().meta({
57947
- unit: "%",
57948
- label: "Charge"
57949
- }),
58697
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57950
58698
  /**
57951
58699
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57952
58700
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57973,7 +58721,14 @@ var BatteryStatusSchema = object({
57973
58721
  */
57974
58722
  sleeping: boolean().meta({ label: "Sleeping" }),
57975
58723
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57976
- lastUpdated: number(),
58724
+ lastUpdated: number().meta(readingMeta("none")),
58725
+ /**
58726
+ * Battery voltage in volts, for a cell the firmware reports as voltage
58727
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
58728
+ * that only report charge. A second reading on this cap, not a second
58729
+ * `numeric-sensor` on the same device.
58730
+ */
58731
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57977
58732
  /**
57978
58733
  * Ms epoch of the last time the device PROVED it was reachable — a
57979
58734
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -58020,12 +58775,12 @@ var BATTERY_DEVICE_TYPES = [
58020
58775
  DeviceType.Switch,
58021
58776
  DeviceType.PetFeeder,
58022
58777
  DeviceType.Container,
58023
- DeviceType.Thermostat
58778
+ DeviceType.Thermostat,
58779
+ DeviceType.EventEmitter
58024
58780
  ];
58025
58781
  /** Generic Customize target for the percentage reading (D936). */
58026
58782
  var batteryCustomize = {
58027
58783
  version: 1,
58028
- advanced: "derive",
58029
58784
  targets: [{
58030
58785
  id: "percentage",
58031
58786
  cardinality: "scalar",
@@ -58034,7 +58789,7 @@ var batteryCustomize = {
58034
58789
  stateField: "percentage"
58035
58790
  },
58036
58791
  deviceTypes: BATTERY_DEVICE_TYPES,
58037
- apply: "add-or-replace",
58792
+ apply: "add",
58038
58793
  path: "percentage",
58039
58794
  sources: [{
58040
58795
  kind: "field",
@@ -58155,10 +58910,16 @@ onStatusChanged: { data: object({
58155
58910
  /**
58156
58911
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
58157
58912
  */
58158
- history: { fields: { percentage: {
58159
- kind: "gauge",
58160
- unit: "%"
58161
- } } },
58913
+ history: { fields: {
58914
+ percentage: {
58915
+ kind: "gauge",
58916
+ unit: "%"
58917
+ },
58918
+ volts: {
58919
+ kind: "gauge",
58920
+ unit: "V"
58921
+ }
58922
+ } },
58162
58923
  customize: batteryCustomize,
58163
58924
  /** Clock fields: written, but excluded from the compare that decides
58164
58925
  * whether persisting is worth a SQLite commit. */
@@ -58205,12 +58966,7 @@ var binaryCapability = {
58205
58966
  history: { fields: { on: { kind: "boolean" } } },
58206
58967
  /** Clock fields: written, but excluded from the compare that decides
58207
58968
  * whether persisting is worth a SQLite commit. */
58208
- volatileStateFields: ["lastChangedAt"],
58209
- customize: {
58210
- version: 1,
58211
- advanced: "derive",
58212
- targets: []
58213
- }
58969
+ volatileStateFields: ["lastChangedAt"]
58214
58970
  };
58215
58971
  /**
58216
58972
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -58226,7 +58982,7 @@ var binaryCapability = {
58226
58982
  */
58227
58983
  var BrightnessStatusSchema = object({
58228
58984
  /** Current level as 0..100 inclusive. Firmware-reported. */
58229
- percentage: number().min(0).max(100),
58985
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
58230
58986
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
58231
58987
  lastChangedAt: number()
58232
58988
  });
@@ -58680,21 +59436,21 @@ var ClimateControlStatusSchema = object({
58680
59436
  /** Single setpoint in Celsius. Used by single-target modes
58681
59437
  * (heat / cool / dry). Null when the device is in a range mode
58682
59438
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58683
- target: number().nullable(),
59439
+ target: number().nullable().meta(readingMeta("temperature")),
58684
59440
  /** Upper bound of the dual setpoint in Celsius. Populated only
58685
59441
  * in `heat_cool` mode. */
58686
- targetHigh: number().nullable(),
59442
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58687
59443
  /** Lower bound of the dual setpoint in Celsius. Populated only
58688
59444
  * in `heat_cool` mode. */
58689
- targetLow: number().nullable(),
59445
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58690
59446
  /** Target relative humidity (0..100). Null when the device has no
58691
59447
  * humidity control. */
58692
- targetHumidity: number().min(0).max(100).nullable(),
59448
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58693
59449
  /** Current measured temperature in Celsius. Read-only — pushed by
58694
59450
  * the device's internal sensor. */
58695
- currentTemp: number().nullable(),
59451
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58696
59452
  /** Current measured relative humidity (0..100). Read-only. */
58697
- currentHumidity: number().min(0).max(100).nullable(),
59453
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58698
59454
  /** Vertical louver swing on/off. Null when the device has no vertical
58699
59455
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58700
59456
  swingVertical: boolean().nullable(),
@@ -58702,11 +59458,11 @@ var ClimateControlStatusSchema = object({
58702
59458
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58703
59459
  swingHorizontal: boolean().nullable(),
58704
59460
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58705
- minTemp: number().nullable().optional(),
59461
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58706
59462
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58707
- maxTemp: number().nullable().optional(),
59463
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58708
59464
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58709
- tempStep: number().positive().nullable().optional(),
59465
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58710
59466
  /** Ms epoch when the slice was last updated (push or command). */
58711
59467
  lastFetchedAt: number()
58712
59468
  });
@@ -58848,17 +59604,17 @@ var climateControlCapability = {
58848
59604
  * modes to expose in the picker without a slice fetch.
58849
59605
  */
58850
59606
  var RgbTripletSchema = object({
58851
- r: number().int().min(0).max(255),
58852
- g: number().int().min(0).max(255),
58853
- b: number().int().min(0).max(255)
59607
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
59608
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
59609
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58854
59610
  });
58855
59611
  var HsvTripletSchema = object({
58856
59612
  /** Hue in degrees, 0..360. */
58857
- h: number().min(0).max(360),
59613
+ h: number().min(0).max(360).meta(readingMeta("none")),
58858
59614
  /** Saturation as 0..100 inclusive. */
58859
- s: number().min(0).max(100),
59615
+ s: number().min(0).max(100).meta(readingMeta("none")),
58860
59616
  /** Value/brightness as 0..100 inclusive. */
58861
- v: number().min(0).max(100)
59617
+ v: number().min(0).max(100).meta(readingMeta("none"))
58862
59618
  });
58863
59619
  var ColorInputSchema = discriminatedUnion("mode", [
58864
59620
  object({
@@ -58887,7 +59643,7 @@ var ColorStatusSchema = object({
58887
59643
  /** Populated when `mode === 'hsv'`. */
58888
59644
  hsv: HsvTripletSchema.optional(),
58889
59645
  /** Populated when `mode === 'mired'`. */
58890
- mireds: number().int().optional(),
59646
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58891
59647
  /** Ms epoch of the last operator-driven change. */
58892
59648
  lastChangedAt: number()
58893
59649
  });
@@ -59082,18 +59838,12 @@ _enum([
59082
59838
  "empty",
59083
59839
  "full"
59084
59840
  ]);
59085
- /** Units a remaining-life counter is reported in. Closed: the panel renders
59086
- * each. */
59087
- var ConsumableRemainingUnitSchema = _enum([
59088
- "days",
59089
- "hours",
59090
- "cycles"
59091
- ]);
59092
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
59093
- * days). `value` null = unknown (D393). */
59841
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
59842
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
59843
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
59094
59844
  var ConsumableRemainingSchema = object({
59095
- value: number().min(0).nullable(),
59096
- unit: ConsumableRemainingUnitSchema
59845
+ value: number().min(0).nullable().meta(readingMeta("device")),
59846
+ kind: ConsumableRemainingKindSchema
59097
59847
  });
59098
59848
  /** A single consumable item. Either a continuous `level` (remaining
59099
59849
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -59106,7 +59856,7 @@ var ConsumableItemSchema = object({
59106
59856
  /** Display name. */
59107
59857
  label: string().min(1),
59108
59858
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
59109
- level: number().min(0).max(100).nullable(),
59859
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59110
59860
  /** The provider's discrete verdict when known (binary mode). */
59111
59861
  status: ConsumableStatusSchema.nullable(),
59112
59862
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -59188,8 +59938,8 @@ var consumablesCustomize = {
59188
59938
  transforms: [{ kind: "identity" }]
59189
59939
  }],
59190
59940
  literals: [{
59191
- path: "remaining.unit",
59192
- values: [...ConsumableRemainingUnitSchema.options]
59941
+ path: "remaining.kind",
59942
+ values: [...ConsumableRemainingKindSchema.options]
59193
59943
  }]
59194
59944
  },
59195
59945
  {
@@ -59318,7 +60068,6 @@ reset: method(object({
59318
60068
  },
59319
60069
  "remaining.value": {
59320
60070
  kind: "gauge",
59321
- unitFrom: "remaining.unit",
59322
60071
  deadband: 1,
59323
60072
  displayName: "Remaining"
59324
60073
  }
@@ -59388,10 +60137,9 @@ var contactCapability = {
59388
60137
  * (empty array for the other kinds). It lives in the slice because
59389
60138
  * HA `select.*` entities may update their options at runtime.
59390
60139
  *
59391
- * The `unit`/`min`/`max`/`step` fields are meaningful for
59392
- * `kind === 'numeric'` and are absent (undefined) for all other
59393
- * kinds. They are populated live from HA attributes on each state
59394
- * push — consistent with `options` and the sensor-unit approach.
60140
+ * Numeric `min` / `max` / `step` live in the slice and share the device
60141
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
60142
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
59395
60143
  */
59396
60144
  var ControlStatusSchema = object({
59397
60145
  kind: _enum([
@@ -59403,39 +60151,17 @@ var ControlStatusSchema = object({
59403
60151
  /** Current value. Numeric kind → number; select/text/datetime → string.
59404
60152
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
59405
60153
  * or time-only — HA accepts all three forms). */
59406
- value: union([number(), string()]),
60154
+ value: union([number().meta(readingMeta("device")), string()]),
59407
60155
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
59408
60156
  options: array(string()),
59409
60157
  /** Ms epoch when the slice was last updated. */
59410
60158
  lastChangedAt: number(),
59411
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
59412
- * Populated live from HA `attributes.unit_of_measurement` on each push.
59413
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59414
- unit: string().optional(),
59415
- /** Minimum allowed value. Populated live from HA `attributes.min`.
59416
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59417
- min: number().optional(),
59418
- /** Maximum allowed value. Populated live from HA `attributes.max`.
59419
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59420
- max: number().optional(),
59421
- /** Step increment. Populated live from HA `attributes.step`.
59422
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59423
- step: number().optional(),
59424
- /** Suggested decimal places for numeric display. Populated live from HA
59425
- * `attributes.suggested_display_precision`, or derived from `step` when the
59426
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
59427
- * other kinds. Falls back to auto-formatting when absent. */
59428
- precision: number().int().min(0).max(10).optional(),
59429
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
59430
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
59431
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
59432
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
59433
- * `has_date`/`has_time` attributes on each push. */
59434
- format: _enum([
59435
- "date",
59436
- "time",
59437
- "datetime"
59438
- ]).optional()
60159
+ /** Minimum allowed value, in the device quantity's base unit. */
60160
+ min: number().optional().meta(readingMeta("device")),
60161
+ /** Maximum allowed value, in the device quantity's base unit. */
60162
+ max: number().optional().meta(readingMeta("device")),
60163
+ /** Step increment, in the device quantity's base unit. */
60164
+ step: number().optional().meta(readingMeta("device"))
59439
60165
  });
59440
60166
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
59441
60167
  object({
@@ -59501,9 +60227,9 @@ var CoverStatusSchema = object({
59501
60227
  ]),
59502
60228
  /** 0 = fully closed, 100 = fully open. Null when the device has
59503
60229
  * no intermediate position surface. */
59504
- position: number().min(0).max(100).nullable(),
60230
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59505
60231
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
59506
- tiltPosition: number().min(0).max(100).nullable(),
60232
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59507
60233
  /** Ms epoch when the slice was last updated. */
59508
60234
  lastChangedAt: number()
59509
60235
  });
@@ -59606,9 +60332,9 @@ var NormalizedRangeSchema$1 = object({
59606
60332
  var DayNightStatusSchema = object({
59607
60333
  mode: DayNightModeSchema,
59608
60334
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
59609
- sensitivity: number().optional(),
60335
+ sensitivity: number().optional().meta(readingMeta("none")),
59610
60336
  /** Delay before the IR-cut filter flips, in seconds. */
59611
- switchDelaySec: number().optional(),
60337
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
59612
60338
  lastFetchedAt: number()
59613
60339
  });
59614
60340
  /**
@@ -59826,11 +60552,11 @@ var DishwasherStatusSchema = object({
59826
60552
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59827
60553
  availablePrograms: array(string()),
59828
60554
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59829
- remainingSeconds: number().int().nonnegative().nullable(),
60555
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59830
60556
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59831
- delayedStartSeconds: number().int().nonnegative().nullable(),
60557
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59832
60558
  /** Cycle completion 0..100, or null when not applicable. */
59833
- progressPercent: number().min(0).max(100).nullable(),
60559
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59834
60560
  /** True when the door is reported open; null when the door state is not known. */
59835
60561
  doorOpen: boolean().nullable(),
59836
60562
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59854,7 +60580,7 @@ var DishwasherStatusSchema = object({
59854
60580
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59855
60581
  machineCareDue: boolean().nullable(),
59856
60582
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59857
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
60583
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59858
60584
  /** Option states: null when the appliance does not expose the option or has not said. */
59859
60585
  halfLoad: boolean().nullable(),
59860
60586
  hygienePlus: boolean().nullable(),
@@ -59869,7 +60595,7 @@ var DishwasherStatusSchema = object({
59869
60595
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59870
60596
  activeEvents: array(string()),
59871
60597
  /** Numeric error code when the appliance reports one; null otherwise. */
59872
- errorCode: number().nullable(),
60598
+ errorCode: number().nullable().meta(readingMeta("none")),
59873
60599
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59874
60600
  errorLabel: string().nullable(),
59875
60601
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -60100,7 +60826,7 @@ var DoorbellStatusSchema = object({
60100
60826
  /** Ms epoch of the last press. null = never observed since this provider started. */
60101
60827
  lastPressedAt: number().nullable(),
60102
60828
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
60103
- pressCountSinceStart: number()
60829
+ pressCountSinceStart: number().meta(readingMeta("count"))
60104
60830
  });
60105
60831
  var DoorbellPressEventSchema = object({
60106
60832
  deviceId: number(),
@@ -60165,17 +60891,7 @@ onPressed: {
60165
60891
  history: "none",
60166
60892
  customize: doorbellCustomize
60167
60893
  };
60168
- /**
60169
- * Enum-state sensor — a string value picked from a finite option set.
60170
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
60171
- * states, weather conditions, contact-source channels, mode strings).
60172
- * The option set is stable across observations; it is persisted in the
60173
- * device config blob (via the control slice at adoption) and is NOT
60174
- * stored in `sourceInfo`.
60175
- */
60176
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
60177
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
60178
- var EnumSensorDateTimeFormatSchema = _enum([
60894
+ _enum([
60179
60895
  "date",
60180
60896
  "time",
60181
60897
  "datetime"
@@ -60183,12 +60899,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
60183
60899
  var EnumSensorStatusSchema = object({
60184
60900
  value: string(),
60185
60901
  /**
60186
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
60187
- * as a locale date/time/datetime; absent for plain enum sensors. HA
60188
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
60189
- * a time-only sensor → `'time'`.
60902
+ * Set when `value` is a timestamp rather than an enum state, so history
60903
+ * does not learn ISO strings as states. The display shape (date / time /
60904
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
60190
60905
  */
60191
- format: EnumSensorDateTimeFormatSchema.optional(),
60906
+ timestamp: boolean().optional(),
60192
60907
  /** Ms epoch when the slice was last updated. */
60193
60908
  lastFetchedAt: number()
60194
60909
  });
@@ -60215,11 +60930,11 @@ var enumSensorCapability = {
60215
60930
  * History (D850): the value as an `enum` series (an appliance's program or
60216
60931
  * operation state, an HVAC action). The option set is per device, so the
60217
60932
  * value list is LEARNED as values are first seen (bounded). A sensor with a
60218
- * `format` carries a timestamp, not a state, and is not recorded.
60933
+ * `timestamp` carries a clock, not a state, and is not recorded.
60219
60934
  */
60220
60935
  history: { fields: { value: {
60221
60936
  kind: "enum",
60222
- skipWhenSet: "format"
60937
+ skipWhenSet: "timestamp"
60223
60938
  } } },
60224
60939
  /** Clock fields: written, but excluded from the compare that decides
60225
60940
  * whether persisting is worth a SQLite commit. */
@@ -60239,13 +60954,13 @@ var EventFireSchema = object({
60239
60954
  deviceId: number(),
60240
60955
  eventType: string(),
60241
60956
  data: record(string(), unknown()).nullable(),
60242
- timestamp: number(),
60243
- seq: number()
60957
+ timestamp: number().meta(readingMeta("none")),
60958
+ seq: number().meta(readingMeta("count"))
60244
60959
  });
60245
60960
  var EventEmitterStatusSchema = object({
60246
60961
  eventTypes: array(string()),
60247
60962
  lastEvent: EventFireSchema.nullable(),
60248
- eventCountSinceStart: number()
60963
+ eventCountSinceStart: number().meta(readingMeta("count"))
60249
60964
  });
60250
60965
  var eventEmitterCapability = {
60251
60966
  name: "event-emitter",
@@ -60658,10 +61373,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60658
61373
  var FanControlStatusSchema = object({
60659
61374
  /** Active speed as 0..100 inclusive. Null when the device has no
60660
61375
  * speed surface (single-speed fan). */
60661
- percentage: number().min(0).max(100).nullable(),
61376
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60662
61377
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60663
61378
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60664
- percentageStep: number().positive().optional(),
61379
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60665
61380
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60666
61381
  * — empty string when no preset is active or supported. */
60667
61382
  preset: string(),
@@ -60782,7 +61497,7 @@ var FeatureProbeStatusSchema = object({
60782
61497
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60783
61498
  model: string().nullable(),
60784
61499
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60785
- channelCount: number().nullable(),
61500
+ channelCount: number().nullable().meta(readingMeta("count")),
60786
61501
  /**
60787
61502
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60788
61503
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60905,9 +61620,151 @@ var gasCapability = {
60905
61620
  * whether persisting is worth a SQLite commit. */
60906
61621
  volatileStateFields: ["lastChangedAt"]
60907
61622
  };
61623
+ var GroupCommandsSchema = object({
61624
+ /** One row per member that has been commanded. A later command replaces that member's row. */
61625
+ results: array(object({
61626
+ deviceId: number().int().nonnegative(),
61627
+ cap: string().min(1),
61628
+ method: string().min(1),
61629
+ ok: boolean(),
61630
+ error: string().optional(),
61631
+ /** Ms epoch of the attempt. */
61632
+ at: number()
61633
+ })) });
61634
+ var groupCommandsCapability = {
61635
+ name: "group-commands",
61636
+ scope: "device",
61637
+ deviceNative: true,
61638
+ mode: "singleton",
61639
+ deviceTypes: [
61640
+ DeviceType.Switch,
61641
+ DeviceType.Light,
61642
+ DeviceType.Fan,
61643
+ DeviceType.Cover,
61644
+ DeviceType.Valve,
61645
+ DeviceType.Lock,
61646
+ DeviceType.MediaPlayer,
61647
+ DeviceType.Thermostat,
61648
+ DeviceType.Climate
61649
+ ],
61650
+ methods: {},
61651
+ status: {
61652
+ schema: GroupCommandsSchema,
61653
+ kind: "push"
61654
+ },
61655
+ runtimeState: GroupCommandsSchema,
61656
+ /**
61657
+ * Runtime-state durability: **session** — the last attempt. A restored
61658
+ * failure would outlive the command that caused it.
61659
+ *
61660
+ * See `RuntimeStateDurability`. Enforced by
61661
+ * `scripts/check-runtime-state-durability.ts`.
61662
+ */
61663
+ durability: "session",
61664
+ history: "none"
61665
+ };
61666
+ /**
61667
+ * Whether a helper actuator group counts as active (D944): at least one
61668
+ * included member is, or every included member is. What "active" means is the
61669
+ * type's: a cover is open, a lock is locked, a media player is playing, a
61670
+ * climate device is not off. A group of lights, switches and fans reports
61671
+ * `switch.on` instead; this cap is for the types whose own state is not a
61672
+ * yes/no. Unknown members are left out (`group-status` counts them) and an
61673
+ * empty included set takes the device offline, never `active: false`.
61674
+ */
61675
+ var GroupActiveSchema = object({
61676
+ active: boolean().meta({ label: "Active" }),
61677
+ /** Ms epoch when the slice was last updated. */
61678
+ lastChangedAt: number()
61679
+ });
61680
+ var groupActiveCapability = {
61681
+ name: "group-active",
61682
+ scope: "device",
61683
+ deviceNative: true,
61684
+ mode: "singleton",
61685
+ deviceTypes: [
61686
+ DeviceType.Cover,
61687
+ DeviceType.Valve,
61688
+ DeviceType.Lock,
61689
+ DeviceType.MediaPlayer,
61690
+ DeviceType.Climate,
61691
+ DeviceType.Thermostat
61692
+ ],
61693
+ methods: {},
61694
+ status: {
61695
+ schema: GroupActiveSchema,
61696
+ kind: "push"
61697
+ },
61698
+ runtimeState: GroupActiveSchema,
61699
+ /**
61700
+ * Runtime-state durability: **session** — recomputed from the members on the
61701
+ * next source event; a restored value would outlive the members it named.
61702
+ *
61703
+ * See `RuntimeStateDurability`. Enforced by
61704
+ * `scripts/check-runtime-state-durability.ts`.
61705
+ */
61706
+ durability: "session",
61707
+ history: "none"
61708
+ };
61709
+ /**
61710
+ * How many members of a helper group answered (D944). Counts are integers,
61711
+ * not measurements: zero included is a real count, never a stand-in reading.
61712
+ * The reading itself stays on the output capability. History is none — the
61713
+ * series that later statistics read is the output cap's own history block.
61714
+ */
61715
+ var GroupStatusSchema = object({
61716
+ /** Members whose current value entered the aggregate. */
61717
+ included: number().int().nonnegative().meta(readingMeta("count", {
61718
+ precision: 0,
61719
+ label: "Reporting"
61720
+ })),
61721
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
61722
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
61723
+ precision: 0,
61724
+ label: "Excluded"
61725
+ })),
61726
+ /** `included` + `excluded`. */
61727
+ total: number().int().nonnegative().meta(readingMeta("count", {
61728
+ precision: 0,
61729
+ label: "Members"
61730
+ }))
61731
+ });
61732
+ var groupStatusCapability = {
61733
+ name: "group-status",
61734
+ scope: "device",
61735
+ deviceNative: true,
61736
+ mode: "singleton",
61737
+ deviceTypes: [
61738
+ DeviceType.Sensor,
61739
+ DeviceType.Switch,
61740
+ DeviceType.Light,
61741
+ DeviceType.Fan,
61742
+ DeviceType.Cover,
61743
+ DeviceType.Valve,
61744
+ DeviceType.Lock,
61745
+ DeviceType.MediaPlayer,
61746
+ DeviceType.Thermostat,
61747
+ DeviceType.Climate
61748
+ ],
61749
+ methods: {},
61750
+ status: {
61751
+ schema: GroupStatusSchema,
61752
+ kind: "push"
61753
+ },
61754
+ runtimeState: GroupStatusSchema,
61755
+ /**
61756
+ * Runtime-state durability: **session** — recomputed from members on the
61757
+ * next source event; a restored count would outlive the readings it named.
61758
+ *
61759
+ * See `RuntimeStateDurability`. Enforced by
61760
+ * `scripts/check-runtime-state-durability.ts`.
61761
+ */
61762
+ durability: "session",
61763
+ history: "none"
61764
+ };
60908
61765
  var HeatingValveStatusSchema = object({
60909
61766
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60910
- position: number().min(0).max(100).nullable(),
61767
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60911
61768
  /** Ms epoch when the slice was last updated (push or poll). */
60912
61769
  lastFetchedAt: number()
60913
61770
  });
@@ -60950,9 +61807,9 @@ var HumidifierStatusSchema = object({
60950
61807
  /** Whether the humidifier is currently on. */
60951
61808
  on: boolean(),
60952
61809
  /** Current measured relative humidity (0..100). Null when not reported. */
60953
- currentHumidity: number().min(0).max(100).nullable(),
61810
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60954
61811
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60955
- targetHumidity: number().min(0).max(100).nullable(),
61812
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60956
61813
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60957
61814
  * device has no mode surface. */
60958
61815
  mode: string().nullable(),
@@ -60962,9 +61819,9 @@ var HumidifierStatusSchema = object({
60962
61819
  * `idle` / `off`). Null when the device doesn't report it. */
60963
61820
  action: string().nullable(),
60964
61821
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60965
- minHumidity: number().nullable(),
61822
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60966
61823
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60967
- maxHumidity: number().nullable(),
61824
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60968
61825
  /** Ms epoch when the slice was last updated. */
60969
61826
  lastChangedAt: number()
60970
61827
  });
@@ -61034,22 +61891,13 @@ var humidifierCapability = {
61034
61891
  * `sourceInfo`.
61035
61892
  */
61036
61893
  var HumiditySensorStatusSchema = object({
61037
- /** Current relative humidity, 0..100. */
61038
- percent: number().min(0).max(100).meta({
61039
- unit: "%",
61894
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
61895
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
61896
+ precision: 0,
61040
61897
  label: "Humidity"
61041
- }),
61898
+ })),
61042
61899
  /** Ms epoch when the slice was last updated. */
61043
- lastFetchedAt: number(),
61044
- /** Live display unit from the upstream source (e.g. HA
61045
- * `attributes.unit_of_measurement`). The UI prefers this over the
61046
- * role's canonical unit. Absent → fall back to the canonical unit. */
61047
- unit: string().optional(),
61048
- /** Suggested decimal places for numeric display.
61049
- * Populated live from the upstream source when provided (e.g. HA
61050
- * `attributes.suggested_display_precision`). Falls back to
61051
- * auto-formatting when absent. */
61052
- precision: number().int().min(0).max(10).optional()
61900
+ lastFetchedAt: number()
61053
61901
  });
61054
61902
  var humiditySensorCapability = {
61055
61903
  name: "humidity-sensor",
@@ -61080,12 +61928,7 @@ var humiditySensorCapability = {
61080
61928
  } } },
61081
61929
  /** Clock fields: written, but excluded from the compare that decides
61082
61930
  * whether persisting is worth a SQLite commit. */
61083
- volatileStateFields: ["lastFetchedAt"],
61084
- customize: {
61085
- version: 1,
61086
- advanced: "derive",
61087
- targets: []
61088
- }
61931
+ volatileStateFields: ["lastFetchedAt"]
61089
61932
  };
61090
61933
  /**
61091
61934
  * Image display cap. Models a single still image exposed by an integration —
@@ -61106,7 +61949,7 @@ var ImageStatusSchema = object({
61106
61949
  * entity exposes no `entity_picture` (yet). */
61107
61950
  url: string().nullable(),
61108
61951
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
61109
- lastUpdated: number().nullable()
61952
+ lastUpdated: number().nullable().meta(readingMeta("none"))
61110
61953
  });
61111
61954
  var imageCapability = {
61112
61955
  name: "image",
@@ -61194,19 +62037,19 @@ var NormalizedRangeSchema = object({
61194
62037
  */
61195
62038
  var ImageSettingsStatusSchema = object({
61196
62039
  /** Normalized 0–100. */
61197
- brightness: number().optional(),
62040
+ brightness: number().optional().meta(readingMeta("none")),
61198
62041
  /** Normalized 0–100. */
61199
- contrast: number().optional(),
62042
+ contrast: number().optional().meta(readingMeta("none")),
61200
62043
  /** Normalized 0–100. */
61201
- saturation: number().optional(),
62044
+ saturation: number().optional().meta(readingMeta("none")),
61202
62045
  /** Normalized 0–100. */
61203
- sharpness: number().optional(),
62046
+ sharpness: number().optional().meta(readingMeta("none")),
61204
62047
  mirror: boolean().optional(),
61205
62048
  flip: boolean().optional(),
61206
62049
  rotate: ImageRotateSchema.optional(),
61207
62050
  whiteBalance: WhiteBalanceModeSchema.optional(),
61208
62051
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
61209
- warmth: number().optional(),
62052
+ warmth: number().optional().meta(readingMeta("none")),
61210
62053
  exposureMode: ExposureModeSchema.optional(),
61211
62054
  backlightMode: BacklightModeSchema.optional(),
61212
62055
  lastFetchedAt: number()
@@ -61485,10 +62328,10 @@ var IntercomAbilitySchema = object({
61485
62328
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
61486
62329
  codecs: array(string()),
61487
62330
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
61488
- sampleRate: number(),
62331
+ sampleRate: number().meta(readingMeta("none")),
61489
62332
  duplex: _enum(["half", "full"]),
61490
62333
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
61491
- maxBacklogMs: number()
62334
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
61492
62335
  });
61493
62336
  var IntercomStatusSchema = object({
61494
62337
  /** True while a talk session is open. */
@@ -61715,13 +62558,13 @@ var LawnMowerControlStatusSchema = object({
61715
62558
  activity: LawnMowerActivitySchema,
61716
62559
  /** 0..100 battery percentage. Null when the device has no battery
61717
62560
  * reading. */
61718
- batteryLevel: number().min(0).max(100).nullable(),
62561
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61719
62562
  /** 0..100 mowing-completion percentage of the current task, or null when no
61720
62563
  * task is active / progress is unavailable. */
61721
- progressPercent: number().min(0).max(100).nullable(),
62564
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61722
62565
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61723
62566
  * or null when unknown. */
61724
- currentCode: number().nullable(),
62567
+ currentCode: number().nullable().meta(readingMeta("none")),
61725
62568
  /** Human label for {@link currentCode}, or null when undecodable. */
61726
62569
  currentCodeLabel: string().nullable(),
61727
62570
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -62174,7 +63017,14 @@ var lockControlCapability = {
62174
63017
  }),
62175
63018
  unlock: method(object({
62176
63019
  deviceId: number().int().nonnegative(),
62177
- code: string().min(1).optional()
63020
+ code: string().min(1).optional(),
63021
+ /**
63022
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
63023
+ * with one call is refused unless the caller says it was confirmed. A
63024
+ * single lock ignores it. The refusal lives in the callee, so no
63025
+ * automation or API caller reaches the doors by skipping a dialog.
63026
+ */
63027
+ confirm: boolean().optional()
62178
63028
  }), _void(), {
62179
63029
  kind: "mutation",
62180
63030
  auth: "protected"
@@ -62211,6 +63061,44 @@ var lockControlCapability = {
62211
63061
  volatileStateFields: ["lastChangedAt"]
62212
63062
  };
62213
63063
  /**
63064
+ * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
63065
+ * so dashboards keep one series; every other instantaneous reading lands here.
63066
+ * `value` is the time integral in the source quantity's accumulated base
63067
+ * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
63068
+ */
63069
+ var MeterStatusSchema = object({
63070
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
63071
+ lastFetchedAt: number()
63072
+ });
63073
+ var meterCapability = {
63074
+ name: "meter",
63075
+ scope: "device",
63076
+ deviceNative: true,
63077
+ mode: "singleton",
63078
+ deviceTypes: [DeviceType.Sensor],
63079
+ methods: {},
63080
+ status: {
63081
+ schema: MeterStatusSchema,
63082
+ kind: "push"
63083
+ },
63084
+ runtimeState: MeterStatusSchema,
63085
+ /**
63086
+ * Runtime-state durability: **restored** — the total is a cache of the
63087
+ * history integral. A restart shows it only until the history pass
63088
+ * recomputes the open period; it is not an in-memory buffer.
63089
+ *
63090
+ * See `RuntimeStateDurability`. Enforced by
63091
+ * `scripts/check-runtime-state-durability.ts`.
63092
+ */
63093
+ durability: "restored",
63094
+ history: { fields: { value: {
63095
+ kind: "counter",
63096
+ deadband: .001,
63097
+ minIntervalMs: 6e4
63098
+ } } },
63099
+ volatileStateFields: ["lastFetchedAt"]
63100
+ };
63101
+ /**
62214
63102
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
62215
63103
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
62216
63104
  * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
@@ -62254,7 +63142,7 @@ var MediaPlayerStatusSchema = object({
62254
63142
  state: MediaPlayerStateSchema,
62255
63143
  /** Volume as 0..100 inclusive. Null when the device has no volume
62256
63144
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
62257
- volumeLevel: number().min(0).max(100).nullable(),
63145
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62258
63146
  /** Mute toggle distinct from volume=0. Null when no mute surface.
62259
63147
  * Pair with `DeviceFeature.MediaPlayerMute`. */
62260
63148
  isMuted: boolean().nullable(),
@@ -62267,10 +63155,10 @@ var MediaPlayerStatusSchema = object({
62267
63155
  currentMedia: MediaInfoSchema.nullable(),
62268
63156
  /** Current playback position in ms. Null when not seekable or
62269
63157
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
62270
- positionMs: number().int().nonnegative().nullable(),
63158
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62271
63159
  /** Total duration of the current media in ms. Null when unknown
62272
63160
  * (live stream) or nothing is playing. */
62273
- durationMs: number().int().nonnegative().nullable(),
63161
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62274
63162
  /** Shuffle toggle. Null when no shuffle surface.
62275
63163
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
62276
63164
  shuffle: boolean().nullable(),
@@ -62579,7 +63467,7 @@ var motionCustomize = {
62579
63467
  eventName: "onMotionChanged"
62580
63468
  },
62581
63469
  deviceTypes: [DeviceType.Camera],
62582
- apply: "replace",
63470
+ apply: "add",
62583
63471
  applyAuthority: {
62584
63472
  kind: "camera-sensors",
62585
63473
  timeoutField: "motionCooldownMs"
@@ -62651,7 +63539,7 @@ var MotionStatusSchema = object({
62651
63539
  * it, every provider seeded it at boot, and every session left it
62652
63540
  * `detected: true` after its detach.
62653
63541
  */
62654
- autoClearAfterMs: number().nullable()
63542
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62655
63543
  });
62656
63544
  /**
62657
63545
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62688,12 +63576,18 @@ var motionCapability = {
62688
63576
  methods: {
62689
63577
  /**
62690
63578
  * Pull the current motion state synchronously. Convenience shortcut
62691
- * for consumers that don't need the full status object; equivalent
62692
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62693
- * `getStatus` once the auto-injected status surface lands in every
62694
- * consumer.
63579
+ * for consumers that don't need the full status object. A composed
63580
+ * group answers from its slice (D944): unknown throws, it is not off.
62695
63581
  */
62696
63582
  isDetected: method(object({ deviceId: number() }), boolean()) },
63583
+ /**
63584
+ * A composed motion group serves `isDetected` from `detected` (D944).
63585
+ * Without this the cap stays uncomposable (`has-commands`).
63586
+ */
63587
+ composedMethods: { isDetected: {
63588
+ kind: "slice",
63589
+ field: "detected"
63590
+ } },
62697
63591
  events: {
62698
63592
  /**
62699
63593
  * Fires every time the runner transitions a camera between
@@ -62833,7 +63727,7 @@ var MotionZoneRegionSchema = object({
62833
63727
  * the grid region(s). */
62834
63728
  var MotionZoneStatusSchema = object({
62835
63729
  enabled: boolean(),
62836
- sensitivity: number(),
63730
+ sensitivity: number().meta(readingMeta("none")),
62837
63731
  /** Grid region(s). Today exactly one `grid` shape. */
62838
63732
  regions: array(MotionZoneRegionSchema),
62839
63733
  lastFetchedAt: number()
@@ -63203,7 +64097,7 @@ var NavigationStatusSchema = object({
63203
64097
  following: boolean(),
63204
64098
  flash: boolean(),
63205
64099
  lightMode: NavigationLightModeSchema,
63206
- lightLevel: number().min(40).max(100),
64100
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
63207
64101
  /** Ms epoch when the slice was last updated. */
63208
64102
  lastChangedAt: number()
63209
64103
  });
@@ -63337,13 +64231,13 @@ var NetworkLinkStatusSchema = object({
63337
64231
  * one whose reading has not landed must not be drawn at 0 %. Consumers
63338
64232
  * SKIP a null rather than coerce it.
63339
64233
  */
63340
- signalPercent: number().min(0).max(100).nullable(),
64234
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63341
64235
  /** Raw received signal strength in dBm, when the firmware reports one. */
63342
- rssiDbm: number().optional(),
64236
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
63343
64237
  /** Network name of a wireless link, when the firmware reports it. */
63344
64238
  ssid: string().optional(),
63345
64239
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
63346
- lastUpdated: number()
64240
+ lastUpdated: number().meta(readingMeta("none"))
63347
64241
  });
63348
64242
  /** The slice a provider seeds before its first read: nothing is known yet. */
63349
64243
  var NETWORK_LINK_UNKNOWN = {
@@ -63748,7 +64642,7 @@ var NotifierStatusSchema = object({
63748
64642
  lastError: string().nullable(),
63749
64643
  /** Number of deliveries currently buffered server-side (rate-limited
63750
64644
  * notifiers may queue). 0 when the provider sends synchronously. */
63751
- queueDepth: number().int().nonnegative(),
64645
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63752
64646
  /** Per-feature capability matrix — authoritative source for the
63753
64647
  * compose-form field gating. */
63754
64648
  supports: NotifierSupportsSchema
@@ -63840,11 +64734,9 @@ onSent: { data: object({
63840
64734
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63841
64735
  * for: water flow, distance, weight, frequency, signal strength, …).
63842
64736
  *
63843
- * The `unit` and `precision` fields carry the live display metadata that
63844
- * the upstream source provides with each state push (HA
63845
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63846
- * The UI reads them directly from the slice — they are the single source
63847
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
64737
+ * The quantity is not on this slice. It is chosen when the device is
64738
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
64739
+ * that quantity's base unit. `null` is a refused sample (D393).
63848
64740
  *
63849
64741
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63850
64742
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63853,15 +64745,8 @@ onSent: { data: object({
63853
64745
  * numeric values land here.
63854
64746
  */
63855
64747
  var NumericSensorStatusSchema = object({
63856
- value: number(),
63857
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63858
- * Populated live from the upstream source on each state push.
63859
- * Absent for unitless sensors. */
63860
- unit: string().optional(),
63861
- /** Suggested decimal places for numeric display.
63862
- * Populated live from the upstream source when provided.
63863
- * Falls back to auto-formatting when absent. */
63864
- precision: number().int().min(0).max(10).optional(),
64748
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
64749
+ value: number().nullable().meta(readingMeta("device")),
63865
64750
  /** Ms epoch when the slice was last updated. */
63866
64751
  lastFetchedAt: number()
63867
64752
  });
@@ -63885,11 +64770,11 @@ var numericSensorCapability = {
63885
64770
  */
63886
64771
  durability: "restored",
63887
64772
  /**
63888
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
64773
+ * History (D850): recorded series. The unit is the device quantity's base,
64774
+ * resolved when the series is read — the cap has no single unit.
63889
64775
  */
63890
64776
  history: { fields: { value: {
63891
64777
  kind: "gauge",
63892
- unitFrom: "unit",
63893
64778
  minIntervalMs: 3e4
63894
64779
  } } },
63895
64780
  /** Clock fields: written, but excluded from the compare that decides
@@ -64227,23 +65112,26 @@ var PetFeederStatusSchema = object({
64227
65112
  /** Food currently in the bowl (grams). Null when the device has not
64228
65113
  * reported a reading yet. On dual-hopper models this is the combined
64229
65114
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
64230
- foodLevel: number().nullable(),
65115
+ foodLevel: number().nullable().meta(readingMeta("none")),
64231
65116
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
64232
65117
  * single-hopper models. */
64233
- food1: number().nullable(),
65118
+ food1: number().nullable().meta(readingMeta("none")),
64234
65119
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
64235
65120
  * single-hopper models. */
64236
- food2: number().nullable(),
65121
+ food2: number().nullable().meta(readingMeta("none")),
64237
65122
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
64238
65123
  * (`device_class: problem`, on = low). True when the bowl is empty /
64239
65124
  * below the feeder's low threshold. */
64240
65125
  lowFood: boolean(),
64241
65126
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
64242
65127
  * device has no battery reading. */
64243
- batteryPower: number().min(0).max(100).nullable(),
64244
- /** Days of desiccant life remaining. Null when the model has no
64245
- * desiccant sensor. */
64246
- desiccantLeftDays: number().nullable(),
65128
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
65129
+ /**
65130
+ * Desiccant life remaining, stored in seconds (duration base, D944).
65131
+ * Providers convert from the days the feeder reports. Null when the
65132
+ * model has no desiccant sensor.
65133
+ */
65134
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
64247
65135
  /** True while a feed is in progress. */
64248
65136
  feeding: boolean(),
64249
65137
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -64254,7 +65142,7 @@ var PetFeederStatusSchema = object({
64254
65142
  * with `errorCode` for consumers that want the raw integer. */
64255
65143
  error: string().nullable(),
64256
65144
  /** Raw device error code (0 / null = no error). */
64257
- errorCode: number().nullable(),
65145
+ errorCode: number().nullable().meta(readingMeta("none")),
64258
65146
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
64259
65147
  isDualHopper: boolean(),
64260
65148
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -64264,7 +65152,7 @@ var PetFeederStatusSchema = object({
64264
65152
  /** Play a chime when dispensing. */
64265
65153
  feedSound: boolean(),
64266
65154
  /** Speaker / prompt volume level (device-scaled integer). */
64267
- volume: number(),
65155
+ volume: number().meta(readingMeta("none")),
64268
65156
  /** Ms epoch when the slice was last refreshed from the cloud. */
64269
65157
  lastFetchedAt: number()
64270
65158
  });
@@ -64383,7 +65271,7 @@ var petFeederCapability = {
64383
65271
  },
64384
65272
  desiccantLeftDays: {
64385
65273
  kind: "gauge",
64386
- unit: "days"
65274
+ unit: "s"
64387
65275
  }
64388
65276
  } }
64389
65277
  };
@@ -64784,43 +65672,88 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64784
65672
  */
64785
65673
  var PowerMeterStatusSchema = object({
64786
65674
  /** Instantaneous power draw in watts. */
64787
- watts: number().optional().meta({
64788
- unit: "W",
64789
- label: "Power"
64790
- }),
65675
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64791
65676
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64792
- kwhTotal: number().optional().meta({
64793
- unit: "kWh",
64794
- label: "Energy"
64795
- }),
65677
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64796
65678
  /** Voltage in volts. */
64797
- volts: number().optional(),
65679
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
64798
65680
  /** Current in amperes. */
64799
- amps: number().optional(),
64800
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64801
- va: number().optional(),
65681
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
65682
+ /** Apparent power in volt-amperes. */
65683
+ va: number().optional().meta(readingMeta("apparent-power")),
64802
65684
  /** Power factor (0..1) if reported. */
64803
- powerFactor: number().min(0).max(1).optional(),
65685
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64804
65686
  /** Ms epoch when the slice was last updated. */
64805
- lastFetchedAt: number(),
64806
- /** Live display unit of the single metric this slice carries (e.g. HA
64807
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64808
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64809
- * unit per slice is unambiguous. The UI prefers this over the role's
64810
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64811
- unit: string().optional(),
64812
- /** Suggested decimal places for numeric display.
64813
- * Populated live from the upstream source when provided (e.g. HA
64814
- * `attributes.suggested_display_precision`). Falls back to
64815
- * auto-formatting when absent. */
64816
- precision: number().int().min(0).max(10).optional()
65687
+ lastFetchedAt: number()
64817
65688
  });
65689
+ /**
65690
+ * Loads that draw metered mains power (D951). The reading is a feature of the
65691
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
65692
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
65693
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
65694
+ * camera and the like are left out: their draw is not what anyone meters.
65695
+ */
65696
+ var POWER_METER_DEVICE_TYPES = [
65697
+ DeviceType.Sensor,
65698
+ DeviceType.Switch,
65699
+ DeviceType.Light,
65700
+ DeviceType.Fan,
65701
+ DeviceType.Climate,
65702
+ DeviceType.Thermostat,
65703
+ DeviceType.Humidifier,
65704
+ DeviceType.WaterHeater,
65705
+ DeviceType.MediaPlayer,
65706
+ DeviceType.Washer,
65707
+ DeviceType.Dryer,
65708
+ DeviceType.Dishwasher,
65709
+ DeviceType.Vacuum,
65710
+ DeviceType.LawnMower,
65711
+ DeviceType.PetFeeder
65712
+ ];
65713
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
65714
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
65715
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
65716
+ function powerTarget(id, unit) {
65717
+ return {
65718
+ id,
65719
+ cardinality: "scalar",
65720
+ vocabulary: {
65721
+ cap: "power-meter",
65722
+ stateField: id
65723
+ },
65724
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
65725
+ apply: "add",
65726
+ path: id,
65727
+ sources: [{
65728
+ kind: "numeric",
65729
+ requiredUnit: unit,
65730
+ transforms: [{ kind: "identity" }]
65731
+ }],
65732
+ companions: [{
65733
+ kind: "clock",
65734
+ path: "lastFetchedAt",
65735
+ when: "always"
65736
+ }],
65737
+ sourceReport: { status: "assignment-status" },
65738
+ advancedExpression: true
65739
+ };
65740
+ }
65741
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
65742
+ var powerMeterCustomize = {
65743
+ version: 1,
65744
+ targets: [
65745
+ powerTarget("watts", "W"),
65746
+ powerTarget("kwhTotal", "kWh"),
65747
+ powerTarget("volts", "V"),
65748
+ powerTarget("amps", "A")
65749
+ ]
65750
+ };
64818
65751
  var powerMeterCapability = {
64819
65752
  name: "power-meter",
64820
65753
  scope: "device",
64821
65754
  deviceNative: true,
64822
65755
  mode: "singleton",
64823
- deviceTypes: [DeviceType.Sensor],
65756
+ deviceTypes: POWER_METER_DEVICE_TYPES,
64824
65757
  methods: {},
64825
65758
  status: {
64826
65759
  schema: PowerMeterStatusSchema,
@@ -64878,11 +65811,7 @@ var powerMeterCapability = {
64878
65811
  /** Clock fields: written, but excluded from the compare that decides
64879
65812
  * whether persisting is worth a SQLite commit. */
64880
65813
  volatileStateFields: ["lastFetchedAt"],
64881
- customize: {
64882
- version: 1,
64883
- advanced: "derive",
64884
- targets: []
64885
- }
65814
+ customize: powerMeterCustomize
64886
65815
  };
64887
65816
  /**
64888
65817
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64902,11 +65831,11 @@ var powerMeterCapability = {
64902
65831
  */
64903
65832
  var GpsLocationSchema = object({
64904
65833
  /** Latitude in decimal degrees, -90..90. */
64905
- latitude: number().min(-90).max(90),
65834
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64906
65835
  /** Longitude in decimal degrees, -180..180. */
64907
- longitude: number().min(-180).max(180),
65836
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64908
65837
  /** Reported accuracy in meters (lower = better). */
64909
- accuracyMeters: number().nonnegative()
65838
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64910
65839
  });
64911
65840
  var PresenceStatusSchema = object({
64912
65841
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64918,7 +65847,7 @@ var PresenceStatusSchema = object({
64918
65847
  gps: GpsLocationSchema.nullable(),
64919
65848
  /** Optional battery level of the tracking device (0..100). Null
64920
65849
  * when not reported. */
64921
- batteryPercent: number().min(0).max(100).nullable(),
65850
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64922
65851
  /** Ms epoch when the slice was last updated. */
64923
65852
  lastChangedAt: number()
64924
65853
  });
@@ -64968,19 +65897,10 @@ var presenceCapability = {
64968
65897
  * `sourceInfo`.
64969
65898
  */
64970
65899
  var PressureSensorStatusSchema = object({
64971
- /** Current pressure in hPa. */
64972
- hpa: number(),
65900
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
65901
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64973
65902
  /** Ms epoch when the slice was last updated. */
64974
- lastFetchedAt: number(),
64975
- /** Live display unit from the upstream source (e.g. HA
64976
- * `attributes.unit_of_measurement`). The UI prefers this over the
64977
- * role's canonical unit. Absent → fall back to the canonical unit. */
64978
- unit: string().optional(),
64979
- /** Suggested decimal places for numeric display.
64980
- * Populated live from the upstream source when provided (e.g. HA
64981
- * `attributes.suggested_display_precision`). Falls back to
64982
- * auto-formatting when absent. */
64983
- precision: number().int().min(0).max(10).optional()
65903
+ lastFetchedAt: number()
64984
65904
  });
64985
65905
  var pressureSensorCapability = {
64986
65906
  name: "pressure-sensor",
@@ -65506,8 +66426,8 @@ var ptzCapability = {
65506
66426
  };
65507
66427
  var PtzAutotrackSettingsSchema = object({
65508
66428
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
65509
- stopDelaySeconds: number().int().min(0).max(300),
65510
- disappearDelaySeconds: number().int().min(0).max(300)
66429
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
66430
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
65511
66431
  });
65512
66432
  /**
65513
66433
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65661,7 +66581,7 @@ var RunningStateStatusSchema = object({
65661
66581
  /** null = unknown — never coerced to false (D393). */
65662
66582
  running: boolean().nullable(),
65663
66583
  /** Epoch ms the current boolean began; null while `running` is null. */
65664
- since: number().nullable(),
66584
+ since: number().nullable().meta(readingMeta("none")),
65665
66585
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65666
66586
  phase: string().nullable(),
65667
66587
  /** null until a reading has been published (D393). */
@@ -65676,7 +66596,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65676
66596
  /** Generic Customize target for the running boolean (D936). */
65677
66597
  var runningCustomize = {
65678
66598
  version: 1,
65679
- advanced: "derive",
65680
66599
  targets: [{
65681
66600
  id: "running",
65682
66601
  cardinality: "scalar",
@@ -65685,6 +66604,8 @@ var runningCustomize = {
65685
66604
  stateField: "running"
65686
66605
  },
65687
66606
  deviceTypes: [
66607
+ DeviceType.Washer,
66608
+ DeviceType.Dryer,
65688
66609
  DeviceType.Dishwasher,
65689
66610
  DeviceType.Vacuum,
65690
66611
  DeviceType.LawnMower,
@@ -65696,7 +66617,7 @@ var runningCustomize = {
65696
66617
  DeviceType.Thermostat,
65697
66618
  DeviceType.PetFeeder
65698
66619
  ],
65699
- apply: "add-or-replace",
66620
+ apply: "add",
65700
66621
  path: "running",
65701
66622
  sources: [
65702
66623
  {
@@ -66989,9 +67910,9 @@ var RecordTriggerSchema = _enum([
66989
67910
  */
66990
67911
  var RecordWindowSchema = object({
66991
67912
  trigger: RecordTriggerSchema,
66992
- day: number().int().min(0).max(6),
66993
- startMinute: number().int().min(0).max(1439),
66994
- endMinute: number().int().min(1).max(1440)
67913
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
67914
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
67915
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
66995
67916
  });
66996
67917
  /** Status of one physical volume, as the camera itself describes it. */
66997
67918
  var OnboardStorageVolumeSchema = object({
@@ -67067,10 +67988,10 @@ var RecordingOnboardStatusSchema = object({
67067
67988
  /** Master "record to the card at all" switch. */
67068
67989
  enabled: boolean().nullable(),
67069
67990
  overwriteWhenFull: boolean().nullable(),
67070
- preRecordSec: number().nullable(),
67071
- postRecordSec: number().nullable(),
67991
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
67992
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
67072
67993
  /** Length of one recorded file, in minutes. */
67073
- segmentMinutes: number().nullable(),
67994
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
67074
67995
  /** The primary track's weekly windows, flattened. */
67075
67996
  windows: array(RecordWindowSchema),
67076
67997
  /**
@@ -67086,7 +68007,7 @@ var RecordingOnboardStatusSchema = object({
67086
68007
  * provider reporting a non-zero count also reports the schedule as not
67087
68008
  * writable.
67088
68009
  */
67089
- unreadableWindows: number(),
68010
+ unreadableWindows: number().meta(readingMeta("count")),
67090
68011
  /**
67091
68012
  * The camera is scheduled to record and has NO usable storage.
67092
68013
  *
@@ -68096,7 +69017,7 @@ var CamStreamDescriptorSchema = object({
68096
69017
  url: string().optional(),
68097
69018
  codec: string().optional(),
68098
69019
  resolution: CamStreamResolutionSchema.optional(),
68099
- fps: number().positive().optional(),
69020
+ fps: number().positive().optional().meta(readingMeta("rate")),
68100
69021
  label: string().optional(),
68101
69022
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
68102
69023
  deviceFeatures: array(string()).optional(),
@@ -68183,18 +69104,18 @@ var StreamProfileSchema = _enum([
68183
69104
  ]);
68184
69105
  /** The current encoding config of one stream profile. */
68185
69106
  var StreamProfileConfigSchema = object({
68186
- width: number(),
68187
- height: number(),
69107
+ width: number().meta(readingMeta("none")),
69108
+ height: number().meta(readingMeta("none")),
68188
69109
  codec: _enum(["h264", "h265"]),
68189
- framerate: number(),
68190
- bitrate: number(),
69110
+ framerate: number().meta(readingMeta("rate")),
69111
+ bitrate: number().meta(readingMeta("bitrate")),
68191
69112
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
68192
69113
  encoderProfile: _enum([
68193
69114
  "high",
68194
69115
  "main",
68195
69116
  "baseline"
68196
69117
  ]).optional(),
68197
- gop: number().optional(),
69118
+ gop: number().optional().meta(readingMeta("none")),
68198
69119
  /**
68199
69120
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
68200
69121
  *
@@ -68566,7 +69487,8 @@ var switchCapability = {
68566
69487
  deviceTypes: [
68567
69488
  DeviceType.Switch,
68568
69489
  DeviceType.Siren,
68569
- DeviceType.Light
69490
+ DeviceType.Light,
69491
+ DeviceType.Fan
68570
69492
  ],
68571
69493
  methods: { setState: method(object({
68572
69494
  deviceId: number(),
@@ -68613,12 +69535,7 @@ var switchCapability = {
68613
69535
  label: "On",
68614
69536
  kind: "boolean"
68615
69537
  }
68616
- }] },
68617
- customize: {
68618
- version: 1,
68619
- advanced: "derive",
68620
- targets: []
68621
- }
69538
+ }] }
68622
69539
  };
68623
69540
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
68624
69541
  function hasControlCharacter(value) {
@@ -69201,35 +70118,21 @@ var tamperCapability = {
69201
70118
  * entries with `device_class: temperature` and any future native
69202
70119
  * thermometer.
69203
70120
  *
69204
- * Unit normalisation: providers convert to Celsius before storing.
69205
- * The slice value is always Celsius so cross-cap aggregators
69206
- * (climate-control's `currentTemp`, energy analytics) can compose
69207
- * without per-source unit fixups. The canonical display unit (`°C`) is
69208
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
69209
- * `sourceInfo`.
70121
+ * The number is Celsius (D944). Providers convert on ingest. A failed
70122
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
69210
70123
  *
69211
70124
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
69212
70125
  * frozen feed (provider hung) distinct from a "temperature hasn't
69213
70126
  * changed" steady state.
69214
70127
  */
69215
70128
  var TemperatureSensorStatusSchema = object({
69216
- /** Current temperature in Celsius. */
69217
- celsius: number().meta({
69218
- unit: "°C",
70129
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
70130
+ celsius: number().nullable().meta(readingMeta("temperature", {
70131
+ precision: 1,
69219
70132
  label: "Temperature"
69220
- }),
70133
+ })),
69221
70134
  /** Ms epoch when the slice was last updated (push or poll). */
69222
- lastFetchedAt: number(),
69223
- /** Live display unit from the upstream source (e.g. HA
69224
- * `attributes.unit_of_measurement`). The UI prefers this over the
69225
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
69226
- * Absent → the UI falls back to the role's canonical unit. */
69227
- unit: string().optional(),
69228
- /** Suggested decimal places for numeric display.
69229
- * Populated live from the upstream source when provided (e.g. HA
69230
- * `attributes.suggested_display_precision`). Falls back to
69231
- * auto-formatting when absent. */
69232
- precision: number().int().min(0).max(10).optional()
70135
+ lastFetchedAt: number()
69233
70136
  });
69234
70137
  var temperatureSensorCapability = {
69235
70138
  name: "temperature-sensor",
@@ -69260,12 +70163,7 @@ var temperatureSensorCapability = {
69260
70163
  } } },
69261
70164
  /** Clock fields: written, but excluded from the compare that decides
69262
70165
  * whether persisting is worth a SQLite commit. */
69263
- volatileStateFields: ["lastFetchedAt"],
69264
- customize: {
69265
- version: 1,
69266
- advanced: "derive",
69267
- targets: []
69268
- }
70166
+ volatileStateFields: ["lastFetchedAt"]
69269
70167
  };
69270
70168
  /**
69271
70169
  * toast — system-scoped singleton capability that streams toast
@@ -69548,16 +70446,16 @@ var VacuumControlStatusSchema = object({
69548
70446
  ]),
69549
70447
  /** 0..100 battery percentage. Null when the device has no battery
69550
70448
  * reading. */
69551
- batteryLevel: number().min(0).max(100).nullable(),
70449
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69552
70450
  /** Current fan-speed token (provider-verbatim). Null when unknown or
69553
70451
  * the vacuum has no speed control. */
69554
70452
  fanSpeed: string().nullable(),
69555
70453
  /** Speed tokens the hardware accepts — drives the UI selector. */
69556
70454
  availableFanSpeeds: array(string()),
69557
70455
  /** 0..100 cleaning-completion percentage of the current task, or null. */
69558
- progressPercent: number().min(0).max(100).nullable(),
70456
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69559
70457
  /** Current error code (0 / null = no error). */
69560
- errorCode: number().nullable(),
70458
+ errorCode: number().nullable().meta(readingMeta("none")),
69561
70459
  /** Human label for {@link errorCode}, or null when none / undecodable. */
69562
70460
  errorLabel: string().nullable(),
69563
70461
  /** Ms epoch when the slice was last updated. */
@@ -69635,7 +70533,7 @@ var ValveStatusSchema = object({
69635
70533
  ]),
69636
70534
  /** 0 = fully closed, 100 = fully open. Null when the device has no
69637
70535
  * intermediate position surface. */
69638
- position: number().min(0).max(100).nullable(),
70536
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69639
70537
  /** Ms epoch when the slice was last updated. */
69640
70538
  lastChangedAt: number()
69641
70539
  });
@@ -69743,9 +70641,9 @@ var vibrationCapability = {
69743
70641
  */
69744
70642
  var WaterHeaterStatusSchema = object({
69745
70643
  /** Current measured temperature. Null when not reported. */
69746
- currentTemp: number().nullable(),
70644
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
69747
70645
  /** Target temperature setpoint. Null when no setpoint surface. */
69748
- targetTemp: number().nullable(),
70646
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
69749
70647
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
69750
70648
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
69751
70649
  * device reports an unknown state. */
@@ -69756,9 +70654,9 @@ var WaterHeaterStatusSchema = object({
69756
70654
  * has no away surface. */
69757
70655
  away: boolean().nullable(),
69758
70656
  /** HA `min_temp` attribute. Null when not reported. */
69759
- minTemp: number().nullable(),
70657
+ minTemp: number().nullable().meta(readingMeta("temperature")),
69760
70658
  /** HA `max_temp` attribute. Null when not reported. */
69761
- maxTemp: number().nullable(),
70659
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
69762
70660
  /** Ms epoch when the slice was last updated. */
69763
70661
  lastChangedAt: number()
69764
70662
  });
@@ -69843,25 +70741,18 @@ var waterHeaterCapability = {
69843
70741
  * Forecast deferred — current conditions only for v1.
69844
70742
  */
69845
70743
  var WeatherStatusSchema = object({
69846
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69847
- * when no condition has been reported yet. */
70744
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69848
70745
  condition: string().nullable(),
69849
- /** Current temperature in the reported unit. Null when not provided. */
69850
- temperature: number().nullable(),
69851
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69852
- temperatureUnit: string().nullable(),
70746
+ /** Current temperature in Celsius. Null when not provided or refused. */
70747
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69853
70748
  /** Relative humidity (0..100). Null when not provided. */
69854
- humidity: number().min(0).max(100).nullable(),
69855
- /** Barometric pressure in the reported unit. Null when not provided. */
69856
- pressure: number().nullable(),
69857
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69858
- pressureUnit: string().nullable(),
69859
- /** Wind speed in the reported unit. Null when not provided. */
69860
- windSpeed: number().nullable(),
69861
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69862
- windSpeedUnit: string().nullable(),
70749
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
70750
+ /** Barometric pressure in hPa. Null when not provided or refused. */
70751
+ pressure: number().nullable().meta(readingMeta("pressure")),
70752
+ /** Wind speed in metres per second. Null when not provided or refused. */
70753
+ windSpeed: number().nullable().meta(readingMeta("speed")),
69863
70754
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69864
- windBearing: number().nullable(),
70755
+ windBearing: number().nullable().meta(readingMeta("angle")),
69865
70756
  /** Ms epoch when the slice was last updated. */
69866
70757
  lastFetchedAt: number()
69867
70758
  });
@@ -69889,12 +70780,12 @@ var weatherCapability = {
69889
70780
  */
69890
70781
  durability: "session",
69891
70782
  /**
69892
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
70783
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69893
70784
  */
69894
70785
  history: { fields: {
69895
70786
  temperature: {
69896
70787
  kind: "gauge",
69897
- unitFrom: "temperatureUnit",
70788
+ unit: "°C",
69898
70789
  deadband: .2
69899
70790
  },
69900
70791
  humidity: {
@@ -69904,12 +70795,12 @@ var weatherCapability = {
69904
70795
  },
69905
70796
  pressure: {
69906
70797
  kind: "gauge",
69907
- unitFrom: "pressureUnit",
70798
+ unit: "hPa",
69908
70799
  deadband: .5
69909
70800
  },
69910
70801
  windSpeed: {
69911
70802
  kind: "gauge",
69912
- unitFrom: "windSpeedUnit",
70803
+ unit: "m/s",
69913
70804
  deadband: .5
69914
70805
  }
69915
70806
  } }
@@ -70943,8 +71834,9 @@ function withClipEventLabels(provider, deps) {
70943
71834
  listClips
70944
71835
  };
70945
71836
  }
71837
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
70946
71838
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
70947
- 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;
71839
+ 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;
70948
71840
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
70949
71841
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
70950
71842
  new Set(CAP_NAMES);
@@ -71288,6 +72180,9 @@ var DEVICE_LOCAL_STATE_CAPS = {
71288
72180
  featureProbe: featureProbeCapability,
71289
72181
  flood: floodCapability,
71290
72182
  gas: gasCapability,
72183
+ groupActive: groupActiveCapability,
72184
+ groupCommands: groupCommandsCapability,
72185
+ groupStatus: groupStatusCapability,
71291
72186
  heatingValve: heatingValveCapability,
71292
72187
  humidifier: humidifierCapability,
71293
72188
  humiditySensor: humiditySensorCapability,
@@ -71297,6 +72192,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
71297
72192
  lawnMowerControl: lawnMowerControlCapability,
71298
72193
  lockControl: lockControlCapability,
71299
72194
  mediaPlayer: mediaPlayerCapability,
72195
+ meter: meterCapability,
71300
72196
  motion: motionCapability,
71301
72197
  motionTrigger: motionTriggerCapability,
71302
72198
  motionZones: motionZonesCapability,
@@ -72556,7 +73452,7 @@ var BaseDeviceProvider = class extends BaseAddon {
72556
73452
  return toDeviceSummary(device, this.addonId);
72557
73453
  }
72558
73454
  };
72559
- 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;
73455
+ 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;
72560
73456
  new Set(Object.values(DeviceType));
72561
73457
  Object.freeze({
72562
73458
  "accessories.getStatus": {
@@ -72664,6 +73560,13 @@ Object.freeze({
72664
73560
  access: "view",
72665
73561
  role: "admin"
72666
73562
  },
73563
+ "addons.getPackageDocs": {
73564
+ capName: "addons",
73565
+ capScope: "system",
73566
+ addonId: null,
73567
+ access: "view",
73568
+ role: "admin"
73569
+ },
72667
73570
  "addons.getVersions": {
72668
73571
  capName: "addons",
72669
73572
  capScope: "system",
@@ -75772,6 +76675,27 @@ Object.freeze({
75772
76675
  access: "view",
75773
76676
  role: "viewer"
75774
76677
  },
76678
+ "groupActive.getStatus": {
76679
+ capName: "group-active",
76680
+ capScope: "device",
76681
+ addonId: null,
76682
+ access: "view",
76683
+ role: "viewer"
76684
+ },
76685
+ "groupCommands.getStatus": {
76686
+ capName: "group-commands",
76687
+ capScope: "device",
76688
+ addonId: null,
76689
+ access: "view",
76690
+ role: "viewer"
76691
+ },
76692
+ "groupStatus.getStatus": {
76693
+ capName: "group-status",
76694
+ capScope: "device",
76695
+ addonId: null,
76696
+ access: "view",
76697
+ role: "viewer"
76698
+ },
75775
76699
  "heatingValve.getStatus": {
75776
76700
  capName: "heating-valve",
75777
76701
  capScope: "device",
@@ -76724,6 +77648,13 @@ Object.freeze({
76724
77648
  access: "create",
76725
77649
  role: "admin"
76726
77650
  },
77651
+ "meter.getStatus": {
77652
+ capName: "meter",
77653
+ capScope: "device",
77654
+ addonId: null,
77655
+ access: "view",
77656
+ role: "viewer"
77657
+ },
76727
77658
  "metricsProvider.collectSnapshot": {
76728
77659
  capName: "metrics-provider",
76729
77660
  capScope: "system",
@@ -84822,7 +85753,39 @@ var APPLIANCE_CAP_VOCABULARY = {
84822
85753
  phrase: t("When {device} finishes", "Quando {device} finisce"),
84823
85754
  icon: "square"
84824
85755
  }
84825
- }
85756
+ },
85757
+ overrides: { byType: {
85758
+ [DeviceType.Washer]: {
85759
+ state: { running: {
85760
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
85761
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
85762
+ } },
85763
+ events: {
85764
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
85765
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
85766
+ }
85767
+ },
85768
+ [DeviceType.Dryer]: {
85769
+ state: { running: {
85770
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
85771
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
85772
+ } },
85773
+ events: {
85774
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
85775
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
85776
+ }
85777
+ },
85778
+ [DeviceType.Dishwasher]: {
85779
+ state: { running: {
85780
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
85781
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
85782
+ } },
85783
+ events: {
85784
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
85785
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
85786
+ }
85787
+ }
85788
+ } }
84826
85789
  },
84827
85790
  update: {
84828
85791
  scope: "device",
@@ -85594,6 +86557,41 @@ var SENSOR_CAP_VOCABULARY = {
85594
86557
  icon: "flame"
85595
86558
  }]
85596
86559
  },
86560
+ "group-active": {
86561
+ scope: "device",
86562
+ noun: t("Group state", "Stato del gruppo")
86563
+ },
86564
+ "group-commands": {
86565
+ scope: "device",
86566
+ noun: t("Group command", "Comando di gruppo")
86567
+ },
86568
+ "group-status": {
86569
+ scope: "device",
86570
+ noun: t("Group", "Gruppo"),
86571
+ state: [
86572
+ {
86573
+ kind: "numeric",
86574
+ field: "included",
86575
+ quantity: t("the members reporting", "i membri che rispondono"),
86576
+ label: t("Reporting", "Rispondono"),
86577
+ icon: "users"
86578
+ },
86579
+ {
86580
+ kind: "numeric",
86581
+ field: "excluded",
86582
+ quantity: t("the members left out", "i membri esclusi"),
86583
+ label: t("Excluded", "Esclusi"),
86584
+ icon: "users"
86585
+ },
86586
+ {
86587
+ kind: "numeric",
86588
+ field: "total",
86589
+ quantity: t("the member count", "il numero di membri"),
86590
+ label: t("Members", "Membri"),
86591
+ icon: "sigma"
86592
+ }
86593
+ ]
86594
+ },
85597
86595
  "humidity-sensor": {
85598
86596
  scope: "device",
85599
86597
  noun: t("Humidity", "Umidità"),
@@ -85619,6 +86617,17 @@ var SENSOR_CAP_VOCABULARY = {
85619
86617
  }],
85620
86618
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
85621
86619
  },
86620
+ meter: {
86621
+ scope: "device",
86622
+ noun: t("Meter", "Contatore"),
86623
+ state: [{
86624
+ kind: "numeric",
86625
+ field: "value",
86626
+ quantity: t("the total", "il totale"),
86627
+ label: t("Total", "Totale"),
86628
+ icon: "gauge"
86629
+ }]
86630
+ },
85622
86631
  "numeric-sensor": {
85623
86632
  scope: "device",
85624
86633
  noun: t("Sensor", "Sensore"),
@@ -85627,7 +86636,6 @@ var SENSOR_CAP_VOCABULARY = {
85627
86636
  field: "value",
85628
86637
  quantity: t("the reading", "il valore"),
85629
86638
  label: t("Reading", "Valore"),
85630
- unitField: "unit",
85631
86639
  icon: "gauge"
85632
86640
  }]
85633
86641
  },
@@ -85763,7 +86771,7 @@ var SENSOR_CAP_VOCABULARY = {
85763
86771
  field: "temperature",
85764
86772
  quantity: t("the outdoor temperature", "la temperatura esterna"),
85765
86773
  label: t("Temperature", "Temperatura"),
85766
- unitField: "temperatureUnit",
86774
+ unit: "°C",
85767
86775
  icon: "thermometer"
85768
86776
  },
85769
86777
  {
@@ -85779,7 +86787,7 @@ var SENSOR_CAP_VOCABULARY = {
85779
86787
  field: "windSpeed",
85780
86788
  quantity: t("the wind speed", "la velocità del vento"),
85781
86789
  label: t("Wind", "Vento"),
85782
- unitField: "windSpeedUnit",
86790
+ unit: "m/s",
85783
86791
  icon: "wind"
85784
86792
  },
85785
86793
  {
@@ -85787,7 +86795,7 @@ var SENSOR_CAP_VOCABULARY = {
85787
86795
  field: "pressure",
85788
86796
  quantity: t("the air pressure", "la pressione atmosferica"),
85789
86797
  label: t("Pressure", "Pressione"),
85790
- unitField: "pressureUnit",
86798
+ unit: "hPa",
85791
86799
  icon: "gauge"
85792
86800
  }
85793
86801
  ]
@@ -86092,8 +87100,47 @@ function projectAction(cap, action) {
86092
87100
  };
86093
87101
  }
86094
87102
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
86095
- 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;
87103
+ 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;
86096
87104
  DeviceFeature.BatteryOperated;
87105
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
87106
+ 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.";
87107
+ 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.";
87108
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
87109
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
87110
+ 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.";
87111
+ function withheld(reason) {
87112
+ return {
87113
+ [DeviceType.Washer]: {
87114
+ exported: false,
87115
+ reason
87116
+ },
87117
+ [DeviceType.Dryer]: {
87118
+ exported: false,
87119
+ reason
87120
+ },
87121
+ [DeviceType.Dishwasher]: {
87122
+ exported: false,
87123
+ reason
87124
+ }
87125
+ };
87126
+ }
87127
+ function offered(received) {
87128
+ return {
87129
+ [DeviceType.Washer]: {
87130
+ exported: true,
87131
+ as: received
87132
+ },
87133
+ [DeviceType.Dryer]: {
87134
+ exported: true,
87135
+ as: received
87136
+ },
87137
+ [DeviceType.Dishwasher]: {
87138
+ exported: true,
87139
+ as: received
87140
+ }
87141
+ };
87142
+ }
87143
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
86097
87144
  var LAST_FETCHED_FIELD = "lastFetchedAt";
86098
87145
  /**
86099
87146
  * How long a bridge stops re-attempting a refresh that did not land.
@@ -87212,6 +88259,21 @@ Object.freeze({
87212
88259
  form: "single",
87213
88260
  optional: false
87214
88261
  }],
88262
+ "groupActive.getStatus": [{
88263
+ name: "deviceId",
88264
+ form: "single",
88265
+ optional: false
88266
+ }],
88267
+ "groupCommands.getStatus": [{
88268
+ name: "deviceId",
88269
+ form: "single",
88270
+ optional: false
88271
+ }],
88272
+ "groupStatus.getStatus": [{
88273
+ name: "deviceId",
88274
+ form: "single",
88275
+ optional: false
88276
+ }],
87215
88277
  "heatingValve.getStatus": [{
87216
88278
  name: "deviceId",
87217
88279
  form: "single",
@@ -87412,6 +88474,11 @@ Object.freeze({
87412
88474
  form: "single",
87413
88475
  optional: false
87414
88476
  }],
88477
+ "meter.getStatus": [{
88478
+ name: "deviceId",
88479
+ form: "single",
88480
+ optional: false
88481
+ }],
87415
88482
  "motion.getStatus": [{
87416
88483
  name: "deviceId",
87417
88484
  form: "single",
@@ -89019,7 +90086,6 @@ Object.freeze({
89019
90086
  "network-access": "ingress",
89020
90087
  "smtp-provider": "email"
89021
90088
  });
89022
- DeviceType.Camera;
89023
90089
  /**
89024
90090
  * A saved Customize assignment (D936). The schema is the type: callers use
89025
90091
  * `z.infer`, they do not restate the shape.
@@ -89196,6 +90262,8 @@ new Map(Object.entries({
89196
90262
  "detection-blind": "device-events",
89197
90263
  "motion-noisy": "device-events",
89198
90264
  "motion-calm": "device-events",
90265
+ "package-llm-silent": "device-events",
90266
+ "package-llm-answered": "device-events",
89199
90267
  "recording-stalled": "device-events",
89200
90268
  "recording-healthy": "device-events",
89201
90269
  "recording-degraded": "device-events",
@@ -89279,6 +90347,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
89279
90347
  label: "Motion without a subject",
89280
90348
  kinds: ["motion-noisy", "motion-calm"]
89281
90349
  },
90350
+ {
90351
+ id: "package-llm",
90352
+ family: "device-events",
90353
+ label: "Package confirm",
90354
+ kinds: ["package-llm-silent", "package-llm-answered"]
90355
+ },
89282
90356
  {
89283
90357
  id: "recording",
89284
90358
  family: "device-events",
@@ -89460,6 +90534,8 @@ var DEVICE_EVENTS = [
89460
90534
  "detection-blind",
89461
90535
  "motion-noisy",
89462
90536
  "motion-calm",
90537
+ "package-llm-silent",
90538
+ "package-llm-answered",
89463
90539
  "recording-stalled",
89464
90540
  "recording-healthy",
89465
90541
  "recording-degraded",
@@ -89728,6 +90804,15 @@ new Map([
89728
90804
  snapshotLabel: "Cameras with constant motion now",
89729
90805
  snapshotSection: null
89730
90806
  },
90807
+ {
90808
+ id: "package-llm-confirm",
90809
+ family: "device-events",
90810
+ label: "Package confirm (not answering / answering again)",
90811
+ bad: ["package-llm-silent"],
90812
+ recovery: ["package-llm-answered"],
90813
+ snapshotLabel: "Cameras whose package confirm is not answering",
90814
+ snapshotSection: null
90815
+ },
89731
90816
  {
89732
90817
  id: "recording-health",
89733
90818
  family: "device-events",