@camstack/addon-provider-rtsp 1.2.146 → 1.2.148

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -23,7 +23,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  //#endregion
24
24
  let node_net = require("node:net");
25
25
  node_net = __toESM(node_net);
26
- //#region ../types/dist/event-category-DHXxB8t9.mjs
26
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
27
27
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
28
28
  EventCategory["SystemBoot"] = "system.boot";
29
29
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -349,6 +349,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
349
349
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
350
350
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
351
351
  /**
352
+ * A camera's package LLM confirm has failed closed three times in a row
353
+ * (D931, 2026-10-26). One edge per camera until
354
+ * {@link PipelinePackageLlmAnswered}.
355
+ */
356
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
357
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
358
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
359
+ /**
352
360
  * Per-camera pipeline config was mutated by the orchestrator
353
361
  * (a settings change via `setAgentInferenceDevices` /
354
362
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5458,7 +5466,7 @@ var ZodIssueCode = {
5458
5466
  var ZodFirstPartyTypeKind;
5459
5467
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5460
5468
  //#endregion
5461
- //#region ../types/dist/sleep-BErAxhul.mjs
5469
+ //#region ../types/dist/sleep--jXS9hWo.mjs
5462
5470
  /**
5463
5471
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5464
5472
  * window to float samples (D455).
@@ -8012,6 +8020,99 @@ var RecordKindDeclarationSchema = object({
8012
8020
  }).strict();
8013
8021
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8014
8022
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8023
+ var READING_QUANTITIES = [
8024
+ "temperature",
8025
+ "pressure",
8026
+ "speed",
8027
+ "precipitation",
8028
+ "precipitation-rate",
8029
+ "length",
8030
+ "area",
8031
+ "illuminance",
8032
+ "irradiance",
8033
+ "power",
8034
+ "energy",
8035
+ "voltage",
8036
+ "current",
8037
+ "apparent-power",
8038
+ "humidity",
8039
+ "percentage",
8040
+ "concentration-volume",
8041
+ "concentration-mass",
8042
+ "concentration-ppb",
8043
+ "volume",
8044
+ "duration",
8045
+ "duration-ms",
8046
+ "information",
8047
+ "sound-level",
8048
+ "signal-level",
8049
+ "colour-temperature",
8050
+ "angle",
8051
+ "bitrate",
8052
+ "rate",
8053
+ "mass",
8054
+ "index",
8055
+ "ratio",
8056
+ "count",
8057
+ "none",
8058
+ "device"
8059
+ ];
8060
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8061
+ new Set(READING_QUANTITIES);
8062
+ /**
8063
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8064
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8065
+ */
8066
+ var QUANTITY_BASE = {
8067
+ temperature: "°C",
8068
+ pressure: "hPa",
8069
+ speed: "m/s",
8070
+ precipitation: "mm",
8071
+ "precipitation-rate": "mm/h",
8072
+ length: "m",
8073
+ area: "m²",
8074
+ illuminance: "lx",
8075
+ irradiance: "W/m²",
8076
+ power: "W",
8077
+ energy: "kWh",
8078
+ voltage: "V",
8079
+ current: "A",
8080
+ "apparent-power": "VA",
8081
+ humidity: "%",
8082
+ percentage: "%",
8083
+ "concentration-volume": "ppm",
8084
+ "concentration-mass": "µg/m³",
8085
+ "concentration-ppb": "ppb",
8086
+ volume: "L",
8087
+ duration: "s",
8088
+ "duration-ms": "ms",
8089
+ information: "B",
8090
+ "sound-level": "dBFS",
8091
+ "signal-level": "dBm",
8092
+ "colour-temperature": "mired",
8093
+ angle: "°",
8094
+ bitrate: "kbps",
8095
+ rate: "fps",
8096
+ mass: "kg",
8097
+ index: "none",
8098
+ ratio: "none",
8099
+ count: "none",
8100
+ none: "none",
8101
+ device: "device"
8102
+ };
8103
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8104
+ function baseUnitOf(quantity) {
8105
+ const symbol = QUANTITY_BASE[quantity];
8106
+ if (symbol === "none" || symbol === "device") return void 0;
8107
+ return symbol;
8108
+ }
8109
+ function readingMeta(quantity, extras) {
8110
+ return {
8111
+ quantity,
8112
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8113
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8114
+ };
8115
+ }
8015
8116
  /** Shared Zod schemas used across streaming capabilities. */
8016
8117
  var CamProfileSchema = _enum([
8017
8118
  "high",
@@ -8039,8 +8140,8 @@ var CamStreamKindSchema = _enum([
8039
8140
  "derived"
8040
8141
  ]);
8041
8142
  var CamStreamResolutionSchema = object({
8042
- width: number().int().positive(),
8043
- height: number().int().positive()
8143
+ width: number().int().positive().meta(readingMeta("none")),
8144
+ height: number().int().positive().meta(readingMeta("none"))
8044
8145
  });
8045
8146
  var CameraStreamSchema = object({
8046
8147
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8558,6 +8659,9 @@ var CAP_NAMES = Object.freeze([
8558
8659
  "filesystem-browse",
8559
8660
  "flood",
8560
8661
  "gas",
8662
+ "group-active",
8663
+ "group-commands",
8664
+ "group-status",
8561
8665
  "heating-valve",
8562
8666
  "humidifier",
8563
8667
  "humidity-sensor",
@@ -8578,6 +8682,7 @@ var CAP_NAMES = Object.freeze([
8578
8682
  "login-method",
8579
8683
  "media-player",
8580
8684
  "mesh-network",
8685
+ "meter",
8581
8686
  "metrics-provider",
8582
8687
  "model-convert",
8583
8688
  "model-distributor",
@@ -8687,14 +8792,6 @@ Object.freeze({
8687
8792
  {
8688
8793
  "path": "aqi",
8689
8794
  "valueType": "number"
8690
- },
8691
- {
8692
- "path": "unit",
8693
- "valueType": "string"
8694
- },
8695
- {
8696
- "path": "precision",
8697
- "valueType": "number"
8698
8795
  }
8699
8796
  ],
8700
8797
  "alarm-panel": [
@@ -8730,20 +8827,10 @@ Object.freeze({
8730
8827
  "valueType": "boolean"
8731
8828
  }
8732
8829
  ],
8733
- "ambient-light-sensor": [
8734
- {
8735
- "path": "lux",
8736
- "valueType": "number"
8737
- },
8738
- {
8739
- "path": "unit",
8740
- "valueType": "string"
8741
- },
8742
- {
8743
- "path": "precision",
8744
- "valueType": "number"
8745
- }
8746
- ],
8830
+ "ambient-light-sensor": [{
8831
+ "path": "lux",
8832
+ "valueType": "number"
8833
+ }],
8747
8834
  "audio-metrics": [
8748
8835
  {
8749
8836
  "path": "ts",
@@ -8823,6 +8910,10 @@ Object.freeze({
8823
8910
  "path": "lastUpdated",
8824
8911
  "valueType": "number"
8825
8912
  },
8913
+ {
8914
+ "path": "volts",
8915
+ "valueType": "number"
8916
+ },
8826
8917
  {
8827
8918
  "path": "lastContactAt",
8828
8919
  "valueType": "number"
@@ -9065,10 +9156,6 @@ Object.freeze({
9065
9156
  "datetime"
9066
9157
  ]
9067
9158
  },
9068
- {
9069
- "path": "unit",
9070
- "valueType": "string"
9071
- },
9072
9159
  {
9073
9160
  "path": "min",
9074
9161
  "valueType": "number"
@@ -9080,19 +9167,6 @@ Object.freeze({
9080
9167
  {
9081
9168
  "path": "step",
9082
9169
  "valueType": "number"
9083
- },
9084
- {
9085
- "path": "precision",
9086
- "valueType": "number"
9087
- },
9088
- {
9089
- "path": "format",
9090
- "valueType": "enum",
9091
- "enumValues": [
9092
- "date",
9093
- "time",
9094
- "datetime"
9095
- ]
9096
9170
  }
9097
9171
  ],
9098
9172
  "cover": [
@@ -9333,13 +9407,8 @@ Object.freeze({
9333
9407
  "path": "value",
9334
9408
  "valueType": "string"
9335
9409
  }, {
9336
- "path": "format",
9337
- "valueType": "enum",
9338
- "enumValues": [
9339
- "date",
9340
- "time",
9341
- "datetime"
9342
- ]
9410
+ "path": "timestamp",
9411
+ "valueType": "boolean"
9343
9412
  }],
9344
9413
  "event-emitter": [
9345
9414
  {
@@ -9412,6 +9481,24 @@ Object.freeze({
9412
9481
  "path": "detected",
9413
9482
  "valueType": "boolean"
9414
9483
  }],
9484
+ "group-active": [{
9485
+ "path": "active",
9486
+ "valueType": "boolean"
9487
+ }],
9488
+ "group-status": [
9489
+ {
9490
+ "path": "included",
9491
+ "valueType": "number"
9492
+ },
9493
+ {
9494
+ "path": "excluded",
9495
+ "valueType": "number"
9496
+ },
9497
+ {
9498
+ "path": "total",
9499
+ "valueType": "number"
9500
+ }
9501
+ ],
9415
9502
  "heating-valve": [{
9416
9503
  "path": "position",
9417
9504
  "valueType": "number"
@@ -9446,20 +9533,10 @@ Object.freeze({
9446
9533
  "valueType": "number"
9447
9534
  }
9448
9535
  ],
9449
- "humidity-sensor": [
9450
- {
9451
- "path": "percent",
9452
- "valueType": "number"
9453
- },
9454
- {
9455
- "path": "unit",
9456
- "valueType": "string"
9457
- },
9458
- {
9459
- "path": "precision",
9460
- "valueType": "number"
9461
- }
9462
- ],
9536
+ "humidity-sensor": [{
9537
+ "path": "percent",
9538
+ "valueType": "number"
9539
+ }],
9463
9540
  "image": [{
9464
9541
  "path": "url",
9465
9542
  "valueType": "string"
@@ -9667,6 +9744,10 @@ Object.freeze({
9667
9744
  ]
9668
9745
  }
9669
9746
  ],
9747
+ "meter": [{
9748
+ "path": "value",
9749
+ "valueType": "number"
9750
+ }],
9670
9751
  "motion": [
9671
9752
  {
9672
9753
  "path": "detected",
@@ -9798,20 +9879,10 @@ Object.freeze({
9798
9879
  "valueType": "boolean"
9799
9880
  }
9800
9881
  ],
9801
- "numeric-sensor": [
9802
- {
9803
- "path": "value",
9804
- "valueType": "number"
9805
- },
9806
- {
9807
- "path": "unit",
9808
- "valueType": "string"
9809
- },
9810
- {
9811
- "path": "precision",
9812
- "valueType": "number"
9813
- }
9814
- ],
9882
+ "numeric-sensor": [{
9883
+ "path": "value",
9884
+ "valueType": "number"
9885
+ }],
9815
9886
  "pet-feeder": [
9816
9887
  {
9817
9888
  "path": "foodLevel",
@@ -9903,14 +9974,6 @@ Object.freeze({
9903
9974
  {
9904
9975
  "path": "powerFactor",
9905
9976
  "valueType": "number"
9906
- },
9907
- {
9908
- "path": "unit",
9909
- "valueType": "string"
9910
- },
9911
- {
9912
- "path": "precision",
9913
- "valueType": "number"
9914
9977
  }
9915
9978
  ],
9916
9979
  "presence": [
@@ -9939,20 +10002,10 @@ Object.freeze({
9939
10002
  "valueType": "number"
9940
10003
  }
9941
10004
  ],
9942
- "pressure-sensor": [
9943
- {
9944
- "path": "hpa",
9945
- "valueType": "number"
9946
- },
9947
- {
9948
- "path": "unit",
9949
- "valueType": "string"
9950
- },
9951
- {
9952
- "path": "precision",
9953
- "valueType": "number"
9954
- }
9955
- ],
10005
+ "pressure-sensor": [{
10006
+ "path": "hpa",
10007
+ "valueType": "number"
10008
+ }],
9956
10009
  "privacy-mask": [{
9957
10010
  "path": "enabled",
9958
10011
  "valueType": "boolean"
@@ -10263,20 +10316,10 @@ Object.freeze({
10263
10316
  "path": "tampered",
10264
10317
  "valueType": "boolean"
10265
10318
  }],
10266
- "temperature-sensor": [
10267
- {
10268
- "path": "celsius",
10269
- "valueType": "number"
10270
- },
10271
- {
10272
- "path": "unit",
10273
- "valueType": "string"
10274
- },
10275
- {
10276
- "path": "precision",
10277
- "valueType": "number"
10278
- }
10279
- ],
10319
+ "temperature-sensor": [{
10320
+ "path": "celsius",
10321
+ "valueType": "number"
10322
+ }],
10280
10323
  "update": [
10281
10324
  {
10282
10325
  "path": "currentVersion",
@@ -10387,10 +10430,6 @@ Object.freeze({
10387
10430
  "path": "temperature",
10388
10431
  "valueType": "number"
10389
10432
  },
10390
- {
10391
- "path": "temperatureUnit",
10392
- "valueType": "string"
10393
- },
10394
10433
  {
10395
10434
  "path": "humidity",
10396
10435
  "valueType": "number"
@@ -10399,18 +10438,10 @@ Object.freeze({
10399
10438
  "path": "pressure",
10400
10439
  "valueType": "number"
10401
10440
  },
10402
- {
10403
- "path": "pressureUnit",
10404
- "valueType": "string"
10405
- },
10406
10441
  {
10407
10442
  "path": "windSpeed",
10408
10443
  "valueType": "number"
10409
10444
  },
10410
- {
10411
- "path": "windSpeedUnit",
10412
- "valueType": "string"
10413
- },
10414
10445
  {
10415
10446
  "path": "windBearing",
10416
10447
  "valueType": "number"
@@ -10549,7 +10580,7 @@ function maskUrlCredentials(rawUrl) {
10549
10580
  }
10550
10581
  }
10551
10582
  //#endregion
10552
- //#region ../types/dist/err-msg-BXKzRBwg.mjs
10583
+ //#region ../types/dist/err-msg-DIorGYeC.mjs
10553
10584
  var DeviceType = /* @__PURE__ */ function(DeviceType) {
10554
10585
  DeviceType["Camera"] = "camera";
10555
10586
  DeviceType["Hub"] = "hub";
@@ -10643,8 +10674,14 @@ var DeviceType = /* @__PURE__ */ function(DeviceType) {
10643
10674
  DeviceType["PetFeeder"] = "pet-feeder";
10644
10675
  /** Built-in dishwasher — program lifecycle, door / remote-start gates,
10645
10676
  * salt and rinse-aid levels. Installed with the `dishwasher` cap.
10646
- * Sources: native Bosch Home Connect, reusable by similar integrations. */
10677
+ * Sources: native Bosch Home Connect, reusable by similar integrations.
10678
+ * A plug-built dishwasher carries running-state and power instead. */
10647
10679
  DeviceType["Dishwasher"] = "dishwasher";
10680
+ /** Washing machine. A plug-built one is running while its power stays
10681
+ * above a threshold. No program cap — that surface is the dishwasher's. */
10682
+ DeviceType["Washer"] = "washer";
10683
+ /** Tumble dryer. Same plug-built running surface as {@link Washer}. */
10684
+ DeviceType["Dryer"] = "dryer";
10648
10685
  return DeviceType;
10649
10686
  }({});
10650
10687
  var DeviceFeature = /* @__PURE__ */ function(DeviceFeature) {
@@ -10902,7 +10939,7 @@ function errMsg(err) {
10902
10939
  return String(err);
10903
10940
  }
10904
10941
  //#endregion
10905
- //#region ../types/dist/composition-kEl9mevu.mjs
10942
+ //#region ../types/dist/composition-Bb8iwHEJ.mjs
10906
10943
  /**
10907
10944
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10908
10945
  *
@@ -11217,6 +11254,13 @@ var table = {
11217
11254
  apply: () => {
11218
11255
  throw new ExpressionEvalError("latchedAt: stateful evaluation context is required");
11219
11256
  }
11257
+ },
11258
+ schmitt: {
11259
+ minArgs: 3,
11260
+ maxArgs: 5,
11261
+ apply: () => {
11262
+ throw new ExpressionEvalError("schmitt: stateful evaluation context is required");
11263
+ }
11220
11264
  }
11221
11265
  };
11222
11266
  Object.freeze(Object.assign(Object.create(null), table));
@@ -11657,15 +11701,232 @@ function validateExpressionSource(src) {
11657
11701
  }
11658
11702
  return null;
11659
11703
  }
11660
- /** A composed device's stableId is this prefix plus the block id. */
11661
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11662
- /** Longest snippet a `code` source may carry (slice 4). */
11663
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11704
+ /**
11705
+ * One power reading becomes a washer, dryer or dishwasher (D944 helper).
11706
+ *
11707
+ * Standby of the control board is under 5 W; a motor or a heater is far
11708
+ * above that, so 5 W is the line. A door lock or a short pump spike is not
11709
+ * a cycle, so running starts only after 30 s above the line.
11710
+ *
11711
+ * A wash, dry or dishwasher pause (soak, heat-wait, heater off between
11712
+ * bursts) lasts several minutes. Ending the cycle on the first dip would
11713
+ * report "finished" in the middle of the program, so the stop dwell is
11714
+ * 10 minutes on every type. The number is stored per type so one machine
11715
+ * can move later without moving the others.
11716
+ */
11717
+ var APPLIANCE_TEMPLATE_ID = "appliance";
11718
+ var APPLIANCE_KINDS = [
11719
+ "washer",
11720
+ "dryer",
11721
+ "dishwasher"
11722
+ ];
11723
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
11724
+ /**
11725
+ * Statistics and integrals over a history read (D944).
11726
+ *
11727
+ * Pure: the same points always give the same answer, so a restart recomputes
11728
+ * rather than trusting a buffer. A gauge mean is time-weighted (value × ms),
11729
+ * never the mean of the samples. A hole with no held value is not integrated
11730
+ * as zero. A truncated read is not published.
11731
+ */
11732
+ var STATISTIC_STATS = [
11733
+ "min",
11734
+ "max",
11735
+ "mean",
11736
+ "sum",
11737
+ "count",
11738
+ "change"
11739
+ ];
11740
+ /**
11741
+ * The range a statistics or integral helper asks history for (D944).
11742
+ *
11743
+ * Calendar edges are the hub's timezone, never UTC midnight: a day is 23, 24
11744
+ * or 25 hours across a DST change. A tier is eligible only when its retention
11745
+ * is strictly longer than the longest that range can be. Forever qualifies.
11746
+ * Equal to a tier's retention is a refusal, not a short answer.
11747
+ */
11748
+ var MINUTE_MS = 6e4;
11749
+ var HOUR_MS = 60 * MINUTE_MS;
11750
+ var DAY_MS = 24 * HOUR_MS;
11751
+ var ROLLING_SPANS = [
11752
+ "1h",
11753
+ "24h",
11754
+ "2d",
11755
+ "7d",
11756
+ "30d",
11757
+ "custom"
11758
+ ];
11759
+ var CALENDAR_PERIODS = [
11760
+ "today",
11761
+ "yesterday",
11762
+ "this-week",
11763
+ "this-month"
11764
+ ];
11765
+ var RESET_PERIODS = [
11766
+ "never",
11767
+ "daily",
11768
+ "weekly",
11769
+ "monthly"
11770
+ ];
11771
+ 5 * MINUTE_MS;
11772
+ 24 * HOUR_MS, 2 * DAY_MS, 7 * DAY_MS, 30 * DAY_MS;
11773
+ 7 * DAY_MS, 30 * DAY_MS, 365 * DAY_MS;
11774
+ var rollingWindowObject = object({
11775
+ kind: literal("rolling"),
11776
+ span: _enum(ROLLING_SPANS),
11777
+ customMinutes: number().int().min(5).optional()
11778
+ }).check((payload) => {
11779
+ if (payload.value.span === "custom" && payload.value.customMinutes === void 0) payload.issues.push({
11780
+ code: "custom",
11781
+ message: "a custom range needs a duration in minutes",
11782
+ path: ["customMinutes"],
11783
+ input: payload.value
11784
+ });
11785
+ if (payload.value.span !== "custom" && payload.value.customMinutes !== void 0) payload.issues.push({
11786
+ code: "custom",
11787
+ message: "only a custom range carries a duration",
11788
+ path: ["customMinutes"],
11789
+ input: payload.value
11790
+ });
11791
+ });
11792
+ var RollingWindowSchema = rollingWindowObject;
11793
+ var StatisticWindowSchema = discriminatedUnion("kind", [rollingWindowObject, object({
11794
+ kind: literal("calendar"),
11795
+ period: _enum(CALENDAR_PERIODS)
11796
+ })]);
11797
+ /**
11798
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11799
+ * id. Split out so helper schemas can name it without a cycle through
11800
+ * `composition.ts` (D659).
11801
+ */
11664
11802
  var CompositionSourceRefSchema = object({
11665
11803
  /** The owning addon; `stableId` is unique only within it. */
11666
11804
  addonId: string().min(1),
11667
11805
  stableId: string().min(1)
11668
11806
  });
11807
+ var GROUP_TEMPLATE_IDS = [
11808
+ "temperature",
11809
+ "humidity",
11810
+ "pressure",
11811
+ "illuminance",
11812
+ "power",
11813
+ "energy",
11814
+ "battery",
11815
+ "numeric",
11816
+ "motion",
11817
+ "contact",
11818
+ "leak",
11819
+ "smoke",
11820
+ "occupancy",
11821
+ "binary",
11822
+ "switch",
11823
+ "light",
11824
+ "fan",
11825
+ "cover",
11826
+ "valve",
11827
+ "lock",
11828
+ "media-player",
11829
+ "climate"
11830
+ ];
11831
+ var NUMERIC_AGGREGATES = [
11832
+ "mean",
11833
+ "median",
11834
+ "min",
11835
+ "max",
11836
+ "sum"
11837
+ ];
11838
+ var BINARY_AGGREGATES = ["any", "all"];
11839
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11840
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11841
+ var AGGREGATE_REDUCERS = [
11842
+ "mean",
11843
+ "median",
11844
+ "min",
11845
+ "max",
11846
+ "sum",
11847
+ "any",
11848
+ "all"
11849
+ ];
11850
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11851
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11852
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11853
+ /** One member. The field is stored so occupancy can name motion or binary. */
11854
+ var HelperMemberSchema = object({
11855
+ source: CompositionSourceRefSchema,
11856
+ cap: string().min(1),
11857
+ fieldPath: string().min(1)
11858
+ });
11859
+ var HelperSpecSchema = discriminatedUnion("kind", [
11860
+ object({
11861
+ kind: literal("group"),
11862
+ templateId: GroupTemplateIdSchema,
11863
+ aggregate: AggregateReducerSchema,
11864
+ members: array(HelperMemberSchema).min(1).max(64),
11865
+ /** Actuator groups only. Readings leave this unset. */
11866
+ active: ActiveCriterionSchema.optional()
11867
+ }),
11868
+ object({
11869
+ kind: literal("appliance"),
11870
+ templateId: literal(APPLIANCE_TEMPLATE_ID),
11871
+ appliance: _enum(APPLIANCE_KINDS),
11872
+ thresholdWatts: number().positive(),
11873
+ startDwellMs: number().int().nonnegative(),
11874
+ stopDwellMs: number().int().nonnegative(),
11875
+ member: HelperMemberSchema
11876
+ }),
11877
+ object({
11878
+ kind: literal("statistic"),
11879
+ stat: _enum(STATISTIC_STATS),
11880
+ window: StatisticWindowSchema,
11881
+ source: HelperMemberSchema
11882
+ }),
11883
+ object({
11884
+ kind: literal("integral"),
11885
+ reset: _enum(RESET_PERIODS),
11886
+ output: _enum(["energy", "meter"]),
11887
+ source: HelperMemberSchema
11888
+ }),
11889
+ object({
11890
+ kind: literal("threshold"),
11891
+ source: HelperMemberSchema,
11892
+ high: number(),
11893
+ low: number(),
11894
+ onForMs: number().int().nonnegative(),
11895
+ offForMs: number().int().nonnegative()
11896
+ }),
11897
+ object({
11898
+ kind: literal("arithmetic"),
11899
+ op: _enum([
11900
+ "sum",
11901
+ "difference",
11902
+ "ratio"
11903
+ ]),
11904
+ inputs: array(HelperMemberSchema).min(2).max(64)
11905
+ }),
11906
+ object({
11907
+ kind: literal("expression"),
11908
+ expr: string().min(1),
11909
+ inputs: array(object({
11910
+ name: string().min(1),
11911
+ member: HelperMemberSchema
11912
+ })).min(1).max(64),
11913
+ output: _enum(["number", "boolean"])
11914
+ }),
11915
+ object({
11916
+ kind: literal("derivative"),
11917
+ window: RollingWindowSchema,
11918
+ per: _enum([
11919
+ "second",
11920
+ "minute",
11921
+ "hour"
11922
+ ]),
11923
+ source: HelperMemberSchema
11924
+ })
11925
+ ]);
11926
+ /** A composed device's stableId is this prefix plus the block id. */
11927
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11928
+ /** Longest snippet a `code` source may carry (slice 4). */
11929
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11669
11930
  /** One field of one capability of one source device. */
11670
11931
  var CompositionFieldReadSchema = object({
11671
11932
  source: CompositionSourceRefSchema,
@@ -11709,7 +11970,59 @@ var CompositionCodeSourceSchema = object({
11709
11970
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11710
11971
  CompositionFromSourceSchema,
11711
11972
  CompositionExpressionSourceSchema,
11712
- CompositionCodeSourceSchema
11973
+ CompositionCodeSourceSchema,
11974
+ object({
11975
+ kind: literal("aggregate"),
11976
+ reducer: _enum([
11977
+ "mean",
11978
+ "median",
11979
+ "min",
11980
+ "max",
11981
+ "sum",
11982
+ "any",
11983
+ "all"
11984
+ ]),
11985
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11986
+ /**
11987
+ * Set when the reads are an enum state, not a yes/no: the listed states
11988
+ * count as active (a cover is `open`). A state outside the list is inactive;
11989
+ * a read that is not a state at all is left out. Only `any` / `all` use it.
11990
+ */
11991
+ activeWhen: array(string().min(1)).min(1).optional(),
11992
+ /** `value` is the reading; the others are the member counts. */
11993
+ publish: _enum([
11994
+ "value",
11995
+ "included",
11996
+ "excluded",
11997
+ "total"
11998
+ ]),
11999
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12000
+ empty: _enum([
12001
+ "null",
12002
+ "omit",
12003
+ "offline"
12004
+ ])
12005
+ }),
12006
+ object({
12007
+ kind: literal("series"),
12008
+ read: CompositionFieldReadSchema,
12009
+ /** `value` is the statistic or the integral. The others are the gap counts. */
12010
+ publish: _enum([
12011
+ "value",
12012
+ "included",
12013
+ "excluded",
12014
+ "total"
12015
+ ]),
12016
+ /**
12017
+ * What an unanswered `value` does. Counts ignore this. `offline` leaves the
12018
+ * device unseeded until history answers, so a restart cannot publish 0.
12019
+ */
12020
+ empty: _enum([
12021
+ "null",
12022
+ "omit",
12023
+ "offline"
12024
+ ])
12025
+ })
11713
12026
  ]);
11714
12027
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11715
12028
  kind: literal("forward"),
@@ -11755,7 +12068,13 @@ var CompositionSchema = object({
11755
12068
  kind: literal("existing"),
11756
12069
  device: CompositionSourceRefSchema
11757
12070
  })]),
11758
- features: array(CompositionFeatureSchema).min(1).max(16)
12071
+ features: array(CompositionFeatureSchema).min(1).max(16),
12072
+ /**
12073
+ * Present when the block is a helper template. Absent on a free
12074
+ * composition (the room example, device 4603). The editor reads this,
12075
+ * never the compiled features.
12076
+ */
12077
+ helper: HelperSpecSchema.optional()
11759
12078
  }).superRefine((composition, ctx) => {
11760
12079
  const seen = /* @__PURE__ */ new Set();
11761
12080
  composition.features.forEach((feature, index) => {
@@ -16894,7 +17213,22 @@ var BrokerProviderInfoSchema = object({
16894
17213
  */
16895
17214
  interactiveConnectPath: string().min(1).optional(),
16896
17215
  /** Relative hub path to disconnect the linked account (same popup pattern). */
16897
- interactiveDisconnectPath: string().min(1).optional()
17216
+ interactiveDisconnectPath: string().min(1).optional(),
17217
+ /**
17218
+ * The kind signs in through a server-held interactive session instead of
17219
+ * (or before) typed credentials. `basePath` is a hub-relative route set
17220
+ * served by the owning addon (`/addon/<id>/...`) speaking the sign-in
17221
+ * contract: `POST {basePath}/start` (body: the form's settings) answers
17222
+ * `{ sessionId, loginUrl }`; `GET {basePath}/status?sessionId=` answers
17223
+ * `{ phase: 'starting' | 'awaiting-login' | 'captured' | 'error' |
17224
+ * 'cancelled', loginUrl, error }`; `POST {basePath}/commit`
17225
+ * (`{ sessionId, name, brokerId? }`) stores the credentials on a new or
17226
+ * existing broker and answers `{ brokerId }`; `POST {basePath}/cancel`
17227
+ * (`{ sessionId }`) drops the session. The dialog that edits a broker
17228
+ * offers it as the default path; the fields the session fills are
17229
+ * marked `filledBySignIn` in the kind's settings schema.
17230
+ */
17231
+ signIn: object({ basePath: string().min(1) }).optional()
16898
17232
  }))
16899
17233
  });
16900
17234
  var ListInputSchema = object({
@@ -17995,7 +18329,12 @@ var CompositionProblemSchema = object({
17995
18329
  "item-field-reserved",
17996
18330
  "target-missing",
17997
18331
  "target-unreadable",
17998
- "not-replaceable"
18332
+ "not-replaceable",
18333
+ "native-capability-replace",
18334
+ "mixed-quantity",
18335
+ "helper-mismatch",
18336
+ "member-constraint",
18337
+ "retention"
17999
18338
  ]),
18000
18339
  path: string(),
18001
18340
  message: string()
@@ -18016,7 +18355,8 @@ var CompositionUnavailableModeSchema = _enum([
18016
18355
  "null",
18017
18356
  "offline",
18018
18357
  "unknown",
18019
- "release"
18358
+ "release",
18359
+ "omit"
18020
18360
  ]);
18021
18361
  /**
18022
18362
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18053,9 +18393,11 @@ var CompositionFieldPlanSchema = object({
18053
18393
  reads: array(CompositionFieldReadSchema)
18054
18394
  });
18055
18395
  /**
18056
- * What a feature does to its target (D663): `new` builds a capability on a new
18057
- * device; on an existing device it `add`s a capability the device lacks, or
18058
- * `replace`s named fields of one it has natively.
18396
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18397
+ * new device; on an existing device it `add`s a capability the device lacks.
18398
+ * Replacing a capability the device already serves is refused
18399
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18400
+ * wire for a claim the hub already holds.
18059
18401
  */
18060
18402
  var CompositionFeatureModeSchema = _enum([
18061
18403
  "new",
@@ -18226,8 +18568,392 @@ Object.freeze({
18226
18568
  latchedAt: {
18227
18569
  kind: "fixed",
18228
18570
  result: ["number", "null"]
18571
+ },
18572
+ schmitt: {
18573
+ kind: "fixed",
18574
+ result: ["boolean", "null"]
18229
18575
  }
18230
18576
  });
18577
+ /**
18578
+ * One catalog row per base reading a group helper can build (D944 §3).
18579
+ * Data, not a form default written into the block.
18580
+ */
18581
+ var NOW$2 = {
18582
+ fieldPath: "lastFetchedAt",
18583
+ expr: "now"
18584
+ };
18585
+ var CHANGED = {
18586
+ fieldPath: "lastChangedAt",
18587
+ expr: "now"
18588
+ };
18589
+ function numeric(input) {
18590
+ return {
18591
+ ...input,
18592
+ family: "numeric",
18593
+ deviceType: DeviceType.Sensor,
18594
+ memberDeviceType: DeviceType.Sensor,
18595
+ memberDeviceTypes: [DeviceType.Sensor],
18596
+ activeWhen: void 0,
18597
+ commandCaps: [],
18598
+ baseUnit: baseUnitOf(input.quantity),
18599
+ aggregates: NUMERIC_AGGREGATES,
18600
+ accept: [{
18601
+ cap: input.outputCap,
18602
+ fieldPath: input.outputField
18603
+ }]
18604
+ };
18605
+ }
18606
+ function binary(input) {
18607
+ return {
18608
+ ...input,
18609
+ family: "binary",
18610
+ deviceType: DeviceType.Sensor,
18611
+ memberDeviceType: DeviceType.Sensor,
18612
+ memberDeviceTypes: [DeviceType.Sensor],
18613
+ activeWhen: void 0,
18614
+ commandCaps: [],
18615
+ quantity: "none",
18616
+ baseUnit: void 0,
18617
+ aggregates: BINARY_AGGREGATES,
18618
+ empty: "offline"
18619
+ };
18620
+ }
18621
+ var GROUP_TEMPLATES = [
18622
+ numeric({
18623
+ id: "temperature",
18624
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18625
+ role: DeviceRole.TemperatureSensor,
18626
+ outputCap: "temperature-sensor",
18627
+ outputField: "celsius",
18628
+ quantity: "temperature",
18629
+ defaultAggregate: "mean",
18630
+ empty: "null",
18631
+ companions: [NOW$2]
18632
+ }),
18633
+ numeric({
18634
+ id: "humidity",
18635
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18636
+ role: DeviceRole.HumiditySensor,
18637
+ outputCap: "humidity-sensor",
18638
+ outputField: "percent",
18639
+ quantity: "humidity",
18640
+ defaultAggregate: "mean",
18641
+ empty: "null",
18642
+ companions: [NOW$2]
18643
+ }),
18644
+ numeric({
18645
+ id: "pressure",
18646
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18647
+ role: DeviceRole.PressureSensor,
18648
+ outputCap: "pressure-sensor",
18649
+ outputField: "hpa",
18650
+ quantity: "pressure",
18651
+ defaultAggregate: "mean",
18652
+ empty: "null",
18653
+ companions: [NOW$2]
18654
+ }),
18655
+ numeric({
18656
+ id: "illuminance",
18657
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18658
+ role: DeviceRole.AmbientLightSensor,
18659
+ outputCap: "ambient-light-sensor",
18660
+ outputField: "lux",
18661
+ quantity: "illuminance",
18662
+ defaultAggregate: "mean",
18663
+ empty: "null",
18664
+ companions: [NOW$2]
18665
+ }),
18666
+ numeric({
18667
+ id: "power",
18668
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18669
+ role: DeviceRole.PowerSensor,
18670
+ outputCap: "power-meter",
18671
+ outputField: "watts",
18672
+ quantity: "power",
18673
+ defaultAggregate: "sum",
18674
+ empty: "omit",
18675
+ companions: [NOW$2]
18676
+ }),
18677
+ numeric({
18678
+ id: "energy",
18679
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18680
+ role: DeviceRole.EnergySensor,
18681
+ outputCap: "power-meter",
18682
+ outputField: "kwhTotal",
18683
+ quantity: "energy",
18684
+ defaultAggregate: "sum",
18685
+ empty: "omit",
18686
+ companions: [NOW$2]
18687
+ }),
18688
+ numeric({
18689
+ id: "battery",
18690
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18691
+ role: DeviceRole.BatterySensor,
18692
+ outputCap: "battery",
18693
+ outputField: "percentage",
18694
+ quantity: "percentage",
18695
+ defaultAggregate: "mean",
18696
+ empty: "null",
18697
+ companions: [
18698
+ {
18699
+ fieldPath: "charging",
18700
+ expr: "\"unknown\""
18701
+ },
18702
+ {
18703
+ fieldPath: "sleeping",
18704
+ expr: "false"
18705
+ },
18706
+ {
18707
+ fieldPath: "lastUpdated",
18708
+ expr: "now"
18709
+ }
18710
+ ]
18711
+ }),
18712
+ numeric({
18713
+ id: "numeric",
18714
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18715
+ role: DeviceRole.NumericSensor,
18716
+ outputCap: "numeric-sensor",
18717
+ outputField: "value",
18718
+ quantity: "device",
18719
+ defaultAggregate: "mean",
18720
+ empty: "null",
18721
+ companions: [NOW$2]
18722
+ }),
18723
+ binary({
18724
+ id: "motion",
18725
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18726
+ role: DeviceRole.MotionSensor,
18727
+ outputCap: "motion",
18728
+ outputField: "detected",
18729
+ defaultAggregate: "any",
18730
+ accept: [{
18731
+ cap: "motion",
18732
+ fieldPath: "detected"
18733
+ }],
18734
+ companions: [{
18735
+ fieldPath: "lastDetectedAt",
18736
+ expr: "null"
18737
+ }, {
18738
+ fieldPath: "autoClearAfterMs",
18739
+ expr: "null"
18740
+ }]
18741
+ }),
18742
+ binary({
18743
+ id: "contact",
18744
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18745
+ role: DeviceRole.ContactSensor,
18746
+ outputCap: "contact",
18747
+ outputField: "entryOpen",
18748
+ defaultAggregate: "any",
18749
+ accept: [{
18750
+ cap: "contact",
18751
+ fieldPath: "entryOpen"
18752
+ }],
18753
+ companions: [CHANGED]
18754
+ }),
18755
+ binary({
18756
+ id: "leak",
18757
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18758
+ role: DeviceRole.LeakSensor,
18759
+ outputCap: "flood",
18760
+ outputField: "flooded",
18761
+ defaultAggregate: "any",
18762
+ accept: [{
18763
+ cap: "flood",
18764
+ fieldPath: "flooded"
18765
+ }],
18766
+ companions: [CHANGED]
18767
+ }),
18768
+ binary({
18769
+ id: "smoke",
18770
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18771
+ role: DeviceRole.SmokeSensor,
18772
+ outputCap: "smoke",
18773
+ outputField: "detected",
18774
+ defaultAggregate: "any",
18775
+ accept: [{
18776
+ cap: "smoke",
18777
+ fieldPath: "detected"
18778
+ }],
18779
+ companions: [CHANGED]
18780
+ }),
18781
+ binary({
18782
+ id: "occupancy",
18783
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18784
+ role: DeviceRole.BinarySensor,
18785
+ outputCap: "binary",
18786
+ outputField: "on",
18787
+ defaultAggregate: "any",
18788
+ accept: [{
18789
+ cap: "motion",
18790
+ fieldPath: "detected"
18791
+ }, {
18792
+ cap: "binary",
18793
+ fieldPath: "on"
18794
+ }],
18795
+ companions: [CHANGED]
18796
+ }),
18797
+ binary({
18798
+ id: "binary",
18799
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18800
+ role: DeviceRole.BinarySensor,
18801
+ outputCap: "binary",
18802
+ outputField: "on",
18803
+ defaultAggregate: "any",
18804
+ accept: [{
18805
+ cap: "binary",
18806
+ fieldPath: "on"
18807
+ }],
18808
+ companions: [CHANGED]
18809
+ }),
18810
+ actuator({
18811
+ id: "switch",
18812
+ 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.",
18813
+ deviceType: DeviceType.Switch,
18814
+ outputCap: "switch",
18815
+ outputField: "on",
18816
+ member: {
18817
+ cap: "switch",
18818
+ fieldPath: "on"
18819
+ },
18820
+ commandCaps: ["switch"]
18821
+ }),
18822
+ actuator({
18823
+ id: "light",
18824
+ 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.",
18825
+ deviceType: DeviceType.Light,
18826
+ outputCap: "switch",
18827
+ outputField: "on",
18828
+ member: {
18829
+ cap: "switch",
18830
+ fieldPath: "on"
18831
+ },
18832
+ commandCaps: [
18833
+ "switch",
18834
+ "brightness",
18835
+ "color"
18836
+ ]
18837
+ }),
18838
+ actuator({
18839
+ id: "fan",
18840
+ 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.",
18841
+ deviceType: DeviceType.Fan,
18842
+ outputCap: "switch",
18843
+ outputField: "on",
18844
+ member: {
18845
+ cap: "switch",
18846
+ fieldPath: "on"
18847
+ },
18848
+ commandCaps: ["switch", "fan-control"]
18849
+ }),
18850
+ actuator({
18851
+ id: "cover",
18852
+ 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.",
18853
+ deviceType: DeviceType.Cover,
18854
+ outputCap: "group-active",
18855
+ outputField: "active",
18856
+ member: {
18857
+ cap: "cover",
18858
+ fieldPath: "state"
18859
+ },
18860
+ activeWhen: [
18861
+ "open",
18862
+ "opening",
18863
+ "stopped"
18864
+ ],
18865
+ commandCaps: ["cover"]
18866
+ }),
18867
+ actuator({
18868
+ id: "valve",
18869
+ 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.",
18870
+ deviceType: DeviceType.Valve,
18871
+ outputCap: "group-active",
18872
+ outputField: "active",
18873
+ member: {
18874
+ cap: "valve",
18875
+ fieldPath: "state"
18876
+ },
18877
+ activeWhen: [
18878
+ "open",
18879
+ "opening",
18880
+ "stopped"
18881
+ ],
18882
+ commandCaps: ["valve"]
18883
+ }),
18884
+ actuator({
18885
+ id: "lock",
18886
+ 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.",
18887
+ deviceType: DeviceType.Lock,
18888
+ outputCap: "group-active",
18889
+ outputField: "active",
18890
+ member: {
18891
+ cap: "lock-control",
18892
+ fieldPath: "state"
18893
+ },
18894
+ activeWhen: ["locked"],
18895
+ commandCaps: ["lock-control"]
18896
+ }),
18897
+ actuator({
18898
+ id: "media-player",
18899
+ 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.",
18900
+ deviceType: DeviceType.MediaPlayer,
18901
+ outputCap: "group-active",
18902
+ outputField: "active",
18903
+ member: {
18904
+ cap: "media-player",
18905
+ fieldPath: "state"
18906
+ },
18907
+ activeWhen: ["playing", "buffering"],
18908
+ commandCaps: ["media-player"]
18909
+ }),
18910
+ actuator({
18911
+ id: "climate",
18912
+ 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.",
18913
+ deviceType: DeviceType.Thermostat,
18914
+ memberDeviceTypes: [DeviceType.Thermostat, DeviceType.Climate],
18915
+ outputCap: "group-active",
18916
+ outputField: "active",
18917
+ member: {
18918
+ cap: "climate-control",
18919
+ fieldPath: "mode"
18920
+ },
18921
+ activeWhen: [
18922
+ "heat",
18923
+ "cool",
18924
+ "auto",
18925
+ "heat_cool",
18926
+ "fan_only",
18927
+ "dry"
18928
+ ],
18929
+ commandCaps: ["climate-control"]
18930
+ })
18931
+ ];
18932
+ function actuator(input) {
18933
+ return {
18934
+ id: input.id,
18935
+ summary: input.summary,
18936
+ deviceType: input.deviceType,
18937
+ outputCap: input.outputCap,
18938
+ outputField: input.outputField,
18939
+ commandCaps: input.commandCaps,
18940
+ family: "actuator",
18941
+ role: void 0,
18942
+ memberDeviceType: input.deviceType,
18943
+ memberDeviceTypes: input.memberDeviceTypes ?? [input.deviceType],
18944
+ activeWhen: input.activeWhen,
18945
+ quantity: "none",
18946
+ baseUnit: void 0,
18947
+ aggregates: BINARY_AGGREGATES,
18948
+ defaultAggregate: "any",
18949
+ empty: "offline",
18950
+ accept: [input.member],
18951
+ companions: [CHANGED]
18952
+ };
18953
+ }
18954
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18955
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18956
+ DeviceType.Camera;
18231
18957
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18232
18958
  /**
18233
18959
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19941,12 +20667,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19941
20667
  * providers detect renames by matching `uniqueId` and patch `id`
19942
20668
  * in-place so dispatch keeps working.
19943
20669
  *
19944
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19945
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19946
- * state push and are the single source of truth. The `device_class` tag is
19947
- * an HA-specific input that is consumed at the discovery boundary
19948
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19949
- * stored after that point.
20670
+ * **Display unit and precision are NOT stored here.** They are a read-time
20671
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20672
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20673
+ * neither is repeated on each sample. The `device_class` tag is an
20674
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20675
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19950
20676
  *
19951
20677
  * The `raw` escape hatch carries provider-specific attributes that don't
19952
20678
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19960,6 +20686,14 @@ var SourceInfoSchema = object({
19960
20686
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19961
20687
  * Used to detect rename when `id` changes. */
19962
20688
  uniqueId: string().optional(),
20689
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20690
+ quantity: ReadingQuantitySchema.optional(),
20691
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20692
+ dateTimeShape: _enum([
20693
+ "date",
20694
+ "time",
20695
+ "datetime"
20696
+ ]).optional(),
19963
20697
  /** Provider-specific extras that don't fit the structured slots.
19964
20698
  * Whitelist what goes here per provider — do NOT dump entire HA
19965
20699
  * attribute blobs (would explode the meta JSON column). */
@@ -41064,7 +41798,8 @@ method(object({
41064
41798
  "enum"
41065
41799
  ]),
41066
41800
  enumValues: array(string()).optional(),
41067
- item: boolean().optional()
41801
+ item: boolean().optional(),
41802
+ quantity: ReadingQuantitySchema.optional()
41068
41803
  })).readonly(),
41069
41804
  itemArray: object({
41070
41805
  path: string(),
@@ -43425,7 +44160,7 @@ var NetworkAccessStatusSchema = object({
43425
44160
  });
43426
44161
  /**
43427
44162
  * Optional, richer endpoint shape returned by providers that expose
43428
- * MORE than one ingress concurrently (Tailscale Ingress with mixed
44163
+ * MORE than one ingress concurrently (Tailscale Funnel with mixed
43429
44164
  * serve+funnel rules, future ngrok multi-tunnel, …). Each entry carries
43430
44165
  * the originating provider config (mode + sourcePort) so the
43431
44166
  * orchestrator UI can label rows distinctly. Providers that expose only
@@ -44908,8 +45643,8 @@ var MaskPolygonShapeSchema = object({
44908
45643
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44909
45644
  var MaskGridShapeSchema = object({
44910
45645
  kind: literal("grid"),
44911
- gridWidth: number(),
44912
- gridHeight: number(),
45646
+ gridWidth: number().meta(readingMeta("none")),
45647
+ gridHeight: number().meta(readingMeta("none")),
44913
45648
  cells: array(boolean())
44914
45649
  });
44915
45650
  discriminatedUnion("kind", [
@@ -45021,6 +45756,8 @@ var NcSystemEventKindSchema = _enum([
45021
45756
  "detection-blind",
45022
45757
  "motion-noisy",
45023
45758
  "motion-calm",
45759
+ "package-llm-silent",
45760
+ "package-llm-answered",
45024
45761
  "recording-stalled",
45025
45762
  "recording-healthy",
45026
45763
  "recording-degraded",
@@ -46110,7 +46847,20 @@ var NcRuleInputSchema = object({
46110
46847
  * save by the hub's grouping guard). `.optional()`, never `.default()`: the addon cap
46111
46848
  * path does not run Zod defaults.
46112
46849
  */
46113
- groupingSec: number().int().min(5).max(60).optional()
46850
+ groupingSec: number().int().min(5).max(60).optional(),
46851
+ /**
46852
+ * ASK ME TO CONFIRM DETECTIONS (#243): a DETECTION notification of this rule
46853
+ * carries "Correct" / "Wrong" buttons (and, where the channel has room, a
46854
+ * "Wrong, it is a <class>" choice). A tap marks the track for retraining, the
46855
+ * same mark the Library writes, and records whose notification it was and
46856
+ * when. A channel with no buttons gets the same two choices as links.
46857
+ *
46858
+ * `.optional()`, never `.default()` (a Zod default does not run on the addon
46859
+ * cap path): absent and `false` both mean off, which is what every rule
46860
+ * authored before this field meant. An editor turns it off by writing
46861
+ * `false`, so it needs no `clear` key.
46862
+ */
46863
+ askVerdict: boolean().optional()
46114
46864
  });
46115
46865
  /**
46116
46866
  * Partial patch for `updateRule` — any subset of the input fields, plus the
@@ -56216,6 +56966,28 @@ var PackageVersionInfoSchema = object({
56216
56966
  deprecated: string().optional(),
56217
56967
  distTags: array(string()).readonly()
56218
56968
  });
56969
+ /**
56970
+ * One published document. `ok` is markdown from the registry. `missing` means
56971
+ * the registry answered and the file was not there. `error` is a registry or
56972
+ * tarball failure — never an empty string standing in for either.
56973
+ */
56974
+ var PackageDocBodySchema = discriminatedUnion("status", [
56975
+ object({
56976
+ status: literal("ok"),
56977
+ markdown: string()
56978
+ }),
56979
+ object({ status: literal("missing") }),
56980
+ object({
56981
+ status: literal("error"),
56982
+ message: string()
56983
+ })
56984
+ ]);
56985
+ var PackageDocsSchema = object({
56986
+ name: string(),
56987
+ version: string(),
56988
+ readme: PackageDocBodySchema,
56989
+ changelog: PackageDocBodySchema
56990
+ });
56219
56991
  var SearchResultSchema = object({
56220
56992
  name: string(),
56221
56993
  version: string(),
@@ -56434,7 +57206,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56434
57206
  }), object({ success: literal(true) }), {
56435
57207
  kind: "mutation",
56436
57208
  auth: "admin"
56437
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57209
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57210
+ name: string().min(1),
57211
+ version: string().min(1).optional()
57212
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56438
57213
  kind: "mutation",
56439
57214
  auth: "admin"
56440
57215
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56493,27 +57268,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56493
57268
  */
56494
57269
  var AirQualitySensorStatusSchema = object({
56495
57270
  /** Carbon dioxide concentration in ppm. */
56496
- co2Ppm: number().min(0).optional(),
57271
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56497
57272
  /** Total volatile organic compounds in ppb. */
56498
- vocPpb: number().min(0).optional(),
57273
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56499
57274
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56500
- pm25: number().min(0).optional(),
57275
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56501
57276
  /** Particulate matter ≤ 10 μm in µg/m³. */
56502
- pm10: number().min(0).optional(),
57277
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56503
57278
  /** Composite AQI value (typically 0..500). */
56504
- aqi: number().optional(),
57279
+ aqi: number().optional().meta(readingMeta("index")),
56505
57280
  /** Ms epoch when the slice was last updated. */
56506
- lastFetchedAt: number(),
56507
- /** Live display unit of the single metric this slice carries (e.g. HA
56508
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56509
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56510
- * per slice is unambiguous. */
56511
- unit: string().optional(),
56512
- /** Suggested decimal places for numeric display.
56513
- * Populated live from the upstream source when provided (e.g. HA
56514
- * `attributes.suggested_display_precision`). Falls back to
56515
- * auto-formatting when absent. */
56516
- precision: number().int().min(0).max(10).optional()
57281
+ lastFetchedAt: number()
56517
57282
  });
56518
57283
  var airQualitySensorCapability = {
56519
57284
  name: "air-quality-sensor",
@@ -56550,12 +57315,12 @@ var airQualitySensorCapability = {
56550
57315
  },
56551
57316
  pm25: {
56552
57317
  kind: "gauge",
56553
- unit: "ug/m3",
57318
+ unit: "µg/m³",
56554
57319
  deadband: 1
56555
57320
  },
56556
57321
  pm10: {
56557
57322
  kind: "gauge",
56558
- unit: "ug/m3",
57323
+ unit: "µg/m³",
56559
57324
  deadband: 1
56560
57325
  },
56561
57326
  aqi: {
@@ -56573,22 +57338,13 @@ var airQualitySensorCapability = {
56573
57338
  * entries with `device_class: illuminance`.
56574
57339
  */
56575
57340
  var AmbientLightSensorStatusSchema = object({
56576
- /** Current illuminance in lux (lx). */
56577
- lux: number().min(0).meta({
56578
- unit: "lx",
57341
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57342
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57343
+ precision: 0,
56579
57344
  label: "Illuminance"
56580
- }),
57345
+ })),
56581
57346
  /** Ms epoch when the slice was last updated. */
56582
- lastFetchedAt: number(),
56583
- /** Live display unit from the upstream source (e.g. HA
56584
- * `attributes.unit_of_measurement`). The UI prefers this over the
56585
- * role's canonical unit. Absent → fall back to the canonical unit. */
56586
- unit: string().optional(),
56587
- /** Suggested decimal places for numeric display.
56588
- * Populated live from the upstream source when provided (e.g. HA
56589
- * `attributes.suggested_display_precision`). Falls back to
56590
- * auto-formatting when absent. */
56591
- precision: number().int().min(0).max(10).optional()
57347
+ lastFetchedAt: number()
56592
57348
  });
56593
57349
  var ambientLightSensorCapability = {
56594
57350
  name: "ambient-light-sensor",
@@ -56620,12 +57376,7 @@ var ambientLightSensorCapability = {
56620
57376
  } } },
56621
57377
  /** Clock fields: written, but excluded from the compare that decides
56622
57378
  * whether persisting is worth a SQLite commit. */
56623
- volatileStateFields: ["lastFetchedAt"],
56624
- customize: {
56625
- version: 1,
56626
- advanced: "derive",
56627
- targets: []
56628
- }
57379
+ volatileStateFields: ["lastFetchedAt"]
56629
57380
  };
56630
57381
  /**
56631
57382
  * Per-class audio metrics aggregated over a sliding window.
@@ -56852,10 +57603,7 @@ var BatteryStatusSchema = object({
56852
57603
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56853
57604
  * declines to band a non-finite reading, which is the correct shape.
56854
57605
  */
56855
- percentage: number().min(0).max(100).nullable().meta({
56856
- unit: "%",
56857
- label: "Charge"
56858
- }),
57606
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56859
57607
  /**
56860
57608
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56861
57609
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56882,7 +57630,14 @@ var BatteryStatusSchema = object({
56882
57630
  */
56883
57631
  sleeping: boolean().meta({ label: "Sleeping" }),
56884
57632
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56885
- lastUpdated: number(),
57633
+ lastUpdated: number().meta(readingMeta("none")),
57634
+ /**
57635
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57636
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57637
+ * that only report charge. A second reading on this cap, not a second
57638
+ * `numeric-sensor` on the same device.
57639
+ */
57640
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56886
57641
  /**
56887
57642
  * Ms epoch of the last time the device PROVED it was reachable — a
56888
57643
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56929,12 +57684,12 @@ var BATTERY_DEVICE_TYPES = [
56929
57684
  DeviceType.Switch,
56930
57685
  DeviceType.PetFeeder,
56931
57686
  DeviceType.Container,
56932
- DeviceType.Thermostat
57687
+ DeviceType.Thermostat,
57688
+ DeviceType.EventEmitter
56933
57689
  ];
56934
57690
  /** Generic Customize target for the percentage reading (D936). */
56935
57691
  var batteryCustomize = {
56936
57692
  version: 1,
56937
- advanced: "derive",
56938
57693
  targets: [{
56939
57694
  id: "percentage",
56940
57695
  cardinality: "scalar",
@@ -56943,7 +57698,7 @@ var batteryCustomize = {
56943
57698
  stateField: "percentage"
56944
57699
  },
56945
57700
  deviceTypes: BATTERY_DEVICE_TYPES,
56946
- apply: "add-or-replace",
57701
+ apply: "add",
56947
57702
  path: "percentage",
56948
57703
  sources: [{
56949
57704
  kind: "field",
@@ -57064,10 +57819,16 @@ onStatusChanged: { data: object({
57064
57819
  /**
57065
57820
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57066
57821
  */
57067
- history: { fields: { percentage: {
57068
- kind: "gauge",
57069
- unit: "%"
57070
- } } },
57822
+ history: { fields: {
57823
+ percentage: {
57824
+ kind: "gauge",
57825
+ unit: "%"
57826
+ },
57827
+ volts: {
57828
+ kind: "gauge",
57829
+ unit: "V"
57830
+ }
57831
+ } },
57071
57832
  customize: batteryCustomize,
57072
57833
  /** Clock fields: written, but excluded from the compare that decides
57073
57834
  * whether persisting is worth a SQLite commit. */
@@ -57114,12 +57875,7 @@ var binaryCapability = {
57114
57875
  history: { fields: { on: { kind: "boolean" } } },
57115
57876
  /** Clock fields: written, but excluded from the compare that decides
57116
57877
  * whether persisting is worth a SQLite commit. */
57117
- volatileStateFields: ["lastChangedAt"],
57118
- customize: {
57119
- version: 1,
57120
- advanced: "derive",
57121
- targets: []
57122
- }
57878
+ volatileStateFields: ["lastChangedAt"]
57123
57879
  };
57124
57880
  /**
57125
57881
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57135,7 +57891,7 @@ var binaryCapability = {
57135
57891
  */
57136
57892
  var BrightnessStatusSchema = object({
57137
57893
  /** Current level as 0..100 inclusive. Firmware-reported. */
57138
- percentage: number().min(0).max(100),
57894
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57139
57895
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57140
57896
  lastChangedAt: number()
57141
57897
  });
@@ -57589,21 +58345,21 @@ var ClimateControlStatusSchema = object({
57589
58345
  /** Single setpoint in Celsius. Used by single-target modes
57590
58346
  * (heat / cool / dry). Null when the device is in a range mode
57591
58347
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57592
- target: number().nullable(),
58348
+ target: number().nullable().meta(readingMeta("temperature")),
57593
58349
  /** Upper bound of the dual setpoint in Celsius. Populated only
57594
58350
  * in `heat_cool` mode. */
57595
- targetHigh: number().nullable(),
58351
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57596
58352
  /** Lower bound of the dual setpoint in Celsius. Populated only
57597
58353
  * in `heat_cool` mode. */
57598
- targetLow: number().nullable(),
58354
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57599
58355
  /** Target relative humidity (0..100). Null when the device has no
57600
58356
  * humidity control. */
57601
- targetHumidity: number().min(0).max(100).nullable(),
58357
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57602
58358
  /** Current measured temperature in Celsius. Read-only — pushed by
57603
58359
  * the device's internal sensor. */
57604
- currentTemp: number().nullable(),
58360
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57605
58361
  /** Current measured relative humidity (0..100). Read-only. */
57606
- currentHumidity: number().min(0).max(100).nullable(),
58362
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57607
58363
  /** Vertical louver swing on/off. Null when the device has no vertical
57608
58364
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57609
58365
  swingVertical: boolean().nullable(),
@@ -57611,11 +58367,11 @@ var ClimateControlStatusSchema = object({
57611
58367
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57612
58368
  swingHorizontal: boolean().nullable(),
57613
58369
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57614
- minTemp: number().nullable().optional(),
58370
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57615
58371
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57616
- maxTemp: number().nullable().optional(),
58372
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57617
58373
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57618
- tempStep: number().positive().nullable().optional(),
58374
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57619
58375
  /** Ms epoch when the slice was last updated (push or command). */
57620
58376
  lastFetchedAt: number()
57621
58377
  });
@@ -57757,17 +58513,17 @@ var climateControlCapability = {
57757
58513
  * modes to expose in the picker without a slice fetch.
57758
58514
  */
57759
58515
  var RgbTripletSchema = object({
57760
- r: number().int().min(0).max(255),
57761
- g: number().int().min(0).max(255),
57762
- b: number().int().min(0).max(255)
58516
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58517
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58518
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57763
58519
  });
57764
58520
  var HsvTripletSchema = object({
57765
58521
  /** Hue in degrees, 0..360. */
57766
- h: number().min(0).max(360),
58522
+ h: number().min(0).max(360).meta(readingMeta("none")),
57767
58523
  /** Saturation as 0..100 inclusive. */
57768
- s: number().min(0).max(100),
58524
+ s: number().min(0).max(100).meta(readingMeta("none")),
57769
58525
  /** Value/brightness as 0..100 inclusive. */
57770
- v: number().min(0).max(100)
58526
+ v: number().min(0).max(100).meta(readingMeta("none"))
57771
58527
  });
57772
58528
  var ColorInputSchema = discriminatedUnion("mode", [
57773
58529
  object({
@@ -57796,7 +58552,7 @@ var ColorStatusSchema = object({
57796
58552
  /** Populated when `mode === 'hsv'`. */
57797
58553
  hsv: HsvTripletSchema.optional(),
57798
58554
  /** Populated when `mode === 'mired'`. */
57799
- mireds: number().int().optional(),
58555
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57800
58556
  /** Ms epoch of the last operator-driven change. */
57801
58557
  lastChangedAt: number()
57802
58558
  });
@@ -57999,18 +58755,12 @@ _enum([
57999
58755
  "empty",
58000
58756
  "full"
58001
58757
  ]);
58002
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58003
- * each. */
58004
- var ConsumableRemainingUnitSchema = _enum([
58005
- "days",
58006
- "hours",
58007
- "cycles"
58008
- ]);
58009
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58010
- * days). `value` null = unknown (D393). */
58758
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58759
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58760
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58011
58761
  var ConsumableRemainingSchema = object({
58012
- value: number().min(0).nullable(),
58013
- unit: ConsumableRemainingUnitSchema
58762
+ value: number().min(0).nullable().meta(readingMeta("device")),
58763
+ kind: ConsumableRemainingKindSchema
58014
58764
  });
58015
58765
  /** A single consumable item. Either a continuous `level` (remaining
58016
58766
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58023,7 +58773,7 @@ var ConsumableItemSchema = object({
58023
58773
  /** Display name. */
58024
58774
  label: string().min(1),
58025
58775
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58026
- level: number().min(0).max(100).nullable(),
58776
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58027
58777
  /** The provider's discrete verdict when known (binary mode). */
58028
58778
  status: ConsumableStatusSchema.nullable(),
58029
58779
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58105,8 +58855,8 @@ var consumablesCustomize = {
58105
58855
  transforms: [{ kind: "identity" }]
58106
58856
  }],
58107
58857
  literals: [{
58108
- path: "remaining.unit",
58109
- values: [...ConsumableRemainingUnitSchema.options]
58858
+ path: "remaining.kind",
58859
+ values: [...ConsumableRemainingKindSchema.options]
58110
58860
  }]
58111
58861
  },
58112
58862
  {
@@ -58235,7 +58985,6 @@ reset: method(object({
58235
58985
  },
58236
58986
  "remaining.value": {
58237
58987
  kind: "gauge",
58238
- unitFrom: "remaining.unit",
58239
58988
  deadband: 1,
58240
58989
  displayName: "Remaining"
58241
58990
  }
@@ -58305,10 +59054,9 @@ var contactCapability = {
58305
59054
  * (empty array for the other kinds). It lives in the slice because
58306
59055
  * HA `select.*` entities may update their options at runtime.
58307
59056
  *
58308
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58309
- * `kind === 'numeric'` and are absent (undefined) for all other
58310
- * kinds. They are populated live from HA attributes on each state
58311
- * push — consistent with `options` and the sensor-unit approach.
59057
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59058
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59059
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58312
59060
  */
58313
59061
  var ControlStatusSchema = object({
58314
59062
  kind: _enum([
@@ -58320,39 +59068,17 @@ var ControlStatusSchema = object({
58320
59068
  /** Current value. Numeric kind → number; select/text/datetime → string.
58321
59069
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58322
59070
  * or time-only — HA accepts all three forms). */
58323
- value: union([number(), string()]),
59071
+ value: union([number().meta(readingMeta("device")), string()]),
58324
59072
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58325
59073
  options: array(string()),
58326
59074
  /** Ms epoch when the slice was last updated. */
58327
59075
  lastChangedAt: number(),
58328
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58329
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58330
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58331
- unit: string().optional(),
58332
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58333
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58334
- min: number().optional(),
58335
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58336
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58337
- max: number().optional(),
58338
- /** Step increment. Populated live from HA `attributes.step`.
58339
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58340
- step: number().optional(),
58341
- /** Suggested decimal places for numeric display. Populated live from HA
58342
- * `attributes.suggested_display_precision`, or derived from `step` when the
58343
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58344
- * other kinds. Falls back to auto-formatting when absent. */
58345
- precision: number().int().min(0).max(10).optional(),
58346
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58347
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58348
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58349
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58350
- * `has_date`/`has_time` attributes on each push. */
58351
- format: _enum([
58352
- "date",
58353
- "time",
58354
- "datetime"
58355
- ]).optional()
59076
+ /** Minimum allowed value, in the device quantity's base unit. */
59077
+ min: number().optional().meta(readingMeta("device")),
59078
+ /** Maximum allowed value, in the device quantity's base unit. */
59079
+ max: number().optional().meta(readingMeta("device")),
59080
+ /** Step increment, in the device quantity's base unit. */
59081
+ step: number().optional().meta(readingMeta("device"))
58356
59082
  });
58357
59083
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58358
59084
  object({
@@ -58418,9 +59144,9 @@ var CoverStatusSchema = object({
58418
59144
  ]),
58419
59145
  /** 0 = fully closed, 100 = fully open. Null when the device has
58420
59146
  * no intermediate position surface. */
58421
- position: number().min(0).max(100).nullable(),
59147
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58422
59148
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58423
- tiltPosition: number().min(0).max(100).nullable(),
59149
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58424
59150
  /** Ms epoch when the slice was last updated. */
58425
59151
  lastChangedAt: number()
58426
59152
  });
@@ -58523,9 +59249,9 @@ var NormalizedRangeSchema$1 = object({
58523
59249
  var DayNightStatusSchema = object({
58524
59250
  mode: DayNightModeSchema,
58525
59251
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58526
- sensitivity: number().optional(),
59252
+ sensitivity: number().optional().meta(readingMeta("none")),
58527
59253
  /** Delay before the IR-cut filter flips, in seconds. */
58528
- switchDelaySec: number().optional(),
59254
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58529
59255
  lastFetchedAt: number()
58530
59256
  });
58531
59257
  /**
@@ -58743,11 +59469,11 @@ var DishwasherStatusSchema = object({
58743
59469
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58744
59470
  availablePrograms: array(string()),
58745
59471
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58746
- remainingSeconds: number().int().nonnegative().nullable(),
59472
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58747
59473
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58748
- delayedStartSeconds: number().int().nonnegative().nullable(),
59474
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58749
59475
  /** Cycle completion 0..100, or null when not applicable. */
58750
- progressPercent: number().min(0).max(100).nullable(),
59476
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58751
59477
  /** True when the door is reported open; null when the door state is not known. */
58752
59478
  doorOpen: boolean().nullable(),
58753
59479
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58771,7 +59497,7 @@ var DishwasherStatusSchema = object({
58771
59497
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58772
59498
  machineCareDue: boolean().nullable(),
58773
59499
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58774
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59500
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58775
59501
  /** Option states: null when the appliance does not expose the option or has not said. */
58776
59502
  halfLoad: boolean().nullable(),
58777
59503
  hygienePlus: boolean().nullable(),
@@ -58786,7 +59512,7 @@ var DishwasherStatusSchema = object({
58786
59512
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58787
59513
  activeEvents: array(string()),
58788
59514
  /** Numeric error code when the appliance reports one; null otherwise. */
58789
- errorCode: number().nullable(),
59515
+ errorCode: number().nullable().meta(readingMeta("none")),
58790
59516
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58791
59517
  errorLabel: string().nullable(),
58792
59518
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59017,7 +59743,7 @@ var DoorbellStatusSchema = object({
59017
59743
  /** Ms epoch of the last press. null = never observed since this provider started. */
59018
59744
  lastPressedAt: number().nullable(),
59019
59745
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59020
- pressCountSinceStart: number()
59746
+ pressCountSinceStart: number().meta(readingMeta("count"))
59021
59747
  });
59022
59748
  var DoorbellPressEventSchema = object({
59023
59749
  deviceId: number(),
@@ -59082,17 +59808,7 @@ onPressed: {
59082
59808
  history: "none",
59083
59809
  customize: doorbellCustomize
59084
59810
  };
59085
- /**
59086
- * Enum-state sensor — a string value picked from a finite option set.
59087
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59088
- * states, weather conditions, contact-source channels, mode strings).
59089
- * The option set is stable across observations; it is persisted in the
59090
- * device config blob (via the control slice at adoption) and is NOT
59091
- * stored in `sourceInfo`.
59092
- */
59093
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59094
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59095
- var EnumSensorDateTimeFormatSchema = _enum([
59811
+ _enum([
59096
59812
  "date",
59097
59813
  "time",
59098
59814
  "datetime"
@@ -59100,12 +59816,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59100
59816
  var EnumSensorStatusSchema = object({
59101
59817
  value: string(),
59102
59818
  /**
59103
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59104
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59105
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59106
- * a time-only sensor → `'time'`.
59819
+ * Set when `value` is a timestamp rather than an enum state, so history
59820
+ * does not learn ISO strings as states. The display shape (date / time /
59821
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59107
59822
  */
59108
- format: EnumSensorDateTimeFormatSchema.optional(),
59823
+ timestamp: boolean().optional(),
59109
59824
  /** Ms epoch when the slice was last updated. */
59110
59825
  lastFetchedAt: number()
59111
59826
  });
@@ -59132,11 +59847,11 @@ var enumSensorCapability = {
59132
59847
  * History (D850): the value as an `enum` series (an appliance's program or
59133
59848
  * operation state, an HVAC action). The option set is per device, so the
59134
59849
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59135
- * `format` carries a timestamp, not a state, and is not recorded.
59850
+ * `timestamp` carries a clock, not a state, and is not recorded.
59136
59851
  */
59137
59852
  history: { fields: { value: {
59138
59853
  kind: "enum",
59139
- skipWhenSet: "format"
59854
+ skipWhenSet: "timestamp"
59140
59855
  } } },
59141
59856
  /** Clock fields: written, but excluded from the compare that decides
59142
59857
  * whether persisting is worth a SQLite commit. */
@@ -59156,13 +59871,13 @@ var EventFireSchema = object({
59156
59871
  deviceId: number(),
59157
59872
  eventType: string(),
59158
59873
  data: record(string(), unknown()).nullable(),
59159
- timestamp: number(),
59160
- seq: number()
59874
+ timestamp: number().meta(readingMeta("none")),
59875
+ seq: number().meta(readingMeta("count"))
59161
59876
  });
59162
59877
  var EventEmitterStatusSchema = object({
59163
59878
  eventTypes: array(string()),
59164
59879
  lastEvent: EventFireSchema.nullable(),
59165
- eventCountSinceStart: number()
59880
+ eventCountSinceStart: number().meta(readingMeta("count"))
59166
59881
  });
59167
59882
  var eventEmitterCapability = {
59168
59883
  name: "event-emitter",
@@ -59575,10 +60290,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59575
60290
  var FanControlStatusSchema = object({
59576
60291
  /** Active speed as 0..100 inclusive. Null when the device has no
59577
60292
  * speed surface (single-speed fan). */
59578
- percentage: number().min(0).max(100).nullable(),
60293
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59579
60294
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59580
60295
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59581
- percentageStep: number().positive().optional(),
60296
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59582
60297
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59583
60298
  * — empty string when no preset is active or supported. */
59584
60299
  preset: string(),
@@ -59699,7 +60414,7 @@ var FeatureProbeStatusSchema = object({
59699
60414
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59700
60415
  model: string().nullable(),
59701
60416
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59702
- channelCount: number().nullable(),
60417
+ channelCount: number().nullable().meta(readingMeta("count")),
59703
60418
  /**
59704
60419
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59705
60420
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59822,9 +60537,151 @@ var gasCapability = {
59822
60537
  * whether persisting is worth a SQLite commit. */
59823
60538
  volatileStateFields: ["lastChangedAt"]
59824
60539
  };
60540
+ var GroupCommandsSchema = object({
60541
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60542
+ results: array(object({
60543
+ deviceId: number().int().nonnegative(),
60544
+ cap: string().min(1),
60545
+ method: string().min(1),
60546
+ ok: boolean(),
60547
+ error: string().optional(),
60548
+ /** Ms epoch of the attempt. */
60549
+ at: number()
60550
+ })) });
60551
+ var groupCommandsCapability = {
60552
+ name: "group-commands",
60553
+ scope: "device",
60554
+ deviceNative: true,
60555
+ mode: "singleton",
60556
+ deviceTypes: [
60557
+ DeviceType.Switch,
60558
+ DeviceType.Light,
60559
+ DeviceType.Fan,
60560
+ DeviceType.Cover,
60561
+ DeviceType.Valve,
60562
+ DeviceType.Lock,
60563
+ DeviceType.MediaPlayer,
60564
+ DeviceType.Thermostat,
60565
+ DeviceType.Climate
60566
+ ],
60567
+ methods: {},
60568
+ status: {
60569
+ schema: GroupCommandsSchema,
60570
+ kind: "push"
60571
+ },
60572
+ runtimeState: GroupCommandsSchema,
60573
+ /**
60574
+ * Runtime-state durability: **session** — the last attempt. A restored
60575
+ * failure would outlive the command that caused it.
60576
+ *
60577
+ * See `RuntimeStateDurability`. Enforced by
60578
+ * `scripts/check-runtime-state-durability.ts`.
60579
+ */
60580
+ durability: "session",
60581
+ history: "none"
60582
+ };
60583
+ /**
60584
+ * Whether a helper actuator group counts as active (D944): at least one
60585
+ * included member is, or every included member is. What "active" means is the
60586
+ * type's: a cover is open, a lock is locked, a media player is playing, a
60587
+ * climate device is not off. A group of lights, switches and fans reports
60588
+ * `switch.on` instead; this cap is for the types whose own state is not a
60589
+ * yes/no. Unknown members are left out (`group-status` counts them) and an
60590
+ * empty included set takes the device offline, never `active: false`.
60591
+ */
60592
+ var GroupActiveSchema = object({
60593
+ active: boolean().meta({ label: "Active" }),
60594
+ /** Ms epoch when the slice was last updated. */
60595
+ lastChangedAt: number()
60596
+ });
60597
+ var groupActiveCapability = {
60598
+ name: "group-active",
60599
+ scope: "device",
60600
+ deviceNative: true,
60601
+ mode: "singleton",
60602
+ deviceTypes: [
60603
+ DeviceType.Cover,
60604
+ DeviceType.Valve,
60605
+ DeviceType.Lock,
60606
+ DeviceType.MediaPlayer,
60607
+ DeviceType.Climate,
60608
+ DeviceType.Thermostat
60609
+ ],
60610
+ methods: {},
60611
+ status: {
60612
+ schema: GroupActiveSchema,
60613
+ kind: "push"
60614
+ },
60615
+ runtimeState: GroupActiveSchema,
60616
+ /**
60617
+ * Runtime-state durability: **session** — recomputed from the members on the
60618
+ * next source event; a restored value would outlive the members it named.
60619
+ *
60620
+ * See `RuntimeStateDurability`. Enforced by
60621
+ * `scripts/check-runtime-state-durability.ts`.
60622
+ */
60623
+ durability: "session",
60624
+ history: "none"
60625
+ };
60626
+ /**
60627
+ * How many members of a helper group answered (D944). Counts are integers,
60628
+ * not measurements: zero included is a real count, never a stand-in reading.
60629
+ * The reading itself stays on the output capability. History is none — the
60630
+ * series that later statistics read is the output cap's own history block.
60631
+ */
60632
+ var GroupStatusSchema = object({
60633
+ /** Members whose current value entered the aggregate. */
60634
+ included: number().int().nonnegative().meta(readingMeta("count", {
60635
+ precision: 0,
60636
+ label: "Reporting"
60637
+ })),
60638
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60639
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60640
+ precision: 0,
60641
+ label: "Excluded"
60642
+ })),
60643
+ /** `included` + `excluded`. */
60644
+ total: number().int().nonnegative().meta(readingMeta("count", {
60645
+ precision: 0,
60646
+ label: "Members"
60647
+ }))
60648
+ });
60649
+ var groupStatusCapability = {
60650
+ name: "group-status",
60651
+ scope: "device",
60652
+ deviceNative: true,
60653
+ mode: "singleton",
60654
+ deviceTypes: [
60655
+ DeviceType.Sensor,
60656
+ DeviceType.Switch,
60657
+ DeviceType.Light,
60658
+ DeviceType.Fan,
60659
+ DeviceType.Cover,
60660
+ DeviceType.Valve,
60661
+ DeviceType.Lock,
60662
+ DeviceType.MediaPlayer,
60663
+ DeviceType.Thermostat,
60664
+ DeviceType.Climate
60665
+ ],
60666
+ methods: {},
60667
+ status: {
60668
+ schema: GroupStatusSchema,
60669
+ kind: "push"
60670
+ },
60671
+ runtimeState: GroupStatusSchema,
60672
+ /**
60673
+ * Runtime-state durability: **session** — recomputed from members on the
60674
+ * next source event; a restored count would outlive the readings it named.
60675
+ *
60676
+ * See `RuntimeStateDurability`. Enforced by
60677
+ * `scripts/check-runtime-state-durability.ts`.
60678
+ */
60679
+ durability: "session",
60680
+ history: "none"
60681
+ };
59825
60682
  var HeatingValveStatusSchema = object({
59826
60683
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59827
- position: number().min(0).max(100).nullable(),
60684
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59828
60685
  /** Ms epoch when the slice was last updated (push or poll). */
59829
60686
  lastFetchedAt: number()
59830
60687
  });
@@ -59867,9 +60724,9 @@ var HumidifierStatusSchema = object({
59867
60724
  /** Whether the humidifier is currently on. */
59868
60725
  on: boolean(),
59869
60726
  /** Current measured relative humidity (0..100). Null when not reported. */
59870
- currentHumidity: number().min(0).max(100).nullable(),
60727
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59871
60728
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59872
- targetHumidity: number().min(0).max(100).nullable(),
60729
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59873
60730
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59874
60731
  * device has no mode surface. */
59875
60732
  mode: string().nullable(),
@@ -59879,9 +60736,9 @@ var HumidifierStatusSchema = object({
59879
60736
  * `idle` / `off`). Null when the device doesn't report it. */
59880
60737
  action: string().nullable(),
59881
60738
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59882
- minHumidity: number().nullable(),
60739
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59883
60740
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59884
- maxHumidity: number().nullable(),
60741
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59885
60742
  /** Ms epoch when the slice was last updated. */
59886
60743
  lastChangedAt: number()
59887
60744
  });
@@ -59951,22 +60808,13 @@ var humidifierCapability = {
59951
60808
  * `sourceInfo`.
59952
60809
  */
59953
60810
  var HumiditySensorStatusSchema = object({
59954
- /** Current relative humidity, 0..100. */
59955
- percent: number().min(0).max(100).meta({
59956
- unit: "%",
60811
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60812
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60813
+ precision: 0,
59957
60814
  label: "Humidity"
59958
- }),
60815
+ })),
59959
60816
  /** Ms epoch when the slice was last updated. */
59960
- lastFetchedAt: number(),
59961
- /** Live display unit from the upstream source (e.g. HA
59962
- * `attributes.unit_of_measurement`). The UI prefers this over the
59963
- * role's canonical unit. Absent → fall back to the canonical unit. */
59964
- unit: string().optional(),
59965
- /** Suggested decimal places for numeric display.
59966
- * Populated live from the upstream source when provided (e.g. HA
59967
- * `attributes.suggested_display_precision`). Falls back to
59968
- * auto-formatting when absent. */
59969
- precision: number().int().min(0).max(10).optional()
60817
+ lastFetchedAt: number()
59970
60818
  });
59971
60819
  var humiditySensorCapability = {
59972
60820
  name: "humidity-sensor",
@@ -59997,12 +60845,7 @@ var humiditySensorCapability = {
59997
60845
  } } },
59998
60846
  /** Clock fields: written, but excluded from the compare that decides
59999
60847
  * whether persisting is worth a SQLite commit. */
60000
- volatileStateFields: ["lastFetchedAt"],
60001
- customize: {
60002
- version: 1,
60003
- advanced: "derive",
60004
- targets: []
60005
- }
60848
+ volatileStateFields: ["lastFetchedAt"]
60006
60849
  };
60007
60850
  /**
60008
60851
  * Image display cap. Models a single still image exposed by an integration —
@@ -60023,7 +60866,7 @@ var ImageStatusSchema = object({
60023
60866
  * entity exposes no `entity_picture` (yet). */
60024
60867
  url: string().nullable(),
60025
60868
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60026
- lastUpdated: number().nullable()
60869
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60027
60870
  });
60028
60871
  var imageCapability = {
60029
60872
  name: "image",
@@ -60111,19 +60954,19 @@ var NormalizedRangeSchema = object({
60111
60954
  */
60112
60955
  var ImageSettingsStatusSchema = object({
60113
60956
  /** Normalized 0–100. */
60114
- brightness: number().optional(),
60957
+ brightness: number().optional().meta(readingMeta("none")),
60115
60958
  /** Normalized 0–100. */
60116
- contrast: number().optional(),
60959
+ contrast: number().optional().meta(readingMeta("none")),
60117
60960
  /** Normalized 0–100. */
60118
- saturation: number().optional(),
60961
+ saturation: number().optional().meta(readingMeta("none")),
60119
60962
  /** Normalized 0–100. */
60120
- sharpness: number().optional(),
60963
+ sharpness: number().optional().meta(readingMeta("none")),
60121
60964
  mirror: boolean().optional(),
60122
60965
  flip: boolean().optional(),
60123
60966
  rotate: ImageRotateSchema.optional(),
60124
60967
  whiteBalance: WhiteBalanceModeSchema.optional(),
60125
60968
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60126
- warmth: number().optional(),
60969
+ warmth: number().optional().meta(readingMeta("none")),
60127
60970
  exposureMode: ExposureModeSchema.optional(),
60128
60971
  backlightMode: BacklightModeSchema.optional(),
60129
60972
  lastFetchedAt: number()
@@ -60402,10 +61245,10 @@ var IntercomAbilitySchema = object({
60402
61245
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60403
61246
  codecs: array(string()),
60404
61247
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60405
- sampleRate: number(),
61248
+ sampleRate: number().meta(readingMeta("none")),
60406
61249
  duplex: _enum(["half", "full"]),
60407
61250
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60408
- maxBacklogMs: number()
61251
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60409
61252
  });
60410
61253
  var IntercomStatusSchema = object({
60411
61254
  /** True while a talk session is open. */
@@ -60632,13 +61475,13 @@ var LawnMowerControlStatusSchema = object({
60632
61475
  activity: LawnMowerActivitySchema,
60633
61476
  /** 0..100 battery percentage. Null when the device has no battery
60634
61477
  * reading. */
60635
- batteryLevel: number().min(0).max(100).nullable(),
61478
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60636
61479
  /** 0..100 mowing-completion percentage of the current task, or null when no
60637
61480
  * task is active / progress is unavailable. */
60638
- progressPercent: number().min(0).max(100).nullable(),
61481
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60639
61482
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60640
61483
  * or null when unknown. */
60641
- currentCode: number().nullable(),
61484
+ currentCode: number().nullable().meta(readingMeta("none")),
60642
61485
  /** Human label for {@link currentCode}, or null when undecodable. */
60643
61486
  currentCodeLabel: string().nullable(),
60644
61487
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61091,7 +61934,14 @@ var lockControlCapability = {
61091
61934
  }),
61092
61935
  unlock: method(object({
61093
61936
  deviceId: number().int().nonnegative(),
61094
- code: string().min(1).optional()
61937
+ code: string().min(1).optional(),
61938
+ /**
61939
+ * Honoured by a helper lock GROUP only (D944): unlocking several doors
61940
+ * with one call is refused unless the caller says it was confirmed. A
61941
+ * single lock ignores it. The refusal lives in the callee, so no
61942
+ * automation or API caller reaches the doors by skipping a dialog.
61943
+ */
61944
+ confirm: boolean().optional()
61095
61945
  }), _void(), {
61096
61946
  kind: "mutation",
61097
61947
  auth: "protected"
@@ -61128,6 +61978,44 @@ var lockControlCapability = {
61128
61978
  volatileStateFields: ["lastChangedAt"]
61129
61979
  };
61130
61980
  /**
61981
+ * A monotonic total between resets (D944). Energy uses `power-meter.kwhTotal`
61982
+ * so dashboards keep one series; every other instantaneous reading lands here.
61983
+ * `value` is the time integral in the source quantity's accumulated base
61984
+ * (base-unit seconds, or kWh on the power meter). `null` is a gap, never zero.
61985
+ */
61986
+ var MeterStatusSchema = object({
61987
+ value: number().nullable().meta(readingMeta("device", { label: "Total" })),
61988
+ lastFetchedAt: number()
61989
+ });
61990
+ var meterCapability = {
61991
+ name: "meter",
61992
+ scope: "device",
61993
+ deviceNative: true,
61994
+ mode: "singleton",
61995
+ deviceTypes: [DeviceType.Sensor],
61996
+ methods: {},
61997
+ status: {
61998
+ schema: MeterStatusSchema,
61999
+ kind: "push"
62000
+ },
62001
+ runtimeState: MeterStatusSchema,
62002
+ /**
62003
+ * Runtime-state durability: **restored** — the total is a cache of the
62004
+ * history integral. A restart shows it only until the history pass
62005
+ * recomputes the open period; it is not an in-memory buffer.
62006
+ *
62007
+ * See `RuntimeStateDurability`. Enforced by
62008
+ * `scripts/check-runtime-state-durability.ts`.
62009
+ */
62010
+ durability: "restored",
62011
+ history: { fields: { value: {
62012
+ kind: "counter",
62013
+ deadband: .001,
62014
+ minIntervalMs: 6e4
62015
+ } } },
62016
+ volatileStateFields: ["lastFetchedAt"]
62017
+ };
62018
+ /**
61131
62019
  * Media-player cap. Models HA `media_player.*` (Sonos, Chromecast,
61132
62020
  * Apple TV, Spotify, Roku, generic OTT receivers, …) on
61133
62021
  * `DeviceType.MediaPlayer`. Disjoint from the camera streaming
@@ -61171,7 +62059,7 @@ var MediaPlayerStatusSchema = object({
61171
62059
  state: MediaPlayerStateSchema,
61172
62060
  /** Volume as 0..100 inclusive. Null when the device has no volume
61173
62061
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61174
- volumeLevel: number().min(0).max(100).nullable(),
62062
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61175
62063
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61176
62064
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61177
62065
  isMuted: boolean().nullable(),
@@ -61184,10 +62072,10 @@ var MediaPlayerStatusSchema = object({
61184
62072
  currentMedia: MediaInfoSchema.nullable(),
61185
62073
  /** Current playback position in ms. Null when not seekable or
61186
62074
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61187
- positionMs: number().int().nonnegative().nullable(),
62075
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61188
62076
  /** Total duration of the current media in ms. Null when unknown
61189
62077
  * (live stream) or nothing is playing. */
61190
- durationMs: number().int().nonnegative().nullable(),
62078
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61191
62079
  /** Shuffle toggle. Null when no shuffle surface.
61192
62080
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61193
62081
  shuffle: boolean().nullable(),
@@ -61496,7 +62384,7 @@ var motionCustomize = {
61496
62384
  eventName: "onMotionChanged"
61497
62385
  },
61498
62386
  deviceTypes: [DeviceType.Camera],
61499
- apply: "replace",
62387
+ apply: "add",
61500
62388
  applyAuthority: {
61501
62389
  kind: "camera-sensors",
61502
62390
  timeoutField: "motionCooldownMs"
@@ -61568,7 +62456,7 @@ var MotionStatusSchema = object({
61568
62456
  * it, every provider seeded it at boot, and every session left it
61569
62457
  * `detected: true` after its detach.
61570
62458
  */
61571
- autoClearAfterMs: number().nullable()
62459
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61572
62460
  });
61573
62461
  /**
61574
62462
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61605,12 +62493,18 @@ var motionCapability = {
61605
62493
  methods: {
61606
62494
  /**
61607
62495
  * Pull the current motion state synchronously. Convenience shortcut
61608
- * for consumers that don't need the full status object; equivalent
61609
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61610
- * `getStatus` once the auto-injected status surface lands in every
61611
- * consumer.
62496
+ * for consumers that don't need the full status object. A composed
62497
+ * group answers from its slice (D944): unknown throws, it is not off.
61612
62498
  */
61613
62499
  isDetected: method(object({ deviceId: number() }), boolean()) },
62500
+ /**
62501
+ * A composed motion group serves `isDetected` from `detected` (D944).
62502
+ * Without this the cap stays uncomposable (`has-commands`).
62503
+ */
62504
+ composedMethods: { isDetected: {
62505
+ kind: "slice",
62506
+ field: "detected"
62507
+ } },
61614
62508
  events: {
61615
62509
  /**
61616
62510
  * Fires every time the runner transitions a camera between
@@ -61750,7 +62644,7 @@ var MotionZoneRegionSchema = object({
61750
62644
  * the grid region(s). */
61751
62645
  var MotionZoneStatusSchema = object({
61752
62646
  enabled: boolean(),
61753
- sensitivity: number(),
62647
+ sensitivity: number().meta(readingMeta("none")),
61754
62648
  /** Grid region(s). Today exactly one `grid` shape. */
61755
62649
  regions: array(MotionZoneRegionSchema),
61756
62650
  lastFetchedAt: number()
@@ -62120,7 +63014,7 @@ var NavigationStatusSchema = object({
62120
63014
  following: boolean(),
62121
63015
  flash: boolean(),
62122
63016
  lightMode: NavigationLightModeSchema,
62123
- lightLevel: number().min(40).max(100),
63017
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62124
63018
  /** Ms epoch when the slice was last updated. */
62125
63019
  lastChangedAt: number()
62126
63020
  });
@@ -62254,13 +63148,13 @@ var NetworkLinkStatusSchema = object({
62254
63148
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62255
63149
  * SKIP a null rather than coerce it.
62256
63150
  */
62257
- signalPercent: number().min(0).max(100).nullable(),
63151
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62258
63152
  /** Raw received signal strength in dBm, when the firmware reports one. */
62259
- rssiDbm: number().optional(),
63153
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62260
63154
  /** Network name of a wireless link, when the firmware reports it. */
62261
63155
  ssid: string().optional(),
62262
63156
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62263
- lastUpdated: number()
63157
+ lastUpdated: number().meta(readingMeta("none"))
62264
63158
  });
62265
63159
  var networkLinkCapability = {
62266
63160
  name: "network-link",
@@ -62645,7 +63539,7 @@ var NotifierStatusSchema = object({
62645
63539
  lastError: string().nullable(),
62646
63540
  /** Number of deliveries currently buffered server-side (rate-limited
62647
63541
  * notifiers may queue). 0 when the provider sends synchronously. */
62648
- queueDepth: number().int().nonnegative(),
63542
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62649
63543
  /** Per-feature capability matrix — authoritative source for the
62650
63544
  * compose-form field gating. */
62651
63545
  supports: NotifierSupportsSchema
@@ -62737,11 +63631,9 @@ onSent: { data: object({
62737
63631
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62738
63632
  * for: water flow, distance, weight, frequency, signal strength, …).
62739
63633
  *
62740
- * The `unit` and `precision` fields carry the live display metadata that
62741
- * the upstream source provides with each state push (HA
62742
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62743
- * The UI reads them directly from the slice — they are the single source
62744
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63634
+ * The quantity is not on this slice. It is chosen when the device is
63635
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63636
+ * that quantity's base unit. `null` is a refused sample (D393).
62745
63637
  *
62746
63638
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62747
63639
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62750,15 +63642,8 @@ onSent: { data: object({
62750
63642
  * numeric values land here.
62751
63643
  */
62752
63644
  var NumericSensorStatusSchema = object({
62753
- value: number(),
62754
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62755
- * Populated live from the upstream source on each state push.
62756
- * Absent for unitless sensors. */
62757
- unit: string().optional(),
62758
- /** Suggested decimal places for numeric display.
62759
- * Populated live from the upstream source when provided.
62760
- * Falls back to auto-formatting when absent. */
62761
- precision: number().int().min(0).max(10).optional(),
63645
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63646
+ value: number().nullable().meta(readingMeta("device")),
62762
63647
  /** Ms epoch when the slice was last updated. */
62763
63648
  lastFetchedAt: number()
62764
63649
  });
@@ -62782,11 +63667,11 @@ var numericSensorCapability = {
62782
63667
  */
62783
63668
  durability: "restored",
62784
63669
  /**
62785
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63670
+ * History (D850): recorded series. The unit is the device quantity's base,
63671
+ * resolved when the series is read — the cap has no single unit.
62786
63672
  */
62787
63673
  history: { fields: { value: {
62788
63674
  kind: "gauge",
62789
- unitFrom: "unit",
62790
63675
  minIntervalMs: 3e4
62791
63676
  } } },
62792
63677
  /** Clock fields: written, but excluded from the compare that decides
@@ -63078,23 +63963,26 @@ var PetFeederStatusSchema = object({
63078
63963
  /** Food currently in the bowl (grams). Null when the device has not
63079
63964
  * reported a reading yet. On dual-hopper models this is the combined
63080
63965
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63081
- foodLevel: number().nullable(),
63966
+ foodLevel: number().nullable().meta(readingMeta("none")),
63082
63967
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63083
63968
  * single-hopper models. */
63084
- food1: number().nullable(),
63969
+ food1: number().nullable().meta(readingMeta("none")),
63085
63970
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63086
63971
  * single-hopper models. */
63087
- food2: number().nullable(),
63972
+ food2: number().nullable().meta(readingMeta("none")),
63088
63973
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63089
63974
  * (`device_class: problem`, on = low). True when the bowl is empty /
63090
63975
  * below the feeder's low threshold. */
63091
63976
  lowFood: boolean(),
63092
63977
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63093
63978
  * device has no battery reading. */
63094
- batteryPower: number().min(0).max(100).nullable(),
63095
- /** Days of desiccant life remaining. Null when the model has no
63096
- * desiccant sensor. */
63097
- desiccantLeftDays: number().nullable(),
63979
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63980
+ /**
63981
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63982
+ * Providers convert from the days the feeder reports. Null when the
63983
+ * model has no desiccant sensor.
63984
+ */
63985
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63098
63986
  /** True while a feed is in progress. */
63099
63987
  feeding: boolean(),
63100
63988
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63105,7 +63993,7 @@ var PetFeederStatusSchema = object({
63105
63993
  * with `errorCode` for consumers that want the raw integer. */
63106
63994
  error: string().nullable(),
63107
63995
  /** Raw device error code (0 / null = no error). */
63108
- errorCode: number().nullable(),
63996
+ errorCode: number().nullable().meta(readingMeta("none")),
63109
63997
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63110
63998
  isDualHopper: boolean(),
63111
63999
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63115,7 +64003,7 @@ var PetFeederStatusSchema = object({
63115
64003
  /** Play a chime when dispensing. */
63116
64004
  feedSound: boolean(),
63117
64005
  /** Speaker / prompt volume level (device-scaled integer). */
63118
- volume: number(),
64006
+ volume: number().meta(readingMeta("none")),
63119
64007
  /** Ms epoch when the slice was last refreshed from the cloud. */
63120
64008
  lastFetchedAt: number()
63121
64009
  });
@@ -63234,7 +64122,7 @@ var petFeederCapability = {
63234
64122
  },
63235
64123
  desiccantLeftDays: {
63236
64124
  kind: "gauge",
63237
- unit: "days"
64125
+ unit: "s"
63238
64126
  }
63239
64127
  } }
63240
64128
  };
@@ -63635,43 +64523,88 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63635
64523
  */
63636
64524
  var PowerMeterStatusSchema = object({
63637
64525
  /** Instantaneous power draw in watts. */
63638
- watts: number().optional().meta({
63639
- unit: "W",
63640
- label: "Power"
63641
- }),
64526
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63642
64527
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63643
- kwhTotal: number().optional().meta({
63644
- unit: "kWh",
63645
- label: "Energy"
63646
- }),
64528
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63647
64529
  /** Voltage in volts. */
63648
- volts: number().optional(),
64530
+ volts: number().optional().meta(readingMeta("voltage", { label: "Voltage" })),
63649
64531
  /** Current in amperes. */
63650
- amps: number().optional(),
63651
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63652
- va: number().optional(),
64532
+ amps: number().optional().meta(readingMeta("current", { label: "Current" })),
64533
+ /** Apparent power in volt-amperes. */
64534
+ va: number().optional().meta(readingMeta("apparent-power")),
63653
64535
  /** Power factor (0..1) if reported. */
63654
- powerFactor: number().min(0).max(1).optional(),
64536
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63655
64537
  /** Ms epoch when the slice was last updated. */
63656
- lastFetchedAt: number(),
63657
- /** Live display unit of the single metric this slice carries (e.g. HA
63658
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63659
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63660
- * unit per slice is unambiguous. The UI prefers this over the role's
63661
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63662
- unit: string().optional(),
63663
- /** Suggested decimal places for numeric display.
63664
- * Populated live from the upstream source when provided (e.g. HA
63665
- * `attributes.suggested_display_precision`). Falls back to
63666
- * auto-formatting when absent. */
63667
- precision: number().int().min(0).max(10).optional()
64538
+ lastFetchedAt: number()
63668
64539
  });
64540
+ /**
64541
+ * Loads that draw metered mains power (D951). The reading is a feature of the
64542
+ * device itself — a plug, a lamp, a fan, an air conditioner, a boiler, a
64543
+ * vacuum dock — not a separate sensor beside it. A `Sensor` stays: a clamp
64544
+ * meter or a standalone energy monitor IS the device. Cover, lock, valve,
64545
+ * camera and the like are left out: their draw is not what anyone meters.
64546
+ */
64547
+ var POWER_METER_DEVICE_TYPES = [
64548
+ DeviceType.Sensor,
64549
+ DeviceType.Switch,
64550
+ DeviceType.Light,
64551
+ DeviceType.Fan,
64552
+ DeviceType.Climate,
64553
+ DeviceType.Thermostat,
64554
+ DeviceType.Humidifier,
64555
+ DeviceType.WaterHeater,
64556
+ DeviceType.MediaPlayer,
64557
+ DeviceType.Washer,
64558
+ DeviceType.Dryer,
64559
+ DeviceType.Dishwasher,
64560
+ DeviceType.Vacuum,
64561
+ DeviceType.LawnMower,
64562
+ DeviceType.PetFeeder
64563
+ ];
64564
+ /** Everything but a bare `Sensor`: a sensor that takes its reading from another sensor adds nothing. */
64565
+ var POWER_METER_CUSTOMIZE_DEVICE_TYPES = POWER_METER_DEVICE_TYPES.filter((type) => type !== DeviceType.Sensor);
64566
+ /** One scalar target per reading; each is sourced from a device of its own (D951). */
64567
+ function powerTarget(id, unit) {
64568
+ return {
64569
+ id,
64570
+ cardinality: "scalar",
64571
+ vocabulary: {
64572
+ cap: "power-meter",
64573
+ stateField: id
64574
+ },
64575
+ deviceTypes: POWER_METER_CUSTOMIZE_DEVICE_TYPES,
64576
+ apply: "add",
64577
+ path: id,
64578
+ sources: [{
64579
+ kind: "numeric",
64580
+ requiredUnit: unit,
64581
+ transforms: [{ kind: "identity" }]
64582
+ }],
64583
+ companions: [{
64584
+ kind: "clock",
64585
+ path: "lastFetchedAt",
64586
+ when: "always"
64587
+ }],
64588
+ sourceReport: { status: "assignment-status" },
64589
+ advancedExpression: true
64590
+ };
64591
+ }
64592
+ /** "Power and energy" extension: watts, energy, voltage and current from other devices (D951). */
64593
+ var powerMeterCustomize = {
64594
+ version: 1,
64595
+ targets: [
64596
+ powerTarget("watts", "W"),
64597
+ powerTarget("kwhTotal", "kWh"),
64598
+ powerTarget("volts", "V"),
64599
+ powerTarget("amps", "A")
64600
+ ]
64601
+ };
63669
64602
  var powerMeterCapability = {
63670
64603
  name: "power-meter",
63671
64604
  scope: "device",
63672
64605
  deviceNative: true,
63673
64606
  mode: "singleton",
63674
- deviceTypes: [DeviceType.Sensor],
64607
+ deviceTypes: POWER_METER_DEVICE_TYPES,
63675
64608
  methods: {},
63676
64609
  status: {
63677
64610
  schema: PowerMeterStatusSchema,
@@ -63729,11 +64662,7 @@ var powerMeterCapability = {
63729
64662
  /** Clock fields: written, but excluded from the compare that decides
63730
64663
  * whether persisting is worth a SQLite commit. */
63731
64664
  volatileStateFields: ["lastFetchedAt"],
63732
- customize: {
63733
- version: 1,
63734
- advanced: "derive",
63735
- targets: []
63736
- }
64665
+ customize: powerMeterCustomize
63737
64666
  };
63738
64667
  /**
63739
64668
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63753,11 +64682,11 @@ var powerMeterCapability = {
63753
64682
  */
63754
64683
  var GpsLocationSchema = object({
63755
64684
  /** Latitude in decimal degrees, -90..90. */
63756
- latitude: number().min(-90).max(90),
64685
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63757
64686
  /** Longitude in decimal degrees, -180..180. */
63758
- longitude: number().min(-180).max(180),
64687
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63759
64688
  /** Reported accuracy in meters (lower = better). */
63760
- accuracyMeters: number().nonnegative()
64689
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63761
64690
  });
63762
64691
  var PresenceStatusSchema = object({
63763
64692
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63769,7 +64698,7 @@ var PresenceStatusSchema = object({
63769
64698
  gps: GpsLocationSchema.nullable(),
63770
64699
  /** Optional battery level of the tracking device (0..100). Null
63771
64700
  * when not reported. */
63772
- batteryPercent: number().min(0).max(100).nullable(),
64701
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63773
64702
  /** Ms epoch when the slice was last updated. */
63774
64703
  lastChangedAt: number()
63775
64704
  });
@@ -63819,19 +64748,10 @@ var presenceCapability = {
63819
64748
  * `sourceInfo`.
63820
64749
  */
63821
64750
  var PressureSensorStatusSchema = object({
63822
- /** Current pressure in hPa. */
63823
- hpa: number(),
64751
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64752
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63824
64753
  /** Ms epoch when the slice was last updated. */
63825
- lastFetchedAt: number(),
63826
- /** Live display unit from the upstream source (e.g. HA
63827
- * `attributes.unit_of_measurement`). The UI prefers this over the
63828
- * role's canonical unit. Absent → fall back to the canonical unit. */
63829
- unit: string().optional(),
63830
- /** Suggested decimal places for numeric display.
63831
- * Populated live from the upstream source when provided (e.g. HA
63832
- * `attributes.suggested_display_precision`). Falls back to
63833
- * auto-formatting when absent. */
63834
- precision: number().int().min(0).max(10).optional()
64754
+ lastFetchedAt: number()
63835
64755
  });
63836
64756
  var pressureSensorCapability = {
63837
64757
  name: "pressure-sensor",
@@ -64245,8 +65165,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64245
65165
  }), _void(), { kind: "mutation" });
64246
65166
  var PtzAutotrackSettingsSchema = object({
64247
65167
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64248
- stopDelaySeconds: number().int().min(0).max(300),
64249
- disappearDelaySeconds: number().int().min(0).max(300)
65168
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65169
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64250
65170
  });
64251
65171
  /**
64252
65172
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64400,7 +65320,7 @@ var RunningStateStatusSchema = object({
64400
65320
  /** null = unknown — never coerced to false (D393). */
64401
65321
  running: boolean().nullable(),
64402
65322
  /** Epoch ms the current boolean began; null while `running` is null. */
64403
- since: number().nullable(),
65323
+ since: number().nullable().meta(readingMeta("none")),
64404
65324
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64405
65325
  phase: string().nullable(),
64406
65326
  /** null until a reading has been published (D393). */
@@ -64415,7 +65335,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64415
65335
  /** Generic Customize target for the running boolean (D936). */
64416
65336
  var runningCustomize = {
64417
65337
  version: 1,
64418
- advanced: "derive",
64419
65338
  targets: [{
64420
65339
  id: "running",
64421
65340
  cardinality: "scalar",
@@ -64424,6 +65343,8 @@ var runningCustomize = {
64424
65343
  stateField: "running"
64425
65344
  },
64426
65345
  deviceTypes: [
65346
+ DeviceType.Washer,
65347
+ DeviceType.Dryer,
64427
65348
  DeviceType.Dishwasher,
64428
65349
  DeviceType.Vacuum,
64429
65350
  DeviceType.LawnMower,
@@ -64435,7 +65356,7 @@ var runningCustomize = {
64435
65356
  DeviceType.Thermostat,
64436
65357
  DeviceType.PetFeeder
64437
65358
  ],
64438
- apply: "add-or-replace",
65359
+ apply: "add",
64439
65360
  path: "running",
64440
65361
  sources: [
64441
65362
  {
@@ -65699,9 +66620,9 @@ var RecordTriggerSchema = _enum([
65699
66620
  */
65700
66621
  var RecordWindowSchema = object({
65701
66622
  trigger: RecordTriggerSchema,
65702
- day: number().int().min(0).max(6),
65703
- startMinute: number().int().min(0).max(1439),
65704
- endMinute: number().int().min(1).max(1440)
66623
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66624
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66625
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65705
66626
  });
65706
66627
  /** Status of one physical volume, as the camera itself describes it. */
65707
66628
  var OnboardStorageVolumeSchema = object({
@@ -65777,10 +66698,10 @@ var RecordingOnboardStatusSchema = object({
65777
66698
  /** Master "record to the card at all" switch. */
65778
66699
  enabled: boolean().nullable(),
65779
66700
  overwriteWhenFull: boolean().nullable(),
65780
- preRecordSec: number().nullable(),
65781
- postRecordSec: number().nullable(),
66701
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66702
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65782
66703
  /** Length of one recorded file, in minutes. */
65783
- segmentMinutes: number().nullable(),
66704
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65784
66705
  /** The primary track's weekly windows, flattened. */
65785
66706
  windows: array(RecordWindowSchema),
65786
66707
  /**
@@ -65796,7 +66717,7 @@ var RecordingOnboardStatusSchema = object({
65796
66717
  * provider reporting a non-zero count also reports the schedule as not
65797
66718
  * writable.
65798
66719
  */
65799
- unreadableWindows: number(),
66720
+ unreadableWindows: number().meta(readingMeta("count")),
65800
66721
  /**
65801
66722
  * The camera is scheduled to record and has NO usable storage.
65802
66723
  *
@@ -66641,7 +67562,7 @@ var CamStreamDescriptorSchema = object({
66641
67562
  url: string().optional(),
66642
67563
  codec: string().optional(),
66643
67564
  resolution: CamStreamResolutionSchema.optional(),
66644
- fps: number().positive().optional(),
67565
+ fps: number().positive().optional().meta(readingMeta("rate")),
66645
67566
  label: string().optional(),
66646
67567
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66647
67568
  deviceFeatures: array(string()).optional(),
@@ -66728,18 +67649,18 @@ var StreamProfileSchema = _enum([
66728
67649
  ]);
66729
67650
  /** The current encoding config of one stream profile. */
66730
67651
  var StreamProfileConfigSchema = object({
66731
- width: number(),
66732
- height: number(),
67652
+ width: number().meta(readingMeta("none")),
67653
+ height: number().meta(readingMeta("none")),
66733
67654
  codec: _enum(["h264", "h265"]),
66734
- framerate: number(),
66735
- bitrate: number(),
67655
+ framerate: number().meta(readingMeta("rate")),
67656
+ bitrate: number().meta(readingMeta("bitrate")),
66736
67657
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66737
67658
  encoderProfile: _enum([
66738
67659
  "high",
66739
67660
  "main",
66740
67661
  "baseline"
66741
67662
  ]).optional(),
66742
- gop: number().optional(),
67663
+ gop: number().optional().meta(readingMeta("none")),
66743
67664
  /**
66744
67665
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66745
67666
  *
@@ -66913,7 +67834,8 @@ var switchCapability = {
66913
67834
  deviceTypes: [
66914
67835
  DeviceType.Switch,
66915
67836
  DeviceType.Siren,
66916
- DeviceType.Light
67837
+ DeviceType.Light,
67838
+ DeviceType.Fan
66917
67839
  ],
66918
67840
  methods: { setState: method(object({
66919
67841
  deviceId: number(),
@@ -66960,12 +67882,7 @@ var switchCapability = {
66960
67882
  label: "On",
66961
67883
  kind: "boolean"
66962
67884
  }
66963
- }] },
66964
- customize: {
66965
- version: 1,
66966
- advanced: "derive",
66967
- targets: []
66968
- }
67885
+ }] }
66969
67886
  };
66970
67887
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66971
67888
  function hasControlCharacter(value) {
@@ -67548,35 +68465,21 @@ var tamperCapability = {
67548
68465
  * entries with `device_class: temperature` and any future native
67549
68466
  * thermometer.
67550
68467
  *
67551
- * Unit normalisation: providers convert to Celsius before storing.
67552
- * The slice value is always Celsius so cross-cap aggregators
67553
- * (climate-control's `currentTemp`, energy analytics) can compose
67554
- * without per-source unit fixups. The canonical display unit (`°C`) is
67555
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67556
- * `sourceInfo`.
68468
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68469
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67557
68470
  *
67558
68471
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67559
68472
  * frozen feed (provider hung) distinct from a "temperature hasn't
67560
68473
  * changed" steady state.
67561
68474
  */
67562
68475
  var TemperatureSensorStatusSchema = object({
67563
- /** Current temperature in Celsius. */
67564
- celsius: number().meta({
67565
- unit: "°C",
68476
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68477
+ celsius: number().nullable().meta(readingMeta("temperature", {
68478
+ precision: 1,
67566
68479
  label: "Temperature"
67567
- }),
68480
+ })),
67568
68481
  /** Ms epoch when the slice was last updated (push or poll). */
67569
- lastFetchedAt: number(),
67570
- /** Live display unit from the upstream source (e.g. HA
67571
- * `attributes.unit_of_measurement`). The UI prefers this over the
67572
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67573
- * Absent → the UI falls back to the role's canonical unit. */
67574
- unit: string().optional(),
67575
- /** Suggested decimal places for numeric display.
67576
- * Populated live from the upstream source when provided (e.g. HA
67577
- * `attributes.suggested_display_precision`). Falls back to
67578
- * auto-formatting when absent. */
67579
- precision: number().int().min(0).max(10).optional()
68482
+ lastFetchedAt: number()
67580
68483
  });
67581
68484
  var temperatureSensorCapability = {
67582
68485
  name: "temperature-sensor",
@@ -67607,12 +68510,7 @@ var temperatureSensorCapability = {
67607
68510
  } } },
67608
68511
  /** Clock fields: written, but excluded from the compare that decides
67609
68512
  * whether persisting is worth a SQLite commit. */
67610
- volatileStateFields: ["lastFetchedAt"],
67611
- customize: {
67612
- version: 1,
67613
- advanced: "derive",
67614
- targets: []
67615
- }
68513
+ volatileStateFields: ["lastFetchedAt"]
67616
68514
  };
67617
68515
  /**
67618
68516
  * toast — system-scoped singleton capability that streams toast
@@ -67895,16 +68793,16 @@ var VacuumControlStatusSchema = object({
67895
68793
  ]),
67896
68794
  /** 0..100 battery percentage. Null when the device has no battery
67897
68795
  * reading. */
67898
- batteryLevel: number().min(0).max(100).nullable(),
68796
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67899
68797
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67900
68798
  * the vacuum has no speed control. */
67901
68799
  fanSpeed: string().nullable(),
67902
68800
  /** Speed tokens the hardware accepts — drives the UI selector. */
67903
68801
  availableFanSpeeds: array(string()),
67904
68802
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67905
- progressPercent: number().min(0).max(100).nullable(),
68803
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67906
68804
  /** Current error code (0 / null = no error). */
67907
- errorCode: number().nullable(),
68805
+ errorCode: number().nullable().meta(readingMeta("none")),
67908
68806
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67909
68807
  errorLabel: string().nullable(),
67910
68808
  /** Ms epoch when the slice was last updated. */
@@ -67982,7 +68880,7 @@ var ValveStatusSchema = object({
67982
68880
  ]),
67983
68881
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67984
68882
  * intermediate position surface. */
67985
- position: number().min(0).max(100).nullable(),
68883
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67986
68884
  /** Ms epoch when the slice was last updated. */
67987
68885
  lastChangedAt: number()
67988
68886
  });
@@ -68090,9 +68988,9 @@ var vibrationCapability = {
68090
68988
  */
68091
68989
  var WaterHeaterStatusSchema = object({
68092
68990
  /** Current measured temperature. Null when not reported. */
68093
- currentTemp: number().nullable(),
68991
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68094
68992
  /** Target temperature setpoint. Null when no setpoint surface. */
68095
- targetTemp: number().nullable(),
68993
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68096
68994
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68097
68995
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68098
68996
  * device reports an unknown state. */
@@ -68103,9 +69001,9 @@ var WaterHeaterStatusSchema = object({
68103
69001
  * has no away surface. */
68104
69002
  away: boolean().nullable(),
68105
69003
  /** HA `min_temp` attribute. Null when not reported. */
68106
- minTemp: number().nullable(),
69004
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68107
69005
  /** HA `max_temp` attribute. Null when not reported. */
68108
- maxTemp: number().nullable(),
69006
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68109
69007
  /** Ms epoch when the slice was last updated. */
68110
69008
  lastChangedAt: number()
68111
69009
  });
@@ -68190,25 +69088,18 @@ var waterHeaterCapability = {
68190
69088
  * Forecast deferred — current conditions only for v1.
68191
69089
  */
68192
69090
  var WeatherStatusSchema = object({
68193
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68194
- * when no condition has been reported yet. */
69091
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68195
69092
  condition: string().nullable(),
68196
- /** Current temperature in the reported unit. Null when not provided. */
68197
- temperature: number().nullable(),
68198
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68199
- temperatureUnit: string().nullable(),
69093
+ /** Current temperature in Celsius. Null when not provided or refused. */
69094
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68200
69095
  /** Relative humidity (0..100). Null when not provided. */
68201
- humidity: number().min(0).max(100).nullable(),
68202
- /** Barometric pressure in the reported unit. Null when not provided. */
68203
- pressure: number().nullable(),
68204
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68205
- pressureUnit: string().nullable(),
68206
- /** Wind speed in the reported unit. Null when not provided. */
68207
- windSpeed: number().nullable(),
68208
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68209
- windSpeedUnit: string().nullable(),
69096
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
69097
+ /** Barometric pressure in hPa. Null when not provided or refused. */
69098
+ pressure: number().nullable().meta(readingMeta("pressure")),
69099
+ /** Wind speed in metres per second. Null when not provided or refused. */
69100
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68210
69101
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68211
- windBearing: number().nullable(),
69102
+ windBearing: number().nullable().meta(readingMeta("angle")),
68212
69103
  /** Ms epoch when the slice was last updated. */
68213
69104
  lastFetchedAt: number()
68214
69105
  });
@@ -68236,12 +69127,12 @@ var weatherCapability = {
68236
69127
  */
68237
69128
  durability: "session",
68238
69129
  /**
68239
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69130
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68240
69131
  */
68241
69132
  history: { fields: {
68242
69133
  temperature: {
68243
69134
  kind: "gauge",
68244
- unitFrom: "temperatureUnit",
69135
+ unit: "°C",
68245
69136
  deadband: .2
68246
69137
  },
68247
69138
  humidity: {
@@ -68251,12 +69142,12 @@ var weatherCapability = {
68251
69142
  },
68252
69143
  pressure: {
68253
69144
  kind: "gauge",
68254
- unitFrom: "pressureUnit",
69145
+ unit: "hPa",
68255
69146
  deadband: .5
68256
69147
  },
68257
69148
  windSpeed: {
68258
69149
  kind: "gauge",
68259
- unitFrom: "windSpeedUnit",
69150
+ unit: "m/s",
68260
69151
  deadband: .5
68261
69152
  }
68262
69153
  } }
@@ -69157,8 +70048,9 @@ method(object({}), array(TrainingBaseModelSchema).readonly(), { auth: "admin" })
69157
70048
  kind: "mutation",
69158
70049
  auth: "admin"
69159
70050
  });
70051
+ DeviceFeature.FanSpeed, DeviceFeature.FanPreset, DeviceFeature.FanDirection, DeviceFeature.FanOscillating, DeviceFeature.CoverPositionable, DeviceFeature.CoverTilt, DeviceFeature.ValvePositionable, DeviceFeature.MediaPlayerVolume, DeviceFeature.ClimateFanMode;
69160
70052
  SOURCE_CAPS.filter((cap) => cap !== "switch" && cap !== "motion");
69161
- 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;
70053
+ 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;
69162
70054
  String.raw`rose\(active(\d+), changed(\d+), (\d+(?:\.\d+)?)\)`;
69163
70055
  DeviceRole.BinarySensor, DeviceRole.MotionSensor;
69164
70056
  new Set(CAP_NAMES);
@@ -69487,6 +70379,9 @@ var DEVICE_LOCAL_STATE_CAPS = {
69487
70379
  featureProbe: featureProbeCapability,
69488
70380
  flood: floodCapability,
69489
70381
  gas: gasCapability,
70382
+ groupActive: groupActiveCapability,
70383
+ groupCommands: groupCommandsCapability,
70384
+ groupStatus: groupStatusCapability,
69490
70385
  heatingValve: heatingValveCapability,
69491
70386
  humidifier: humidifierCapability,
69492
70387
  humiditySensor: humiditySensorCapability,
@@ -69496,6 +70391,7 @@ var DEVICE_LOCAL_STATE_CAPS = {
69496
70391
  lawnMowerControl: lawnMowerControlCapability,
69497
70392
  lockControl: lockControlCapability,
69498
70393
  mediaPlayer: mediaPlayerCapability,
70394
+ meter: meterCapability,
69499
70395
  motion: motionCapability,
69500
70396
  motionTrigger: motionTriggerCapability,
69501
70397
  motionZones: motionZonesCapability,
@@ -70755,7 +71651,7 @@ var BaseDeviceProvider = class extends BaseAddon {
70755
71651
  return toDeviceSummary(device, this.addonId);
70756
71652
  }
70757
71653
  };
70758
- 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;
71654
+ 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;
70759
71655
  new Set(Object.values(DeviceType));
70760
71656
  Object.freeze({
70761
71657
  "accessories.getStatus": {
@@ -70863,6 +71759,13 @@ Object.freeze({
70863
71759
  access: "view",
70864
71760
  role: "admin"
70865
71761
  },
71762
+ "addons.getPackageDocs": {
71763
+ capName: "addons",
71764
+ capScope: "system",
71765
+ addonId: null,
71766
+ access: "view",
71767
+ role: "admin"
71768
+ },
70866
71769
  "addons.getVersions": {
70867
71770
  capName: "addons",
70868
71771
  capScope: "system",
@@ -73971,6 +74874,27 @@ Object.freeze({
73971
74874
  access: "view",
73972
74875
  role: "viewer"
73973
74876
  },
74877
+ "groupActive.getStatus": {
74878
+ capName: "group-active",
74879
+ capScope: "device",
74880
+ addonId: null,
74881
+ access: "view",
74882
+ role: "viewer"
74883
+ },
74884
+ "groupCommands.getStatus": {
74885
+ capName: "group-commands",
74886
+ capScope: "device",
74887
+ addonId: null,
74888
+ access: "view",
74889
+ role: "viewer"
74890
+ },
74891
+ "groupStatus.getStatus": {
74892
+ capName: "group-status",
74893
+ capScope: "device",
74894
+ addonId: null,
74895
+ access: "view",
74896
+ role: "viewer"
74897
+ },
73974
74898
  "heatingValve.getStatus": {
73975
74899
  capName: "heating-valve",
73976
74900
  capScope: "device",
@@ -74923,6 +75847,13 @@ Object.freeze({
74923
75847
  access: "create",
74924
75848
  role: "admin"
74925
75849
  },
75850
+ "meter.getStatus": {
75851
+ capName: "meter",
75852
+ capScope: "device",
75853
+ addonId: null,
75854
+ access: "view",
75855
+ role: "viewer"
75856
+ },
74926
75857
  "metricsProvider.collectSnapshot": {
74927
75858
  capName: "metrics-provider",
74928
75859
  capScope: "system",
@@ -83021,7 +83952,39 @@ var APPLIANCE_CAP_VOCABULARY = {
83021
83952
  phrase: t("When {device} finishes", "Quando {device} finisce"),
83022
83953
  icon: "square"
83023
83954
  }
83024
- }
83955
+ },
83956
+ overrides: { byType: {
83957
+ [DeviceType.Washer]: {
83958
+ state: { running: {
83959
+ whenTrue: t("When the washing machine starts", "Quando la lavatrice inizia"),
83960
+ whenFalse: t("When the washing machine has finished", "Quando la lavatrice ha finito")
83961
+ } },
83962
+ events: {
83963
+ onStarted: { phrase: t("When the washing machine starts", "Quando la lavatrice inizia") },
83964
+ onFinished: { phrase: t("When the washing machine has finished", "Quando la lavatrice ha finito") }
83965
+ }
83966
+ },
83967
+ [DeviceType.Dryer]: {
83968
+ state: { running: {
83969
+ whenTrue: t("When the dryer starts", "Quando l'asciugatrice inizia"),
83970
+ whenFalse: t("When the dryer has finished", "Quando l'asciugatrice ha finito")
83971
+ } },
83972
+ events: {
83973
+ onStarted: { phrase: t("When the dryer starts", "Quando l'asciugatrice inizia") },
83974
+ onFinished: { phrase: t("When the dryer has finished", "Quando l'asciugatrice ha finito") }
83975
+ }
83976
+ },
83977
+ [DeviceType.Dishwasher]: {
83978
+ state: { running: {
83979
+ whenTrue: t("When the dishwasher starts", "Quando la lavastoviglie inizia"),
83980
+ whenFalse: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito")
83981
+ } },
83982
+ events: {
83983
+ onStarted: { phrase: t("When the dishwasher starts", "Quando la lavastoviglie inizia") },
83984
+ onFinished: { phrase: t("When the dishwasher has finished", "Quando la lavastoviglie ha finito") }
83985
+ }
83986
+ }
83987
+ } }
83025
83988
  },
83026
83989
  update: {
83027
83990
  scope: "device",
@@ -83793,6 +84756,41 @@ var SENSOR_CAP_VOCABULARY = {
83793
84756
  icon: "flame"
83794
84757
  }]
83795
84758
  },
84759
+ "group-active": {
84760
+ scope: "device",
84761
+ noun: t("Group state", "Stato del gruppo")
84762
+ },
84763
+ "group-commands": {
84764
+ scope: "device",
84765
+ noun: t("Group command", "Comando di gruppo")
84766
+ },
84767
+ "group-status": {
84768
+ scope: "device",
84769
+ noun: t("Group", "Gruppo"),
84770
+ state: [
84771
+ {
84772
+ kind: "numeric",
84773
+ field: "included",
84774
+ quantity: t("the members reporting", "i membri che rispondono"),
84775
+ label: t("Reporting", "Rispondono"),
84776
+ icon: "users"
84777
+ },
84778
+ {
84779
+ kind: "numeric",
84780
+ field: "excluded",
84781
+ quantity: t("the members left out", "i membri esclusi"),
84782
+ label: t("Excluded", "Esclusi"),
84783
+ icon: "users"
84784
+ },
84785
+ {
84786
+ kind: "numeric",
84787
+ field: "total",
84788
+ quantity: t("the member count", "il numero di membri"),
84789
+ label: t("Members", "Membri"),
84790
+ icon: "sigma"
84791
+ }
84792
+ ]
84793
+ },
83796
84794
  "humidity-sensor": {
83797
84795
  scope: "device",
83798
84796
  noun: t("Humidity", "Umidità"),
@@ -83818,6 +84816,17 @@ var SENSOR_CAP_VOCABULARY = {
83818
84816
  }],
83819
84817
  events: { onMotionChanged: { notOperatorFacing: "the `detected` state trigger phrases both edges, from the mirror that survives a dropped event (D49)" } }
83820
84818
  },
84819
+ meter: {
84820
+ scope: "device",
84821
+ noun: t("Meter", "Contatore"),
84822
+ state: [{
84823
+ kind: "numeric",
84824
+ field: "value",
84825
+ quantity: t("the total", "il totale"),
84826
+ label: t("Total", "Totale"),
84827
+ icon: "gauge"
84828
+ }]
84829
+ },
83821
84830
  "numeric-sensor": {
83822
84831
  scope: "device",
83823
84832
  noun: t("Sensor", "Sensore"),
@@ -83826,7 +84835,6 @@ var SENSOR_CAP_VOCABULARY = {
83826
84835
  field: "value",
83827
84836
  quantity: t("the reading", "il valore"),
83828
84837
  label: t("Reading", "Valore"),
83829
- unitField: "unit",
83830
84838
  icon: "gauge"
83831
84839
  }]
83832
84840
  },
@@ -83962,7 +84970,7 @@ var SENSOR_CAP_VOCABULARY = {
83962
84970
  field: "temperature",
83963
84971
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83964
84972
  label: t("Temperature", "Temperatura"),
83965
- unitField: "temperatureUnit",
84973
+ unit: "°C",
83966
84974
  icon: "thermometer"
83967
84975
  },
83968
84976
  {
@@ -83978,7 +84986,7 @@ var SENSOR_CAP_VOCABULARY = {
83978
84986
  field: "windSpeed",
83979
84987
  quantity: t("the wind speed", "la velocità del vento"),
83980
84988
  label: t("Wind", "Vento"),
83981
- unitField: "windSpeedUnit",
84989
+ unit: "m/s",
83982
84990
  icon: "wind"
83983
84991
  },
83984
84992
  {
@@ -83986,7 +84994,7 @@ var SENSOR_CAP_VOCABULARY = {
83986
84994
  field: "pressure",
83987
84995
  quantity: t("the air pressure", "la pressione atmosferica"),
83988
84996
  label: t("Pressure", "Pressione"),
83989
- unitField: "pressureUnit",
84997
+ unit: "hPa",
83990
84998
  icon: "gauge"
83991
84999
  }
83992
85000
  ]
@@ -84291,8 +85299,47 @@ function projectAction(cap, action) {
84291
85299
  };
84292
85300
  }
84293
85301
  Object.entries(CAP_VOCABULARY_BY_NAME).flatMap(([cap, entry]) => entry.scope === "device" ? (entry.actions ?? []).map((a) => projectAction(cap, a)) : []);
84294
- 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;
85302
+ 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;
84295
85303
  DeviceFeature.BatteryOperated;
85304
+ DeviceType.Washer, DeviceType.Dryer, DeviceType.Dishwasher;
85305
+ 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.";
85306
+ 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.";
85307
+ var ALEXA_REASON = "Alexa has no laundry category in this export, and neither power nor running has an Alexa interface.";
85308
+ var GOOGLE_REASON = "Google WASHER, DRYER and DISHWASHER require RunCycle. A power threshold is not that trait, and this export does not publish RunCycle.";
85309
+ 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.";
85310
+ function withheld(reason) {
85311
+ return {
85312
+ [DeviceType.Washer]: {
85313
+ exported: false,
85314
+ reason
85315
+ },
85316
+ [DeviceType.Dryer]: {
85317
+ exported: false,
85318
+ reason
85319
+ },
85320
+ [DeviceType.Dishwasher]: {
85321
+ exported: false,
85322
+ reason
85323
+ }
85324
+ };
85325
+ }
85326
+ function offered(received) {
85327
+ return {
85328
+ [DeviceType.Washer]: {
85329
+ exported: true,
85330
+ as: received
85331
+ },
85332
+ [DeviceType.Dryer]: {
85333
+ exported: true,
85334
+ as: received
85335
+ },
85336
+ [DeviceType.Dishwasher]: {
85337
+ exported: true,
85338
+ as: received
85339
+ }
85340
+ };
85341
+ }
85342
+ offered(HA_AS), withheld(HOMEKIT_REASON), withheld(ALEXA_REASON), withheld(GOOGLE_REASON), withheld(MATTER_REASON);
84296
85343
  /** Reject after `ms`; always clears its own timer. */
84297
85344
  async function withTimeout$1(promise, ms, label) {
84298
85345
  let timer;
@@ -85325,6 +86372,21 @@ Object.freeze({
85325
86372
  form: "single",
85326
86373
  optional: false
85327
86374
  }],
86375
+ "groupActive.getStatus": [{
86376
+ name: "deviceId",
86377
+ form: "single",
86378
+ optional: false
86379
+ }],
86380
+ "groupCommands.getStatus": [{
86381
+ name: "deviceId",
86382
+ form: "single",
86383
+ optional: false
86384
+ }],
86385
+ "groupStatus.getStatus": [{
86386
+ name: "deviceId",
86387
+ form: "single",
86388
+ optional: false
86389
+ }],
85328
86390
  "heatingValve.getStatus": [{
85329
86391
  name: "deviceId",
85330
86392
  form: "single",
@@ -85525,6 +86587,11 @@ Object.freeze({
85525
86587
  form: "single",
85526
86588
  optional: false
85527
86589
  }],
86590
+ "meter.getStatus": [{
86591
+ name: "deviceId",
86592
+ form: "single",
86593
+ optional: false
86594
+ }],
85528
86595
  "motion.getStatus": [{
85529
86596
  name: "deviceId",
85530
86597
  form: "single",
@@ -87132,7 +88199,6 @@ Object.freeze({
87132
88199
  "network-access": "ingress",
87133
88200
  "smtp-provider": "email"
87134
88201
  });
87135
- DeviceType.Camera;
87136
88202
  /**
87137
88203
  * A saved Customize assignment (D936). The schema is the type: callers use
87138
88204
  * `z.infer`, they do not restate the shape.
@@ -87309,6 +88375,8 @@ new Map(Object.entries({
87309
88375
  "detection-blind": "device-events",
87310
88376
  "motion-noisy": "device-events",
87311
88377
  "motion-calm": "device-events",
88378
+ "package-llm-silent": "device-events",
88379
+ "package-llm-answered": "device-events",
87312
88380
  "recording-stalled": "device-events",
87313
88381
  "recording-healthy": "device-events",
87314
88382
  "recording-degraded": "device-events",
@@ -87392,6 +88460,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
87392
88460
  label: "Motion without a subject",
87393
88461
  kinds: ["motion-noisy", "motion-calm"]
87394
88462
  },
88463
+ {
88464
+ id: "package-llm",
88465
+ family: "device-events",
88466
+ label: "Package confirm",
88467
+ kinds: ["package-llm-silent", "package-llm-answered"]
88468
+ },
87395
88469
  {
87396
88470
  id: "recording",
87397
88471
  family: "device-events",
@@ -87573,6 +88647,8 @@ var DEVICE_EVENTS = [
87573
88647
  "detection-blind",
87574
88648
  "motion-noisy",
87575
88649
  "motion-calm",
88650
+ "package-llm-silent",
88651
+ "package-llm-answered",
87576
88652
  "recording-stalled",
87577
88653
  "recording-healthy",
87578
88654
  "recording-degraded",
@@ -87841,6 +88917,15 @@ new Map([
87841
88917
  snapshotLabel: "Cameras with constant motion now",
87842
88918
  snapshotSection: null
87843
88919
  },
88920
+ {
88921
+ id: "package-llm-confirm",
88922
+ family: "device-events",
88923
+ label: "Package confirm (not answering / answering again)",
88924
+ bad: ["package-llm-silent"],
88925
+ recovery: ["package-llm-answered"],
88926
+ snapshotLabel: "Cameras whose package confirm is not answering",
88927
+ snapshotSection: null
88928
+ },
87844
88929
  {
87845
88930
  id: "recording-health",
87846
88931
  family: "device-events",