@camstack/addon-provider-rademacher 0.2.146 → 0.2.147

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.mjs CHANGED
@@ -983,7 +983,7 @@ var Rademacher = class {
983
983
  }
984
984
  };
985
985
  //#endregion
986
- //#region ../types/dist/event-category-DHXxB8t9.mjs
986
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
987
987
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
988
988
  EventCategory["SystemBoot"] = "system.boot";
989
989
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -1309,6 +1309,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
1309
1309
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
1310
1310
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
1311
1311
  /**
1312
+ * A camera's package LLM confirm has failed closed three times in a row
1313
+ * (D931, 2026-10-26). One edge per camera until
1314
+ * {@link PipelinePackageLlmAnswered}.
1315
+ */
1316
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
1317
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
1318
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
1319
+ /**
1312
1320
  * Per-camera pipeline config was mutated by the orchestrator
1313
1321
  * (a settings change via `setAgentInferenceDevices` /
1314
1322
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -6418,7 +6426,7 @@ var ZodIssueCode = {
6418
6426
  var ZodFirstPartyTypeKind;
6419
6427
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
6420
6428
  //#endregion
6421
- //#region ../types/dist/sleep-BErAxhul.mjs
6429
+ //#region ../types/dist/sleep-bykKrTgu.mjs
6422
6430
  /**
6423
6431
  * The audio chunk plane's byte format, and the ONE expansion from a coded
6424
6432
  * window to float samples (D455).
@@ -8972,6 +8980,99 @@ var RecordKindDeclarationSchema = object({
8972
8980
  }).strict();
8973
8981
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8974
8982
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8983
+ var READING_QUANTITIES = [
8984
+ "temperature",
8985
+ "pressure",
8986
+ "speed",
8987
+ "precipitation",
8988
+ "precipitation-rate",
8989
+ "length",
8990
+ "area",
8991
+ "illuminance",
8992
+ "irradiance",
8993
+ "power",
8994
+ "energy",
8995
+ "voltage",
8996
+ "current",
8997
+ "apparent-power",
8998
+ "humidity",
8999
+ "percentage",
9000
+ "concentration-volume",
9001
+ "concentration-mass",
9002
+ "concentration-ppb",
9003
+ "volume",
9004
+ "duration",
9005
+ "duration-ms",
9006
+ "information",
9007
+ "sound-level",
9008
+ "signal-level",
9009
+ "colour-temperature",
9010
+ "angle",
9011
+ "bitrate",
9012
+ "rate",
9013
+ "mass",
9014
+ "index",
9015
+ "ratio",
9016
+ "count",
9017
+ "none",
9018
+ "device"
9019
+ ];
9020
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
9021
+ new Set(READING_QUANTITIES);
9022
+ /**
9023
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
9024
+ * (or an index / ratio / count). `device` means the cap has no single unit.
9025
+ */
9026
+ var QUANTITY_BASE = {
9027
+ temperature: "°C",
9028
+ pressure: "hPa",
9029
+ speed: "m/s",
9030
+ precipitation: "mm",
9031
+ "precipitation-rate": "mm/h",
9032
+ length: "m",
9033
+ area: "m²",
9034
+ illuminance: "lx",
9035
+ irradiance: "W/m²",
9036
+ power: "W",
9037
+ energy: "kWh",
9038
+ voltage: "V",
9039
+ current: "A",
9040
+ "apparent-power": "VA",
9041
+ humidity: "%",
9042
+ percentage: "%",
9043
+ "concentration-volume": "ppm",
9044
+ "concentration-mass": "µg/m³",
9045
+ "concentration-ppb": "ppb",
9046
+ volume: "L",
9047
+ duration: "s",
9048
+ "duration-ms": "ms",
9049
+ information: "B",
9050
+ "sound-level": "dBFS",
9051
+ "signal-level": "dBm",
9052
+ "colour-temperature": "mired",
9053
+ angle: "°",
9054
+ bitrate: "kbps",
9055
+ rate: "fps",
9056
+ mass: "kg",
9057
+ index: "none",
9058
+ ratio: "none",
9059
+ count: "none",
9060
+ none: "none",
9061
+ device: "device"
9062
+ };
9063
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
9064
+ function baseUnitOf(quantity) {
9065
+ const symbol = QUANTITY_BASE[quantity];
9066
+ if (symbol === "none" || symbol === "device") return void 0;
9067
+ return symbol;
9068
+ }
9069
+ function readingMeta(quantity, extras) {
9070
+ return {
9071
+ quantity,
9072
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
9073
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
9074
+ };
9075
+ }
8975
9076
  /** Shared Zod schemas used across streaming capabilities. */
8976
9077
  var CamProfileSchema = _enum([
8977
9078
  "high",
@@ -8999,8 +9100,8 @@ var CamStreamKindSchema = _enum([
8999
9100
  "derived"
9000
9101
  ]);
9001
9102
  var CamStreamResolutionSchema = object({
9002
- width: number().int().positive(),
9003
- height: number().int().positive()
9103
+ width: number().int().positive().meta(readingMeta("none")),
9104
+ height: number().int().positive().meta(readingMeta("none"))
9004
9105
  });
9005
9106
  var CameraStreamSchema = object({
9006
9107
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -9518,6 +9619,8 @@ var CAP_NAMES = Object.freeze([
9518
9619
  "filesystem-browse",
9519
9620
  "flood",
9520
9621
  "gas",
9622
+ "group-commands",
9623
+ "group-status",
9521
9624
  "heating-valve",
9522
9625
  "humidifier",
9523
9626
  "humidity-sensor",
@@ -9647,14 +9750,6 @@ Object.freeze({
9647
9750
  {
9648
9751
  "path": "aqi",
9649
9752
  "valueType": "number"
9650
- },
9651
- {
9652
- "path": "unit",
9653
- "valueType": "string"
9654
- },
9655
- {
9656
- "path": "precision",
9657
- "valueType": "number"
9658
9753
  }
9659
9754
  ],
9660
9755
  "alarm-panel": [
@@ -9690,20 +9785,10 @@ Object.freeze({
9690
9785
  "valueType": "boolean"
9691
9786
  }
9692
9787
  ],
9693
- "ambient-light-sensor": [
9694
- {
9695
- "path": "lux",
9696
- "valueType": "number"
9697
- },
9698
- {
9699
- "path": "unit",
9700
- "valueType": "string"
9701
- },
9702
- {
9703
- "path": "precision",
9704
- "valueType": "number"
9705
- }
9706
- ],
9788
+ "ambient-light-sensor": [{
9789
+ "path": "lux",
9790
+ "valueType": "number"
9791
+ }],
9707
9792
  "audio-metrics": [
9708
9793
  {
9709
9794
  "path": "ts",
@@ -9783,6 +9868,10 @@ Object.freeze({
9783
9868
  "path": "lastUpdated",
9784
9869
  "valueType": "number"
9785
9870
  },
9871
+ {
9872
+ "path": "volts",
9873
+ "valueType": "number"
9874
+ },
9786
9875
  {
9787
9876
  "path": "lastContactAt",
9788
9877
  "valueType": "number"
@@ -10025,10 +10114,6 @@ Object.freeze({
10025
10114
  "datetime"
10026
10115
  ]
10027
10116
  },
10028
- {
10029
- "path": "unit",
10030
- "valueType": "string"
10031
- },
10032
10117
  {
10033
10118
  "path": "min",
10034
10119
  "valueType": "number"
@@ -10040,19 +10125,6 @@ Object.freeze({
10040
10125
  {
10041
10126
  "path": "step",
10042
10127
  "valueType": "number"
10043
- },
10044
- {
10045
- "path": "precision",
10046
- "valueType": "number"
10047
- },
10048
- {
10049
- "path": "format",
10050
- "valueType": "enum",
10051
- "enumValues": [
10052
- "date",
10053
- "time",
10054
- "datetime"
10055
- ]
10056
10128
  }
10057
10129
  ],
10058
10130
  "cover": [
@@ -10293,13 +10365,8 @@ Object.freeze({
10293
10365
  "path": "value",
10294
10366
  "valueType": "string"
10295
10367
  }, {
10296
- "path": "format",
10297
- "valueType": "enum",
10298
- "enumValues": [
10299
- "date",
10300
- "time",
10301
- "datetime"
10302
- ]
10368
+ "path": "timestamp",
10369
+ "valueType": "boolean"
10303
10370
  }],
10304
10371
  "event-emitter": [
10305
10372
  {
@@ -10372,6 +10439,20 @@ Object.freeze({
10372
10439
  "path": "detected",
10373
10440
  "valueType": "boolean"
10374
10441
  }],
10442
+ "group-status": [
10443
+ {
10444
+ "path": "included",
10445
+ "valueType": "number"
10446
+ },
10447
+ {
10448
+ "path": "excluded",
10449
+ "valueType": "number"
10450
+ },
10451
+ {
10452
+ "path": "total",
10453
+ "valueType": "number"
10454
+ }
10455
+ ],
10375
10456
  "heating-valve": [{
10376
10457
  "path": "position",
10377
10458
  "valueType": "number"
@@ -10406,20 +10487,10 @@ Object.freeze({
10406
10487
  "valueType": "number"
10407
10488
  }
10408
10489
  ],
10409
- "humidity-sensor": [
10410
- {
10411
- "path": "percent",
10412
- "valueType": "number"
10413
- },
10414
- {
10415
- "path": "unit",
10416
- "valueType": "string"
10417
- },
10418
- {
10419
- "path": "precision",
10420
- "valueType": "number"
10421
- }
10422
- ],
10490
+ "humidity-sensor": [{
10491
+ "path": "percent",
10492
+ "valueType": "number"
10493
+ }],
10423
10494
  "image": [{
10424
10495
  "path": "url",
10425
10496
  "valueType": "string"
@@ -10758,20 +10829,10 @@ Object.freeze({
10758
10829
  "valueType": "boolean"
10759
10830
  }
10760
10831
  ],
10761
- "numeric-sensor": [
10762
- {
10763
- "path": "value",
10764
- "valueType": "number"
10765
- },
10766
- {
10767
- "path": "unit",
10768
- "valueType": "string"
10769
- },
10770
- {
10771
- "path": "precision",
10772
- "valueType": "number"
10773
- }
10774
- ],
10832
+ "numeric-sensor": [{
10833
+ "path": "value",
10834
+ "valueType": "number"
10835
+ }],
10775
10836
  "pet-feeder": [
10776
10837
  {
10777
10838
  "path": "foodLevel",
@@ -10863,14 +10924,6 @@ Object.freeze({
10863
10924
  {
10864
10925
  "path": "powerFactor",
10865
10926
  "valueType": "number"
10866
- },
10867
- {
10868
- "path": "unit",
10869
- "valueType": "string"
10870
- },
10871
- {
10872
- "path": "precision",
10873
- "valueType": "number"
10874
10927
  }
10875
10928
  ],
10876
10929
  "presence": [
@@ -10899,20 +10952,10 @@ Object.freeze({
10899
10952
  "valueType": "number"
10900
10953
  }
10901
10954
  ],
10902
- "pressure-sensor": [
10903
- {
10904
- "path": "hpa",
10905
- "valueType": "number"
10906
- },
10907
- {
10908
- "path": "unit",
10909
- "valueType": "string"
10910
- },
10911
- {
10912
- "path": "precision",
10913
- "valueType": "number"
10914
- }
10915
- ],
10955
+ "pressure-sensor": [{
10956
+ "path": "hpa",
10957
+ "valueType": "number"
10958
+ }],
10916
10959
  "privacy-mask": [{
10917
10960
  "path": "enabled",
10918
10961
  "valueType": "boolean"
@@ -11223,20 +11266,10 @@ Object.freeze({
11223
11266
  "path": "tampered",
11224
11267
  "valueType": "boolean"
11225
11268
  }],
11226
- "temperature-sensor": [
11227
- {
11228
- "path": "celsius",
11229
- "valueType": "number"
11230
- },
11231
- {
11232
- "path": "unit",
11233
- "valueType": "string"
11234
- },
11235
- {
11236
- "path": "precision",
11237
- "valueType": "number"
11238
- }
11239
- ],
11269
+ "temperature-sensor": [{
11270
+ "path": "celsius",
11271
+ "valueType": "number"
11272
+ }],
11240
11273
  "update": [
11241
11274
  {
11242
11275
  "path": "currentVersion",
@@ -11347,10 +11380,6 @@ Object.freeze({
11347
11380
  "path": "temperature",
11348
11381
  "valueType": "number"
11349
11382
  },
11350
- {
11351
- "path": "temperatureUnit",
11352
- "valueType": "string"
11353
- },
11354
11383
  {
11355
11384
  "path": "humidity",
11356
11385
  "valueType": "number"
@@ -11359,18 +11388,10 @@ Object.freeze({
11359
11388
  "path": "pressure",
11360
11389
  "valueType": "number"
11361
11390
  },
11362
- {
11363
- "path": "pressureUnit",
11364
- "valueType": "string"
11365
- },
11366
11391
  {
11367
11392
  "path": "windSpeed",
11368
11393
  "valueType": "number"
11369
11394
  },
11370
- {
11371
- "path": "windSpeedUnit",
11372
- "valueType": "string"
11373
- },
11374
11395
  {
11375
11396
  "path": "windBearing",
11376
11397
  "valueType": "number"
@@ -11773,7 +11794,7 @@ function errMsg(err) {
11773
11794
  return String(err);
11774
11795
  }
11775
11796
  //#endregion
11776
- //#region ../types/dist/composition-kEl9mevu.mjs
11797
+ //#region ../types/dist/composition-OhpuespA.mjs
11777
11798
  /**
11778
11799
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11779
11800
  *
@@ -12528,15 +12549,74 @@ function validateExpressionSource(src) {
12528
12549
  }
12529
12550
  return null;
12530
12551
  }
12531
- /** A composed device's stableId is this prefix plus the block id. */
12532
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12533
- /** Longest snippet a `code` source may carry (slice 4). */
12534
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
12552
+ /**
12553
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
12554
+ * id. Split out so helper schemas can name it without a cycle through
12555
+ * `composition.ts` (D659).
12556
+ */
12535
12557
  var CompositionSourceRefSchema = object({
12536
12558
  /** The owning addon; `stableId` is unique only within it. */
12537
12559
  addonId: string().min(1),
12538
12560
  stableId: string().min(1)
12539
12561
  });
12562
+ var GROUP_TEMPLATE_IDS = [
12563
+ "temperature",
12564
+ "humidity",
12565
+ "pressure",
12566
+ "illuminance",
12567
+ "power",
12568
+ "energy",
12569
+ "battery",
12570
+ "numeric",
12571
+ "motion",
12572
+ "contact",
12573
+ "leak",
12574
+ "smoke",
12575
+ "occupancy",
12576
+ "binary",
12577
+ "switch",
12578
+ "light"
12579
+ ];
12580
+ var NUMERIC_AGGREGATES = [
12581
+ "mean",
12582
+ "median",
12583
+ "min",
12584
+ "max",
12585
+ "sum"
12586
+ ];
12587
+ var BINARY_AGGREGATES = ["any", "all"];
12588
+ /** How an actuator group decides it is on. Unknown members never count as off. */
12589
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
12590
+ var AGGREGATE_REDUCERS = [
12591
+ "mean",
12592
+ "median",
12593
+ "min",
12594
+ "max",
12595
+ "sum",
12596
+ "any",
12597
+ "all"
12598
+ ];
12599
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
12600
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
12601
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
12602
+ /** One member. The field is stored so occupancy can name motion or binary. */
12603
+ var HelperMemberSchema = object({
12604
+ source: CompositionSourceRefSchema,
12605
+ cap: string().min(1),
12606
+ fieldPath: string().min(1)
12607
+ });
12608
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
12609
+ kind: literal("group"),
12610
+ templateId: GroupTemplateIdSchema,
12611
+ aggregate: AggregateReducerSchema,
12612
+ members: array(HelperMemberSchema).min(1).max(64),
12613
+ /** Actuator groups only. Readings leave this unset. */
12614
+ active: ActiveCriterionSchema.optional()
12615
+ })]);
12616
+ /** A composed device's stableId is this prefix plus the block id. */
12617
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
12618
+ /** Longest snippet a `code` source may carry (slice 4). */
12619
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
12540
12620
  /** One field of one capability of one source device. */
12541
12621
  var CompositionFieldReadSchema = object({
12542
12622
  source: CompositionSourceRefSchema,
@@ -12580,7 +12660,33 @@ var CompositionCodeSourceSchema = object({
12580
12660
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
12581
12661
  CompositionFromSourceSchema,
12582
12662
  CompositionExpressionSourceSchema,
12583
- CompositionCodeSourceSchema
12663
+ CompositionCodeSourceSchema,
12664
+ object({
12665
+ kind: literal("aggregate"),
12666
+ reducer: _enum([
12667
+ "mean",
12668
+ "median",
12669
+ "min",
12670
+ "max",
12671
+ "sum",
12672
+ "any",
12673
+ "all"
12674
+ ]),
12675
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12676
+ /** `value` is the reading; the others are the member counts. */
12677
+ publish: _enum([
12678
+ "value",
12679
+ "included",
12680
+ "excluded",
12681
+ "total"
12682
+ ]),
12683
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12684
+ empty: _enum([
12685
+ "null",
12686
+ "omit",
12687
+ "offline"
12688
+ ])
12689
+ })
12584
12690
  ]);
12585
12691
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
12586
12692
  kind: literal("forward"),
@@ -12626,7 +12732,13 @@ var CompositionSchema = object({
12626
12732
  kind: literal("existing"),
12627
12733
  device: CompositionSourceRefSchema
12628
12734
  })]),
12629
- features: array(CompositionFeatureSchema).min(1).max(16)
12735
+ features: array(CompositionFeatureSchema).min(1).max(16),
12736
+ /**
12737
+ * Present when the block is a helper template. Absent on a free
12738
+ * composition (the room example, device 4603). The editor reads this,
12739
+ * never the compiled features.
12740
+ */
12741
+ helper: HelperSpecSchema.optional()
12630
12742
  }).superRefine((composition, ctx) => {
12631
12743
  const seen = /* @__PURE__ */ new Set();
12632
12744
  composition.features.forEach((feature, index) => {
@@ -18834,7 +18946,11 @@ var CompositionProblemSchema = object({
18834
18946
  "item-field-reserved",
18835
18947
  "target-missing",
18836
18948
  "target-unreadable",
18837
- "not-replaceable"
18949
+ "not-replaceable",
18950
+ "native-capability-replace",
18951
+ "mixed-quantity",
18952
+ "helper-mismatch",
18953
+ "member-constraint"
18838
18954
  ]),
18839
18955
  path: string(),
18840
18956
  message: string()
@@ -18855,7 +18971,8 @@ var CompositionUnavailableModeSchema = _enum([
18855
18971
  "null",
18856
18972
  "offline",
18857
18973
  "unknown",
18858
- "release"
18974
+ "release",
18975
+ "omit"
18859
18976
  ]);
18860
18977
  /**
18861
18978
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18892,9 +19009,11 @@ var CompositionFieldPlanSchema = object({
18892
19009
  reads: array(CompositionFieldReadSchema)
18893
19010
  });
18894
19011
  /**
18895
- * What a feature does to its target (D663): `new` builds a capability on a new
18896
- * device; on an existing device it `add`s a capability the device lacks, or
18897
- * `replace`s named fields of one it has natively.
19012
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
19013
+ * new device; on an existing device it `add`s a capability the device lacks.
19014
+ * Replacing a capability the device already serves is refused
19015
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
19016
+ * wire for a claim the hub already holds.
18898
19017
  */
18899
19018
  var CompositionFeatureModeSchema = _enum([
18900
19019
  "new",
@@ -19067,6 +19186,275 @@ Object.freeze({
19067
19186
  result: ["number", "null"]
19068
19187
  }
19069
19188
  });
19189
+ /**
19190
+ * One catalog row per base reading a group helper can build (D944 §3).
19191
+ * Data, not a form default written into the block.
19192
+ */
19193
+ var NOW = {
19194
+ fieldPath: "lastFetchedAt",
19195
+ expr: "now"
19196
+ };
19197
+ var CHANGED = {
19198
+ fieldPath: "lastChangedAt",
19199
+ expr: "now"
19200
+ };
19201
+ function numeric(input) {
19202
+ return {
19203
+ ...input,
19204
+ family: "numeric",
19205
+ deviceType: DeviceType.Sensor,
19206
+ memberDeviceType: DeviceType.Sensor,
19207
+ commandCaps: [],
19208
+ baseUnit: baseUnitOf(input.quantity),
19209
+ aggregates: NUMERIC_AGGREGATES,
19210
+ accept: [{
19211
+ cap: input.outputCap,
19212
+ fieldPath: input.outputField
19213
+ }]
19214
+ };
19215
+ }
19216
+ function binary(input) {
19217
+ return {
19218
+ ...input,
19219
+ family: "binary",
19220
+ deviceType: DeviceType.Sensor,
19221
+ memberDeviceType: DeviceType.Sensor,
19222
+ commandCaps: [],
19223
+ quantity: "none",
19224
+ baseUnit: void 0,
19225
+ aggregates: BINARY_AGGREGATES,
19226
+ empty: "offline"
19227
+ };
19228
+ }
19229
+ var GROUP_TEMPLATES = [
19230
+ numeric({
19231
+ id: "temperature",
19232
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
19233
+ role: DeviceRole.TemperatureSensor,
19234
+ outputCap: "temperature-sensor",
19235
+ outputField: "celsius",
19236
+ quantity: "temperature",
19237
+ defaultAggregate: "mean",
19238
+ empty: "null",
19239
+ companions: [NOW]
19240
+ }),
19241
+ numeric({
19242
+ id: "humidity",
19243
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
19244
+ role: DeviceRole.HumiditySensor,
19245
+ outputCap: "humidity-sensor",
19246
+ outputField: "percent",
19247
+ quantity: "humidity",
19248
+ defaultAggregate: "mean",
19249
+ empty: "null",
19250
+ companions: [NOW]
19251
+ }),
19252
+ numeric({
19253
+ id: "pressure",
19254
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
19255
+ role: DeviceRole.PressureSensor,
19256
+ outputCap: "pressure-sensor",
19257
+ outputField: "hpa",
19258
+ quantity: "pressure",
19259
+ defaultAggregate: "mean",
19260
+ empty: "null",
19261
+ companions: [NOW]
19262
+ }),
19263
+ numeric({
19264
+ id: "illuminance",
19265
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
19266
+ role: DeviceRole.AmbientLightSensor,
19267
+ outputCap: "ambient-light-sensor",
19268
+ outputField: "lux",
19269
+ quantity: "illuminance",
19270
+ defaultAggregate: "mean",
19271
+ empty: "null",
19272
+ companions: [NOW]
19273
+ }),
19274
+ numeric({
19275
+ id: "power",
19276
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
19277
+ role: DeviceRole.PowerSensor,
19278
+ outputCap: "power-meter",
19279
+ outputField: "watts",
19280
+ quantity: "power",
19281
+ defaultAggregate: "sum",
19282
+ empty: "omit",
19283
+ companions: [NOW]
19284
+ }),
19285
+ numeric({
19286
+ id: "energy",
19287
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
19288
+ role: DeviceRole.EnergySensor,
19289
+ outputCap: "power-meter",
19290
+ outputField: "kwhTotal",
19291
+ quantity: "energy",
19292
+ defaultAggregate: "sum",
19293
+ empty: "omit",
19294
+ companions: [NOW]
19295
+ }),
19296
+ numeric({
19297
+ id: "battery",
19298
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
19299
+ role: DeviceRole.BatterySensor,
19300
+ outputCap: "battery",
19301
+ outputField: "percentage",
19302
+ quantity: "percentage",
19303
+ defaultAggregate: "mean",
19304
+ empty: "null",
19305
+ companions: [
19306
+ {
19307
+ fieldPath: "charging",
19308
+ expr: "\"unknown\""
19309
+ },
19310
+ {
19311
+ fieldPath: "sleeping",
19312
+ expr: "false"
19313
+ },
19314
+ {
19315
+ fieldPath: "lastUpdated",
19316
+ expr: "now"
19317
+ }
19318
+ ]
19319
+ }),
19320
+ numeric({
19321
+ id: "numeric",
19322
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
19323
+ role: DeviceRole.NumericSensor,
19324
+ outputCap: "numeric-sensor",
19325
+ outputField: "value",
19326
+ quantity: "device",
19327
+ defaultAggregate: "mean",
19328
+ empty: "null",
19329
+ companions: [NOW]
19330
+ }),
19331
+ binary({
19332
+ id: "motion",
19333
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
19334
+ role: DeviceRole.MotionSensor,
19335
+ outputCap: "motion",
19336
+ outputField: "detected",
19337
+ defaultAggregate: "any",
19338
+ accept: [{
19339
+ cap: "motion",
19340
+ fieldPath: "detected"
19341
+ }],
19342
+ companions: [{
19343
+ fieldPath: "lastDetectedAt",
19344
+ expr: "null"
19345
+ }, {
19346
+ fieldPath: "autoClearAfterMs",
19347
+ expr: "null"
19348
+ }]
19349
+ }),
19350
+ binary({
19351
+ id: "contact",
19352
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
19353
+ role: DeviceRole.ContactSensor,
19354
+ outputCap: "contact",
19355
+ outputField: "entryOpen",
19356
+ defaultAggregate: "any",
19357
+ accept: [{
19358
+ cap: "contact",
19359
+ fieldPath: "entryOpen"
19360
+ }],
19361
+ companions: [CHANGED]
19362
+ }),
19363
+ binary({
19364
+ id: "leak",
19365
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
19366
+ role: DeviceRole.LeakSensor,
19367
+ outputCap: "flood",
19368
+ outputField: "flooded",
19369
+ defaultAggregate: "any",
19370
+ accept: [{
19371
+ cap: "flood",
19372
+ fieldPath: "flooded"
19373
+ }],
19374
+ companions: [CHANGED]
19375
+ }),
19376
+ binary({
19377
+ id: "smoke",
19378
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
19379
+ role: DeviceRole.SmokeSensor,
19380
+ outputCap: "smoke",
19381
+ outputField: "detected",
19382
+ defaultAggregate: "any",
19383
+ accept: [{
19384
+ cap: "smoke",
19385
+ fieldPath: "detected"
19386
+ }],
19387
+ companions: [CHANGED]
19388
+ }),
19389
+ binary({
19390
+ id: "occupancy",
19391
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
19392
+ role: DeviceRole.BinarySensor,
19393
+ outputCap: "binary",
19394
+ outputField: "on",
19395
+ defaultAggregate: "any",
19396
+ accept: [{
19397
+ cap: "motion",
19398
+ fieldPath: "detected"
19399
+ }, {
19400
+ cap: "binary",
19401
+ fieldPath: "on"
19402
+ }],
19403
+ companions: [CHANGED]
19404
+ }),
19405
+ binary({
19406
+ id: "binary",
19407
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
19408
+ role: DeviceRole.BinarySensor,
19409
+ outputCap: "binary",
19410
+ outputField: "on",
19411
+ defaultAggregate: "any",
19412
+ accept: [{
19413
+ cap: "binary",
19414
+ fieldPath: "on"
19415
+ }],
19416
+ companions: [CHANGED]
19417
+ }),
19418
+ actuator({
19419
+ id: "switch",
19420
+ 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.",
19421
+ deviceType: DeviceType.Switch,
19422
+ outputCap: "switch",
19423
+ commandCaps: ["switch"]
19424
+ }),
19425
+ actuator({
19426
+ id: "light",
19427
+ 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.",
19428
+ deviceType: DeviceType.Light,
19429
+ outputCap: "switch",
19430
+ commandCaps: [
19431
+ "switch",
19432
+ "brightness",
19433
+ "color"
19434
+ ]
19435
+ })
19436
+ ];
19437
+ function actuator(input) {
19438
+ return {
19439
+ ...input,
19440
+ family: "actuator",
19441
+ role: void 0,
19442
+ memberDeviceType: input.deviceType,
19443
+ outputField: "on",
19444
+ quantity: "none",
19445
+ baseUnit: void 0,
19446
+ aggregates: BINARY_AGGREGATES,
19447
+ defaultAggregate: "any",
19448
+ empty: "offline",
19449
+ accept: [{
19450
+ cap: "switch",
19451
+ fieldPath: "on"
19452
+ }],
19453
+ companions: [CHANGED]
19454
+ };
19455
+ }
19456
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19457
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
19070
19458
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
19071
19459
  /**
19072
19460
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20893,12 +21281,12 @@ function buildDeviceAdminUrl(parts) {
20893
21281
  * providers detect renames by matching `uniqueId` and patch `id`
20894
21282
  * in-place so dispatch keeps working.
20895
21283
  *
20896
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20897
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20898
- * state push and are the single source of truth. The `device_class` tag is
20899
- * an HA-specific input that is consumed at the discovery boundary
20900
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20901
- * stored after that point.
21284
+ * **Display unit and precision are NOT stored here.** They are a read-time
21285
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
21286
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
21287
+ * neither is repeated on each sample. The `device_class` tag is an
21288
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
21289
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20902
21290
  *
20903
21291
  * The `raw` escape hatch carries provider-specific attributes that don't
20904
21292
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20912,6 +21300,14 @@ var SourceInfoSchema = object({
20912
21300
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20913
21301
  * Used to detect rename when `id` changes. */
20914
21302
  uniqueId: string().optional(),
21303
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
21304
+ quantity: ReadingQuantitySchema.optional(),
21305
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
21306
+ dateTimeShape: _enum([
21307
+ "date",
21308
+ "time",
21309
+ "datetime"
21310
+ ]).optional(),
20915
21311
  /** Provider-specific extras that don't fit the structured slots.
20916
21312
  * Whitelist what goes here per provider — do NOT dump entire HA
20917
21313
  * attribute blobs (would explode the meta JSON column). */
@@ -42016,7 +42412,8 @@ method(object({
42016
42412
  "enum"
42017
42413
  ]),
42018
42414
  enumValues: array(string()).optional(),
42019
- item: boolean().optional()
42415
+ item: boolean().optional(),
42416
+ quantity: ReadingQuantitySchema.optional()
42020
42417
  })).readonly(),
42021
42418
  itemArray: object({
42022
42419
  path: string(),
@@ -45860,8 +46257,8 @@ var MaskPolygonShapeSchema = object({
45860
46257
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45861
46258
  var MaskGridShapeSchema = object({
45862
46259
  kind: literal("grid"),
45863
- gridWidth: number(),
45864
- gridHeight: number(),
46260
+ gridWidth: number().meta(readingMeta("none")),
46261
+ gridHeight: number().meta(readingMeta("none")),
45865
46262
  cells: array(boolean())
45866
46263
  });
45867
46264
  discriminatedUnion("kind", [
@@ -45973,6 +46370,8 @@ var NcSystemEventKindSchema = _enum([
45973
46370
  "detection-blind",
45974
46371
  "motion-noisy",
45975
46372
  "motion-calm",
46373
+ "package-llm-silent",
46374
+ "package-llm-answered",
45976
46375
  "recording-stalled",
45977
46376
  "recording-healthy",
45978
46377
  "recording-degraded",
@@ -57064,6 +57463,28 @@ var PackageVersionInfoSchema = object({
57064
57463
  deprecated: string().optional(),
57065
57464
  distTags: array(string()).readonly()
57066
57465
  });
57466
+ /**
57467
+ * One published document. `ok` is markdown from the registry. `missing` means
57468
+ * the registry answered and the file was not there. `error` is a registry or
57469
+ * tarball failure — never an empty string standing in for either.
57470
+ */
57471
+ var PackageDocBodySchema = discriminatedUnion("status", [
57472
+ object({
57473
+ status: literal("ok"),
57474
+ markdown: string()
57475
+ }),
57476
+ object({ status: literal("missing") }),
57477
+ object({
57478
+ status: literal("error"),
57479
+ message: string()
57480
+ })
57481
+ ]);
57482
+ var PackageDocsSchema = object({
57483
+ name: string(),
57484
+ version: string(),
57485
+ readme: PackageDocBodySchema,
57486
+ changelog: PackageDocBodySchema
57487
+ });
57067
57488
  var SearchResultSchema = object({
57068
57489
  name: string(),
57069
57490
  version: string(),
@@ -57282,7 +57703,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57282
57703
  }), object({ success: literal(true) }), {
57283
57704
  kind: "mutation",
57284
57705
  auth: "admin"
57285
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57706
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57707
+ name: string().min(1),
57708
+ version: string().min(1).optional()
57709
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
57286
57710
  kind: "mutation",
57287
57711
  auth: "admin"
57288
57712
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -57341,27 +57765,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57341
57765
  */
57342
57766
  var AirQualitySensorStatusSchema = object({
57343
57767
  /** Carbon dioxide concentration in ppm. */
57344
- co2Ppm: number().min(0).optional(),
57768
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
57345
57769
  /** Total volatile organic compounds in ppb. */
57346
- vocPpb: number().min(0).optional(),
57770
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
57347
57771
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
57348
- pm25: number().min(0).optional(),
57772
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
57349
57773
  /** Particulate matter ≤ 10 μm in µg/m³. */
57350
- pm10: number().min(0).optional(),
57774
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
57351
57775
  /** Composite AQI value (typically 0..500). */
57352
- aqi: number().optional(),
57776
+ aqi: number().optional().meta(readingMeta("index")),
57353
57777
  /** Ms epoch when the slice was last updated. */
57354
- lastFetchedAt: number(),
57355
- /** Live display unit of the single metric this slice carries (e.g. HA
57356
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
57357
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
57358
- * per slice is unambiguous. */
57359
- unit: string().optional(),
57360
- /** Suggested decimal places for numeric display.
57361
- * Populated live from the upstream source when provided (e.g. HA
57362
- * `attributes.suggested_display_precision`). Falls back to
57363
- * auto-formatting when absent. */
57364
- precision: number().int().min(0).max(10).optional()
57778
+ lastFetchedAt: number()
57365
57779
  });
57366
57780
  var airQualitySensorCapability = {
57367
57781
  name: "air-quality-sensor",
@@ -57398,12 +57812,12 @@ var airQualitySensorCapability = {
57398
57812
  },
57399
57813
  pm25: {
57400
57814
  kind: "gauge",
57401
- unit: "ug/m3",
57815
+ unit: "µg/m³",
57402
57816
  deadband: 1
57403
57817
  },
57404
57818
  pm10: {
57405
57819
  kind: "gauge",
57406
- unit: "ug/m3",
57820
+ unit: "µg/m³",
57407
57821
  deadband: 1
57408
57822
  },
57409
57823
  aqi: {
@@ -57421,22 +57835,13 @@ var airQualitySensorCapability = {
57421
57835
  * entries with `device_class: illuminance`.
57422
57836
  */
57423
57837
  var AmbientLightSensorStatusSchema = object({
57424
- /** Current illuminance in lux (lx). */
57425
- lux: number().min(0).meta({
57426
- unit: "lx",
57838
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57839
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57840
+ precision: 0,
57427
57841
  label: "Illuminance"
57428
- }),
57842
+ })),
57429
57843
  /** Ms epoch when the slice was last updated. */
57430
- lastFetchedAt: number(),
57431
- /** Live display unit from the upstream source (e.g. HA
57432
- * `attributes.unit_of_measurement`). The UI prefers this over the
57433
- * role's canonical unit. Absent → fall back to the canonical unit. */
57434
- unit: string().optional(),
57435
- /** Suggested decimal places for numeric display.
57436
- * Populated live from the upstream source when provided (e.g. HA
57437
- * `attributes.suggested_display_precision`). Falls back to
57438
- * auto-formatting when absent. */
57439
- precision: number().int().min(0).max(10).optional()
57844
+ lastFetchedAt: number()
57440
57845
  });
57441
57846
  var ambientLightSensorCapability = {
57442
57847
  name: "ambient-light-sensor",
@@ -57468,12 +57873,7 @@ var ambientLightSensorCapability = {
57468
57873
  } } },
57469
57874
  /** Clock fields: written, but excluded from the compare that decides
57470
57875
  * whether persisting is worth a SQLite commit. */
57471
- volatileStateFields: ["lastFetchedAt"],
57472
- customize: {
57473
- version: 1,
57474
- advanced: "derive",
57475
- targets: []
57476
- }
57876
+ volatileStateFields: ["lastFetchedAt"]
57477
57877
  };
57478
57878
  /**
57479
57879
  * Per-class audio metrics aggregated over a sliding window.
@@ -57700,10 +58100,7 @@ var BatteryStatusSchema = object({
57700
58100
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57701
58101
  * declines to band a non-finite reading, which is the correct shape.
57702
58102
  */
57703
- percentage: number().min(0).max(100).nullable().meta({
57704
- unit: "%",
57705
- label: "Charge"
57706
- }),
58103
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57707
58104
  /**
57708
58105
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57709
58106
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57730,7 +58127,14 @@ var BatteryStatusSchema = object({
57730
58127
  */
57731
58128
  sleeping: boolean().meta({ label: "Sleeping" }),
57732
58129
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57733
- lastUpdated: number(),
58130
+ lastUpdated: number().meta(readingMeta("none")),
58131
+ /**
58132
+ * Battery voltage in volts, for a cell the firmware reports as voltage
58133
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
58134
+ * that only report charge. A second reading on this cap, not a second
58135
+ * `numeric-sensor` on the same device.
58136
+ */
58137
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57734
58138
  /**
57735
58139
  * Ms epoch of the last time the device PROVED it was reachable — a
57736
58140
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57782,7 +58186,6 @@ var BATTERY_DEVICE_TYPES = [
57782
58186
  /** Generic Customize target for the percentage reading (D936). */
57783
58187
  var batteryCustomize = {
57784
58188
  version: 1,
57785
- advanced: "derive",
57786
58189
  targets: [{
57787
58190
  id: "percentage",
57788
58191
  cardinality: "scalar",
@@ -57791,7 +58194,7 @@ var batteryCustomize = {
57791
58194
  stateField: "percentage"
57792
58195
  },
57793
58196
  deviceTypes: BATTERY_DEVICE_TYPES,
57794
- apply: "add-or-replace",
58197
+ apply: "add",
57795
58198
  path: "percentage",
57796
58199
  sources: [{
57797
58200
  kind: "field",
@@ -57912,10 +58315,16 @@ onStatusChanged: { data: object({
57912
58315
  /**
57913
58316
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57914
58317
  */
57915
- history: { fields: { percentage: {
57916
- kind: "gauge",
57917
- unit: "%"
57918
- } } },
58318
+ history: { fields: {
58319
+ percentage: {
58320
+ kind: "gauge",
58321
+ unit: "%"
58322
+ },
58323
+ volts: {
58324
+ kind: "gauge",
58325
+ unit: "V"
58326
+ }
58327
+ } },
57919
58328
  customize: batteryCustomize,
57920
58329
  /** Clock fields: written, but excluded from the compare that decides
57921
58330
  * whether persisting is worth a SQLite commit. */
@@ -57962,12 +58371,7 @@ var binaryCapability = {
57962
58371
  history: { fields: { on: { kind: "boolean" } } },
57963
58372
  /** Clock fields: written, but excluded from the compare that decides
57964
58373
  * whether persisting is worth a SQLite commit. */
57965
- volatileStateFields: ["lastChangedAt"],
57966
- customize: {
57967
- version: 1,
57968
- advanced: "derive",
57969
- targets: []
57970
- }
58374
+ volatileStateFields: ["lastChangedAt"]
57971
58375
  };
57972
58376
  /**
57973
58377
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57983,7 +58387,7 @@ var binaryCapability = {
57983
58387
  */
57984
58388
  var BrightnessStatusSchema = object({
57985
58389
  /** Current level as 0..100 inclusive. Firmware-reported. */
57986
- percentage: number().min(0).max(100),
58390
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57987
58391
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57988
58392
  lastChangedAt: number()
57989
58393
  });
@@ -58437,21 +58841,21 @@ var ClimateControlStatusSchema = object({
58437
58841
  /** Single setpoint in Celsius. Used by single-target modes
58438
58842
  * (heat / cool / dry). Null when the device is in a range mode
58439
58843
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58440
- target: number().nullable(),
58844
+ target: number().nullable().meta(readingMeta("temperature")),
58441
58845
  /** Upper bound of the dual setpoint in Celsius. Populated only
58442
58846
  * in `heat_cool` mode. */
58443
- targetHigh: number().nullable(),
58847
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58444
58848
  /** Lower bound of the dual setpoint in Celsius. Populated only
58445
58849
  * in `heat_cool` mode. */
58446
- targetLow: number().nullable(),
58850
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58447
58851
  /** Target relative humidity (0..100). Null when the device has no
58448
58852
  * humidity control. */
58449
- targetHumidity: number().min(0).max(100).nullable(),
58853
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58450
58854
  /** Current measured temperature in Celsius. Read-only — pushed by
58451
58855
  * the device's internal sensor. */
58452
- currentTemp: number().nullable(),
58856
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58453
58857
  /** Current measured relative humidity (0..100). Read-only. */
58454
- currentHumidity: number().min(0).max(100).nullable(),
58858
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58455
58859
  /** Vertical louver swing on/off. Null when the device has no vertical
58456
58860
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58457
58861
  swingVertical: boolean().nullable(),
@@ -58459,11 +58863,11 @@ var ClimateControlStatusSchema = object({
58459
58863
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58460
58864
  swingHorizontal: boolean().nullable(),
58461
58865
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58462
- minTemp: number().nullable().optional(),
58866
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58463
58867
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58464
- maxTemp: number().nullable().optional(),
58868
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58465
58869
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58466
- tempStep: number().positive().nullable().optional(),
58870
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58467
58871
  /** Ms epoch when the slice was last updated (push or command). */
58468
58872
  lastFetchedAt: number()
58469
58873
  });
@@ -58605,17 +59009,17 @@ var climateControlCapability = {
58605
59009
  * modes to expose in the picker without a slice fetch.
58606
59010
  */
58607
59011
  var RgbTripletSchema = object({
58608
- r: number().int().min(0).max(255),
58609
- g: number().int().min(0).max(255),
58610
- b: number().int().min(0).max(255)
59012
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
59013
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
59014
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58611
59015
  });
58612
59016
  var HsvTripletSchema = object({
58613
59017
  /** Hue in degrees, 0..360. */
58614
- h: number().min(0).max(360),
59018
+ h: number().min(0).max(360).meta(readingMeta("none")),
58615
59019
  /** Saturation as 0..100 inclusive. */
58616
- s: number().min(0).max(100),
59020
+ s: number().min(0).max(100).meta(readingMeta("none")),
58617
59021
  /** Value/brightness as 0..100 inclusive. */
58618
- v: number().min(0).max(100)
59022
+ v: number().min(0).max(100).meta(readingMeta("none"))
58619
59023
  });
58620
59024
  var ColorInputSchema = discriminatedUnion("mode", [
58621
59025
  object({
@@ -58644,7 +59048,7 @@ var ColorStatusSchema = object({
58644
59048
  /** Populated when `mode === 'hsv'`. */
58645
59049
  hsv: HsvTripletSchema.optional(),
58646
59050
  /** Populated when `mode === 'mired'`. */
58647
- mireds: number().int().optional(),
59051
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58648
59052
  /** Ms epoch of the last operator-driven change. */
58649
59053
  lastChangedAt: number()
58650
59054
  });
@@ -58839,18 +59243,12 @@ _enum([
58839
59243
  "empty",
58840
59244
  "full"
58841
59245
  ]);
58842
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58843
- * each. */
58844
- var ConsumableRemainingUnitSchema = _enum([
58845
- "days",
58846
- "hours",
58847
- "cycles"
58848
- ]);
58849
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58850
- * days). `value` null = unknown (D393). */
59246
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
59247
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
59248
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58851
59249
  var ConsumableRemainingSchema = object({
58852
- value: number().min(0).nullable(),
58853
- unit: ConsumableRemainingUnitSchema
59250
+ value: number().min(0).nullable().meta(readingMeta("device")),
59251
+ kind: ConsumableRemainingKindSchema
58854
59252
  });
58855
59253
  /** A single consumable item. Either a continuous `level` (remaining
58856
59254
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58863,7 +59261,7 @@ var ConsumableItemSchema = object({
58863
59261
  /** Display name. */
58864
59262
  label: string().min(1),
58865
59263
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58866
- level: number().min(0).max(100).nullable(),
59264
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58867
59265
  /** The provider's discrete verdict when known (binary mode). */
58868
59266
  status: ConsumableStatusSchema.nullable(),
58869
59267
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58945,8 +59343,8 @@ var consumablesCustomize = {
58945
59343
  transforms: [{ kind: "identity" }]
58946
59344
  }],
58947
59345
  literals: [{
58948
- path: "remaining.unit",
58949
- values: [...ConsumableRemainingUnitSchema.options]
59346
+ path: "remaining.kind",
59347
+ values: [...ConsumableRemainingKindSchema.options]
58950
59348
  }]
58951
59349
  },
58952
59350
  {
@@ -59075,7 +59473,6 @@ reset: method(object({
59075
59473
  },
59076
59474
  "remaining.value": {
59077
59475
  kind: "gauge",
59078
- unitFrom: "remaining.unit",
59079
59476
  deadband: 1,
59080
59477
  displayName: "Remaining"
59081
59478
  }
@@ -59145,10 +59542,9 @@ var contactCapability = {
59145
59542
  * (empty array for the other kinds). It lives in the slice because
59146
59543
  * HA `select.*` entities may update their options at runtime.
59147
59544
  *
59148
- * The `unit`/`min`/`max`/`step` fields are meaningful for
59149
- * `kind === 'numeric'` and are absent (undefined) for all other
59150
- * kinds. They are populated live from HA attributes on each state
59151
- * push — consistent with `options` and the sensor-unit approach.
59545
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59546
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59547
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
59152
59548
  */
59153
59549
  var ControlStatusSchema = object({
59154
59550
  kind: _enum([
@@ -59160,39 +59556,17 @@ var ControlStatusSchema = object({
59160
59556
  /** Current value. Numeric kind → number; select/text/datetime → string.
59161
59557
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
59162
59558
  * or time-only — HA accepts all three forms). */
59163
- value: union([number(), string()]),
59559
+ value: union([number().meta(readingMeta("device")), string()]),
59164
59560
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
59165
59561
  options: array(string()),
59166
59562
  /** Ms epoch when the slice was last updated. */
59167
59563
  lastChangedAt: number(),
59168
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
59169
- * Populated live from HA `attributes.unit_of_measurement` on each push.
59170
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59171
- unit: string().optional(),
59172
- /** Minimum allowed value. Populated live from HA `attributes.min`.
59173
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59174
- min: number().optional(),
59175
- /** Maximum allowed value. Populated live from HA `attributes.max`.
59176
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59177
- max: number().optional(),
59178
- /** Step increment. Populated live from HA `attributes.step`.
59179
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59180
- step: number().optional(),
59181
- /** Suggested decimal places for numeric display. Populated live from HA
59182
- * `attributes.suggested_display_precision`, or derived from `step` when the
59183
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
59184
- * other kinds. Falls back to auto-formatting when absent. */
59185
- precision: number().int().min(0).max(10).optional(),
59186
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
59187
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
59188
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
59189
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
59190
- * `has_date`/`has_time` attributes on each push. */
59191
- format: _enum([
59192
- "date",
59193
- "time",
59194
- "datetime"
59195
- ]).optional()
59564
+ /** Minimum allowed value, in the device quantity's base unit. */
59565
+ min: number().optional().meta(readingMeta("device")),
59566
+ /** Maximum allowed value, in the device quantity's base unit. */
59567
+ max: number().optional().meta(readingMeta("device")),
59568
+ /** Step increment, in the device quantity's base unit. */
59569
+ step: number().optional().meta(readingMeta("device"))
59196
59570
  });
59197
59571
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
59198
59572
  object({
@@ -59258,9 +59632,9 @@ var CoverStatusSchema = object({
59258
59632
  ]),
59259
59633
  /** 0 = fully closed, 100 = fully open. Null when the device has
59260
59634
  * no intermediate position surface. */
59261
- position: number().min(0).max(100).nullable(),
59635
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59262
59636
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
59263
- tiltPosition: number().min(0).max(100).nullable(),
59637
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59264
59638
  /** Ms epoch when the slice was last updated. */
59265
59639
  lastChangedAt: number()
59266
59640
  });
@@ -59363,9 +59737,9 @@ var NormalizedRangeSchema$1 = object({
59363
59737
  var DayNightStatusSchema = object({
59364
59738
  mode: DayNightModeSchema,
59365
59739
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
59366
- sensitivity: number().optional(),
59740
+ sensitivity: number().optional().meta(readingMeta("none")),
59367
59741
  /** Delay before the IR-cut filter flips, in seconds. */
59368
- switchDelaySec: number().optional(),
59742
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
59369
59743
  lastFetchedAt: number()
59370
59744
  });
59371
59745
  /**
@@ -59583,11 +59957,11 @@ var DishwasherStatusSchema = object({
59583
59957
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59584
59958
  availablePrograms: array(string()),
59585
59959
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59586
- remainingSeconds: number().int().nonnegative().nullable(),
59960
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59587
59961
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59588
- delayedStartSeconds: number().int().nonnegative().nullable(),
59962
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59589
59963
  /** Cycle completion 0..100, or null when not applicable. */
59590
- progressPercent: number().min(0).max(100).nullable(),
59964
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59591
59965
  /** True when the door is reported open; null when the door state is not known. */
59592
59966
  doorOpen: boolean().nullable(),
59593
59967
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59611,7 +59985,7 @@ var DishwasherStatusSchema = object({
59611
59985
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59612
59986
  machineCareDue: boolean().nullable(),
59613
59987
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59614
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59988
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59615
59989
  /** Option states: null when the appliance does not expose the option or has not said. */
59616
59990
  halfLoad: boolean().nullable(),
59617
59991
  hygienePlus: boolean().nullable(),
@@ -59626,7 +60000,7 @@ var DishwasherStatusSchema = object({
59626
60000
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59627
60001
  activeEvents: array(string()),
59628
60002
  /** Numeric error code when the appliance reports one; null otherwise. */
59629
- errorCode: number().nullable(),
60003
+ errorCode: number().nullable().meta(readingMeta("none")),
59630
60004
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59631
60005
  errorLabel: string().nullable(),
59632
60006
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59857,7 +60231,7 @@ var DoorbellStatusSchema = object({
59857
60231
  /** Ms epoch of the last press. null = never observed since this provider started. */
59858
60232
  lastPressedAt: number().nullable(),
59859
60233
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59860
- pressCountSinceStart: number()
60234
+ pressCountSinceStart: number().meta(readingMeta("count"))
59861
60235
  });
59862
60236
  var DoorbellPressEventSchema = object({
59863
60237
  deviceId: number(),
@@ -59922,17 +60296,7 @@ onPressed: {
59922
60296
  history: "none",
59923
60297
  customize: doorbellCustomize
59924
60298
  };
59925
- /**
59926
- * Enum-state sensor — a string value picked from a finite option set.
59927
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59928
- * states, weather conditions, contact-source channels, mode strings).
59929
- * The option set is stable across observations; it is persisted in the
59930
- * device config blob (via the control slice at adoption) and is NOT
59931
- * stored in `sourceInfo`.
59932
- */
59933
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59934
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59935
- var EnumSensorDateTimeFormatSchema = _enum([
60299
+ _enum([
59936
60300
  "date",
59937
60301
  "time",
59938
60302
  "datetime"
@@ -59940,12 +60304,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59940
60304
  var EnumSensorStatusSchema = object({
59941
60305
  value: string(),
59942
60306
  /**
59943
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59944
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59945
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59946
- * a time-only sensor → `'time'`.
60307
+ * Set when `value` is a timestamp rather than an enum state, so history
60308
+ * does not learn ISO strings as states. The display shape (date / time /
60309
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59947
60310
  */
59948
- format: EnumSensorDateTimeFormatSchema.optional(),
60311
+ timestamp: boolean().optional(),
59949
60312
  /** Ms epoch when the slice was last updated. */
59950
60313
  lastFetchedAt: number()
59951
60314
  });
@@ -59972,11 +60335,11 @@ var enumSensorCapability = {
59972
60335
  * History (D850): the value as an `enum` series (an appliance's program or
59973
60336
  * operation state, an HVAC action). The option set is per device, so the
59974
60337
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59975
- * `format` carries a timestamp, not a state, and is not recorded.
60338
+ * `timestamp` carries a clock, not a state, and is not recorded.
59976
60339
  */
59977
60340
  history: { fields: { value: {
59978
60341
  kind: "enum",
59979
- skipWhenSet: "format"
60342
+ skipWhenSet: "timestamp"
59980
60343
  } } },
59981
60344
  /** Clock fields: written, but excluded from the compare that decides
59982
60345
  * whether persisting is worth a SQLite commit. */
@@ -59996,13 +60359,13 @@ var EventFireSchema = object({
59996
60359
  deviceId: number(),
59997
60360
  eventType: string(),
59998
60361
  data: record(string(), unknown()).nullable(),
59999
- timestamp: number(),
60000
- seq: number()
60362
+ timestamp: number().meta(readingMeta("none")),
60363
+ seq: number().meta(readingMeta("count"))
60001
60364
  });
60002
60365
  var EventEmitterStatusSchema = object({
60003
60366
  eventTypes: array(string()),
60004
60367
  lastEvent: EventFireSchema.nullable(),
60005
- eventCountSinceStart: number()
60368
+ eventCountSinceStart: number().meta(readingMeta("count"))
60006
60369
  });
60007
60370
  var eventEmitterCapability = {
60008
60371
  name: "event-emitter",
@@ -60415,10 +60778,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60415
60778
  var FanControlStatusSchema = object({
60416
60779
  /** Active speed as 0..100 inclusive. Null when the device has no
60417
60780
  * speed surface (single-speed fan). */
60418
- percentage: number().min(0).max(100).nullable(),
60781
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60419
60782
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60420
60783
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60421
- percentageStep: number().positive().optional(),
60784
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60422
60785
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60423
60786
  * — empty string when no preset is active or supported. */
60424
60787
  preset: string(),
@@ -60539,7 +60902,7 @@ var FeatureProbeStatusSchema = object({
60539
60902
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60540
60903
  model: string().nullable(),
60541
60904
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60542
- channelCount: number().nullable(),
60905
+ channelCount: number().nullable().meta(readingMeta("count")),
60543
60906
  /**
60544
60907
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60545
60908
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60662,9 +61025,91 @@ var gasCapability = {
60662
61025
  * whether persisting is worth a SQLite commit. */
60663
61026
  volatileStateFields: ["lastChangedAt"]
60664
61027
  };
61028
+ var GroupCommandsSchema = object({
61029
+ /** One row per member that has been commanded. A later command replaces that member's row. */
61030
+ results: array(object({
61031
+ deviceId: number().int().nonnegative(),
61032
+ cap: string().min(1),
61033
+ method: string().min(1),
61034
+ ok: boolean(),
61035
+ error: string().optional(),
61036
+ /** Ms epoch of the attempt. */
61037
+ at: number()
61038
+ })) });
61039
+ var groupCommandsCapability = {
61040
+ name: "group-commands",
61041
+ scope: "device",
61042
+ deviceNative: true,
61043
+ mode: "singleton",
61044
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
61045
+ methods: {},
61046
+ status: {
61047
+ schema: GroupCommandsSchema,
61048
+ kind: "push"
61049
+ },
61050
+ runtimeState: GroupCommandsSchema,
61051
+ /**
61052
+ * Runtime-state durability: **session** — the last attempt. A restored
61053
+ * failure would outlive the command that caused it.
61054
+ *
61055
+ * See `RuntimeStateDurability`. Enforced by
61056
+ * `scripts/check-runtime-state-durability.ts`.
61057
+ */
61058
+ durability: "session",
61059
+ history: "none"
61060
+ };
61061
+ /**
61062
+ * How many members of a helper group answered (D944). Counts are integers,
61063
+ * not measurements: zero included is a real count, never a stand-in reading.
61064
+ * The reading itself stays on the output capability. History is none — the
61065
+ * series that later statistics read is the output cap's own history block.
61066
+ */
61067
+ var GroupStatusSchema = object({
61068
+ /** Members whose current value entered the aggregate. */
61069
+ included: number().int().nonnegative().meta(readingMeta("count", {
61070
+ precision: 0,
61071
+ label: "Reporting"
61072
+ })),
61073
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
61074
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
61075
+ precision: 0,
61076
+ label: "Excluded"
61077
+ })),
61078
+ /** `included` + `excluded`. */
61079
+ total: number().int().nonnegative().meta(readingMeta("count", {
61080
+ precision: 0,
61081
+ label: "Members"
61082
+ }))
61083
+ });
61084
+ var groupStatusCapability = {
61085
+ name: "group-status",
61086
+ scope: "device",
61087
+ deviceNative: true,
61088
+ mode: "singleton",
61089
+ deviceTypes: [
61090
+ DeviceType.Sensor,
61091
+ DeviceType.Switch,
61092
+ DeviceType.Light
61093
+ ],
61094
+ methods: {},
61095
+ status: {
61096
+ schema: GroupStatusSchema,
61097
+ kind: "push"
61098
+ },
61099
+ runtimeState: GroupStatusSchema,
61100
+ /**
61101
+ * Runtime-state durability: **session** — recomputed from members on the
61102
+ * next source event; a restored count would outlive the readings it named.
61103
+ *
61104
+ * See `RuntimeStateDurability`. Enforced by
61105
+ * `scripts/check-runtime-state-durability.ts`.
61106
+ */
61107
+ durability: "session",
61108
+ history: "none"
61109
+ };
60665
61110
  var HeatingValveStatusSchema = object({
60666
61111
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60667
- position: number().min(0).max(100).nullable(),
61112
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60668
61113
  /** Ms epoch when the slice was last updated (push or poll). */
60669
61114
  lastFetchedAt: number()
60670
61115
  });
@@ -60707,9 +61152,9 @@ var HumidifierStatusSchema = object({
60707
61152
  /** Whether the humidifier is currently on. */
60708
61153
  on: boolean(),
60709
61154
  /** Current measured relative humidity (0..100). Null when not reported. */
60710
- currentHumidity: number().min(0).max(100).nullable(),
61155
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60711
61156
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60712
- targetHumidity: number().min(0).max(100).nullable(),
61157
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60713
61158
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60714
61159
  * device has no mode surface. */
60715
61160
  mode: string().nullable(),
@@ -60719,9 +61164,9 @@ var HumidifierStatusSchema = object({
60719
61164
  * `idle` / `off`). Null when the device doesn't report it. */
60720
61165
  action: string().nullable(),
60721
61166
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60722
- minHumidity: number().nullable(),
61167
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60723
61168
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60724
- maxHumidity: number().nullable(),
61169
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60725
61170
  /** Ms epoch when the slice was last updated. */
60726
61171
  lastChangedAt: number()
60727
61172
  });
@@ -60791,22 +61236,13 @@ var humidifierCapability = {
60791
61236
  * `sourceInfo`.
60792
61237
  */
60793
61238
  var HumiditySensorStatusSchema = object({
60794
- /** Current relative humidity, 0..100. */
60795
- percent: number().min(0).max(100).meta({
60796
- unit: "%",
61239
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
61240
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
61241
+ precision: 0,
60797
61242
  label: "Humidity"
60798
- }),
61243
+ })),
60799
61244
  /** Ms epoch when the slice was last updated. */
60800
- lastFetchedAt: number(),
60801
- /** Live display unit from the upstream source (e.g. HA
60802
- * `attributes.unit_of_measurement`). The UI prefers this over the
60803
- * role's canonical unit. Absent → fall back to the canonical unit. */
60804
- unit: string().optional(),
60805
- /** Suggested decimal places for numeric display.
60806
- * Populated live from the upstream source when provided (e.g. HA
60807
- * `attributes.suggested_display_precision`). Falls back to
60808
- * auto-formatting when absent. */
60809
- precision: number().int().min(0).max(10).optional()
61245
+ lastFetchedAt: number()
60810
61246
  });
60811
61247
  var humiditySensorCapability = {
60812
61248
  name: "humidity-sensor",
@@ -60837,12 +61273,7 @@ var humiditySensorCapability = {
60837
61273
  } } },
60838
61274
  /** Clock fields: written, but excluded from the compare that decides
60839
61275
  * whether persisting is worth a SQLite commit. */
60840
- volatileStateFields: ["lastFetchedAt"],
60841
- customize: {
60842
- version: 1,
60843
- advanced: "derive",
60844
- targets: []
60845
- }
61276
+ volatileStateFields: ["lastFetchedAt"]
60846
61277
  };
60847
61278
  /**
60848
61279
  * Image display cap. Models a single still image exposed by an integration —
@@ -60863,7 +61294,7 @@ var ImageStatusSchema = object({
60863
61294
  * entity exposes no `entity_picture` (yet). */
60864
61295
  url: string().nullable(),
60865
61296
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60866
- lastUpdated: number().nullable()
61297
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60867
61298
  });
60868
61299
  var imageCapability = {
60869
61300
  name: "image",
@@ -60951,19 +61382,19 @@ var NormalizedRangeSchema = object({
60951
61382
  */
60952
61383
  var ImageSettingsStatusSchema = object({
60953
61384
  /** Normalized 0–100. */
60954
- brightness: number().optional(),
61385
+ brightness: number().optional().meta(readingMeta("none")),
60955
61386
  /** Normalized 0–100. */
60956
- contrast: number().optional(),
61387
+ contrast: number().optional().meta(readingMeta("none")),
60957
61388
  /** Normalized 0–100. */
60958
- saturation: number().optional(),
61389
+ saturation: number().optional().meta(readingMeta("none")),
60959
61390
  /** Normalized 0–100. */
60960
- sharpness: number().optional(),
61391
+ sharpness: number().optional().meta(readingMeta("none")),
60961
61392
  mirror: boolean().optional(),
60962
61393
  flip: boolean().optional(),
60963
61394
  rotate: ImageRotateSchema.optional(),
60964
61395
  whiteBalance: WhiteBalanceModeSchema.optional(),
60965
61396
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60966
- warmth: number().optional(),
61397
+ warmth: number().optional().meta(readingMeta("none")),
60967
61398
  exposureMode: ExposureModeSchema.optional(),
60968
61399
  backlightMode: BacklightModeSchema.optional(),
60969
61400
  lastFetchedAt: number()
@@ -61242,10 +61673,10 @@ var IntercomAbilitySchema = object({
61242
61673
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
61243
61674
  codecs: array(string()),
61244
61675
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
61245
- sampleRate: number(),
61676
+ sampleRate: number().meta(readingMeta("none")),
61246
61677
  duplex: _enum(["half", "full"]),
61247
61678
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
61248
- maxBacklogMs: number()
61679
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
61249
61680
  });
61250
61681
  var IntercomStatusSchema = object({
61251
61682
  /** True while a talk session is open. */
@@ -61472,13 +61903,13 @@ var LawnMowerControlStatusSchema = object({
61472
61903
  activity: LawnMowerActivitySchema,
61473
61904
  /** 0..100 battery percentage. Null when the device has no battery
61474
61905
  * reading. */
61475
- batteryLevel: number().min(0).max(100).nullable(),
61906
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61476
61907
  /** 0..100 mowing-completion percentage of the current task, or null when no
61477
61908
  * task is active / progress is unavailable. */
61478
- progressPercent: number().min(0).max(100).nullable(),
61909
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61479
61910
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61480
61911
  * or null when unknown. */
61481
- currentCode: number().nullable(),
61912
+ currentCode: number().nullable().meta(readingMeta("none")),
61482
61913
  /** Human label for {@link currentCode}, or null when undecodable. */
61483
61914
  currentCodeLabel: string().nullable(),
61484
61915
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -62011,7 +62442,7 @@ var MediaPlayerStatusSchema = object({
62011
62442
  state: MediaPlayerStateSchema,
62012
62443
  /** Volume as 0..100 inclusive. Null when the device has no volume
62013
62444
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
62014
- volumeLevel: number().min(0).max(100).nullable(),
62445
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62015
62446
  /** Mute toggle distinct from volume=0. Null when no mute surface.
62016
62447
  * Pair with `DeviceFeature.MediaPlayerMute`. */
62017
62448
  isMuted: boolean().nullable(),
@@ -62024,10 +62455,10 @@ var MediaPlayerStatusSchema = object({
62024
62455
  currentMedia: MediaInfoSchema.nullable(),
62025
62456
  /** Current playback position in ms. Null when not seekable or
62026
62457
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
62027
- positionMs: number().int().nonnegative().nullable(),
62458
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62028
62459
  /** Total duration of the current media in ms. Null when unknown
62029
62460
  * (live stream) or nothing is playing. */
62030
- durationMs: number().int().nonnegative().nullable(),
62461
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62031
62462
  /** Shuffle toggle. Null when no shuffle surface.
62032
62463
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
62033
62464
  shuffle: boolean().nullable(),
@@ -62336,7 +62767,7 @@ var motionCustomize = {
62336
62767
  eventName: "onMotionChanged"
62337
62768
  },
62338
62769
  deviceTypes: [DeviceType.Camera],
62339
- apply: "replace",
62770
+ apply: "add",
62340
62771
  applyAuthority: {
62341
62772
  kind: "camera-sensors",
62342
62773
  timeoutField: "motionCooldownMs"
@@ -62408,7 +62839,7 @@ var MotionStatusSchema = object({
62408
62839
  * it, every provider seeded it at boot, and every session left it
62409
62840
  * `detected: true` after its detach.
62410
62841
  */
62411
- autoClearAfterMs: number().nullable()
62842
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62412
62843
  });
62413
62844
  /**
62414
62845
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62445,12 +62876,18 @@ var motionCapability = {
62445
62876
  methods: {
62446
62877
  /**
62447
62878
  * Pull the current motion state synchronously. Convenience shortcut
62448
- * for consumers that don't need the full status object; equivalent
62449
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62450
- * `getStatus` once the auto-injected status surface lands in every
62451
- * consumer.
62879
+ * for consumers that don't need the full status object. A composed
62880
+ * group answers from its slice (D944): unknown throws, it is not off.
62452
62881
  */
62453
62882
  isDetected: method(object({ deviceId: number() }), boolean()) },
62883
+ /**
62884
+ * A composed motion group serves `isDetected` from `detected` (D944).
62885
+ * Without this the cap stays uncomposable (`has-commands`).
62886
+ */
62887
+ composedMethods: { isDetected: {
62888
+ kind: "slice",
62889
+ field: "detected"
62890
+ } },
62454
62891
  events: {
62455
62892
  /**
62456
62893
  * Fires every time the runner transitions a camera between
@@ -62590,7 +63027,7 @@ var MotionZoneRegionSchema = object({
62590
63027
  * the grid region(s). */
62591
63028
  var MotionZoneStatusSchema = object({
62592
63029
  enabled: boolean(),
62593
- sensitivity: number(),
63030
+ sensitivity: number().meta(readingMeta("none")),
62594
63031
  /** Grid region(s). Today exactly one `grid` shape. */
62595
63032
  regions: array(MotionZoneRegionSchema),
62596
63033
  lastFetchedAt: number()
@@ -62960,7 +63397,7 @@ var NavigationStatusSchema = object({
62960
63397
  following: boolean(),
62961
63398
  flash: boolean(),
62962
63399
  lightMode: NavigationLightModeSchema,
62963
- lightLevel: number().min(40).max(100),
63400
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62964
63401
  /** Ms epoch when the slice was last updated. */
62965
63402
  lastChangedAt: number()
62966
63403
  });
@@ -63094,13 +63531,13 @@ var NetworkLinkStatusSchema = object({
63094
63531
  * one whose reading has not landed must not be drawn at 0 %. Consumers
63095
63532
  * SKIP a null rather than coerce it.
63096
63533
  */
63097
- signalPercent: number().min(0).max(100).nullable(),
63534
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63098
63535
  /** Raw received signal strength in dBm, when the firmware reports one. */
63099
- rssiDbm: number().optional(),
63536
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
63100
63537
  /** Network name of a wireless link, when the firmware reports it. */
63101
63538
  ssid: string().optional(),
63102
63539
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
63103
- lastUpdated: number()
63540
+ lastUpdated: number().meta(readingMeta("none"))
63104
63541
  });
63105
63542
  var networkLinkCapability = {
63106
63543
  name: "network-link",
@@ -63485,7 +63922,7 @@ var NotifierStatusSchema = object({
63485
63922
  lastError: string().nullable(),
63486
63923
  /** Number of deliveries currently buffered server-side (rate-limited
63487
63924
  * notifiers may queue). 0 when the provider sends synchronously. */
63488
- queueDepth: number().int().nonnegative(),
63925
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63489
63926
  /** Per-feature capability matrix — authoritative source for the
63490
63927
  * compose-form field gating. */
63491
63928
  supports: NotifierSupportsSchema
@@ -63577,11 +64014,9 @@ onSent: { data: object({
63577
64014
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63578
64015
  * for: water flow, distance, weight, frequency, signal strength, …).
63579
64016
  *
63580
- * The `unit` and `precision` fields carry the live display metadata that
63581
- * the upstream source provides with each state push (HA
63582
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63583
- * The UI reads them directly from the slice — they are the single source
63584
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
64017
+ * The quantity is not on this slice. It is chosen when the device is
64018
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
64019
+ * that quantity's base unit. `null` is a refused sample (D393).
63585
64020
  *
63586
64021
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63587
64022
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63590,15 +64025,8 @@ onSent: { data: object({
63590
64025
  * numeric values land here.
63591
64026
  */
63592
64027
  var NumericSensorStatusSchema = object({
63593
- value: number(),
63594
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63595
- * Populated live from the upstream source on each state push.
63596
- * Absent for unitless sensors. */
63597
- unit: string().optional(),
63598
- /** Suggested decimal places for numeric display.
63599
- * Populated live from the upstream source when provided.
63600
- * Falls back to auto-formatting when absent. */
63601
- precision: number().int().min(0).max(10).optional(),
64028
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
64029
+ value: number().nullable().meta(readingMeta("device")),
63602
64030
  /** Ms epoch when the slice was last updated. */
63603
64031
  lastFetchedAt: number()
63604
64032
  });
@@ -63622,11 +64050,11 @@ var numericSensorCapability = {
63622
64050
  */
63623
64051
  durability: "restored",
63624
64052
  /**
63625
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
64053
+ * History (D850): recorded series. The unit is the device quantity's base,
64054
+ * resolved when the series is read — the cap has no single unit.
63626
64055
  */
63627
64056
  history: { fields: { value: {
63628
64057
  kind: "gauge",
63629
- unitFrom: "unit",
63630
64058
  minIntervalMs: 3e4
63631
64059
  } } },
63632
64060
  /** Clock fields: written, but excluded from the compare that decides
@@ -63918,23 +64346,26 @@ var PetFeederStatusSchema = object({
63918
64346
  /** Food currently in the bowl (grams). Null when the device has not
63919
64347
  * reported a reading yet. On dual-hopper models this is the combined
63920
64348
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63921
- foodLevel: number().nullable(),
64349
+ foodLevel: number().nullable().meta(readingMeta("none")),
63922
64350
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63923
64351
  * single-hopper models. */
63924
- food1: number().nullable(),
64352
+ food1: number().nullable().meta(readingMeta("none")),
63925
64353
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63926
64354
  * single-hopper models. */
63927
- food2: number().nullable(),
64355
+ food2: number().nullable().meta(readingMeta("none")),
63928
64356
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63929
64357
  * (`device_class: problem`, on = low). True when the bowl is empty /
63930
64358
  * below the feeder's low threshold. */
63931
64359
  lowFood: boolean(),
63932
64360
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63933
64361
  * device has no battery reading. */
63934
- batteryPower: number().min(0).max(100).nullable(),
63935
- /** Days of desiccant life remaining. Null when the model has no
63936
- * desiccant sensor. */
63937
- desiccantLeftDays: number().nullable(),
64362
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64363
+ /**
64364
+ * Desiccant life remaining, stored in seconds (duration base, D944).
64365
+ * Providers convert from the days the feeder reports. Null when the
64366
+ * model has no desiccant sensor.
64367
+ */
64368
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63938
64369
  /** True while a feed is in progress. */
63939
64370
  feeding: boolean(),
63940
64371
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63945,7 +64376,7 @@ var PetFeederStatusSchema = object({
63945
64376
  * with `errorCode` for consumers that want the raw integer. */
63946
64377
  error: string().nullable(),
63947
64378
  /** Raw device error code (0 / null = no error). */
63948
- errorCode: number().nullable(),
64379
+ errorCode: number().nullable().meta(readingMeta("none")),
63949
64380
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63950
64381
  isDualHopper: boolean(),
63951
64382
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63955,7 +64386,7 @@ var PetFeederStatusSchema = object({
63955
64386
  /** Play a chime when dispensing. */
63956
64387
  feedSound: boolean(),
63957
64388
  /** Speaker / prompt volume level (device-scaled integer). */
63958
- volume: number(),
64389
+ volume: number().meta(readingMeta("none")),
63959
64390
  /** Ms epoch when the slice was last refreshed from the cloud. */
63960
64391
  lastFetchedAt: number()
63961
64392
  });
@@ -64074,7 +64505,7 @@ var petFeederCapability = {
64074
64505
  },
64075
64506
  desiccantLeftDays: {
64076
64507
  kind: "gauge",
64077
- unit: "days"
64508
+ unit: "s"
64078
64509
  }
64079
64510
  } }
64080
64511
  };
@@ -64475,36 +64906,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64475
64906
  */
64476
64907
  var PowerMeterStatusSchema = object({
64477
64908
  /** Instantaneous power draw in watts. */
64478
- watts: number().optional().meta({
64479
- unit: "W",
64480
- label: "Power"
64481
- }),
64909
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64482
64910
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64483
- kwhTotal: number().optional().meta({
64484
- unit: "kWh",
64485
- label: "Energy"
64486
- }),
64911
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64487
64912
  /** Voltage in volts. */
64488
- volts: number().optional(),
64913
+ volts: number().optional().meta(readingMeta("voltage")),
64489
64914
  /** Current in amperes. */
64490
- amps: number().optional(),
64491
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64492
- va: number().optional(),
64915
+ amps: number().optional().meta(readingMeta("current")),
64916
+ /** Apparent power in volt-amperes. */
64917
+ va: number().optional().meta(readingMeta("apparent-power")),
64493
64918
  /** Power factor (0..1) if reported. */
64494
- powerFactor: number().min(0).max(1).optional(),
64919
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64495
64920
  /** Ms epoch when the slice was last updated. */
64496
- lastFetchedAt: number(),
64497
- /** Live display unit of the single metric this slice carries (e.g. HA
64498
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64499
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64500
- * unit per slice is unambiguous. The UI prefers this over the role's
64501
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64502
- unit: string().optional(),
64503
- /** Suggested decimal places for numeric display.
64504
- * Populated live from the upstream source when provided (e.g. HA
64505
- * `attributes.suggested_display_precision`). Falls back to
64506
- * auto-formatting when absent. */
64507
- precision: number().int().min(0).max(10).optional()
64921
+ lastFetchedAt: number()
64508
64922
  });
64509
64923
  var powerMeterCapability = {
64510
64924
  name: "power-meter",
@@ -64568,12 +64982,7 @@ var powerMeterCapability = {
64568
64982
  } },
64569
64983
  /** Clock fields: written, but excluded from the compare that decides
64570
64984
  * whether persisting is worth a SQLite commit. */
64571
- volatileStateFields: ["lastFetchedAt"],
64572
- customize: {
64573
- version: 1,
64574
- advanced: "derive",
64575
- targets: []
64576
- }
64985
+ volatileStateFields: ["lastFetchedAt"]
64577
64986
  };
64578
64987
  /**
64579
64988
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64593,11 +65002,11 @@ var powerMeterCapability = {
64593
65002
  */
64594
65003
  var GpsLocationSchema = object({
64595
65004
  /** Latitude in decimal degrees, -90..90. */
64596
- latitude: number().min(-90).max(90),
65005
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64597
65006
  /** Longitude in decimal degrees, -180..180. */
64598
- longitude: number().min(-180).max(180),
65007
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64599
65008
  /** Reported accuracy in meters (lower = better). */
64600
- accuracyMeters: number().nonnegative()
65009
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64601
65010
  });
64602
65011
  var PresenceStatusSchema = object({
64603
65012
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64609,7 +65018,7 @@ var PresenceStatusSchema = object({
64609
65018
  gps: GpsLocationSchema.nullable(),
64610
65019
  /** Optional battery level of the tracking device (0..100). Null
64611
65020
  * when not reported. */
64612
- batteryPercent: number().min(0).max(100).nullable(),
65021
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64613
65022
  /** Ms epoch when the slice was last updated. */
64614
65023
  lastChangedAt: number()
64615
65024
  });
@@ -64659,19 +65068,10 @@ var presenceCapability = {
64659
65068
  * `sourceInfo`.
64660
65069
  */
64661
65070
  var PressureSensorStatusSchema = object({
64662
- /** Current pressure in hPa. */
64663
- hpa: number(),
65071
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
65072
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64664
65073
  /** Ms epoch when the slice was last updated. */
64665
- lastFetchedAt: number(),
64666
- /** Live display unit from the upstream source (e.g. HA
64667
- * `attributes.unit_of_measurement`). The UI prefers this over the
64668
- * role's canonical unit. Absent → fall back to the canonical unit. */
64669
- unit: string().optional(),
64670
- /** Suggested decimal places for numeric display.
64671
- * Populated live from the upstream source when provided (e.g. HA
64672
- * `attributes.suggested_display_precision`). Falls back to
64673
- * auto-formatting when absent. */
64674
- precision: number().int().min(0).max(10).optional()
65074
+ lastFetchedAt: number()
64675
65075
  });
64676
65076
  var pressureSensorCapability = {
64677
65077
  name: "pressure-sensor",
@@ -65085,8 +65485,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
65085
65485
  }), _void(), { kind: "mutation" });
65086
65486
  var PtzAutotrackSettingsSchema = object({
65087
65487
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
65088
- stopDelaySeconds: number().int().min(0).max(300),
65089
- disappearDelaySeconds: number().int().min(0).max(300)
65488
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65489
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
65090
65490
  });
65091
65491
  /**
65092
65492
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65240,7 +65640,7 @@ var RunningStateStatusSchema = object({
65240
65640
  /** null = unknown — never coerced to false (D393). */
65241
65641
  running: boolean().nullable(),
65242
65642
  /** Epoch ms the current boolean began; null while `running` is null. */
65243
- since: number().nullable(),
65643
+ since: number().nullable().meta(readingMeta("none")),
65244
65644
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65245
65645
  phase: string().nullable(),
65246
65646
  /** null until a reading has been published (D393). */
@@ -65255,7 +65655,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65255
65655
  /** Generic Customize target for the running boolean (D936). */
65256
65656
  var runningCustomize = {
65257
65657
  version: 1,
65258
- advanced: "derive",
65259
65658
  targets: [{
65260
65659
  id: "running",
65261
65660
  cardinality: "scalar",
@@ -65275,7 +65674,7 @@ var runningCustomize = {
65275
65674
  DeviceType.Thermostat,
65276
65675
  DeviceType.PetFeeder
65277
65676
  ],
65278
- apply: "add-or-replace",
65677
+ apply: "add",
65279
65678
  path: "running",
65280
65679
  sources: [
65281
65680
  {
@@ -66539,9 +66938,9 @@ var RecordTriggerSchema = _enum([
66539
66938
  */
66540
66939
  var RecordWindowSchema = object({
66541
66940
  trigger: RecordTriggerSchema,
66542
- day: number().int().min(0).max(6),
66543
- startMinute: number().int().min(0).max(1439),
66544
- endMinute: number().int().min(1).max(1440)
66941
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66942
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66943
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
66545
66944
  });
66546
66945
  /** Status of one physical volume, as the camera itself describes it. */
66547
66946
  var OnboardStorageVolumeSchema = object({
@@ -66617,10 +67016,10 @@ var RecordingOnboardStatusSchema = object({
66617
67016
  /** Master "record to the card at all" switch. */
66618
67017
  enabled: boolean().nullable(),
66619
67018
  overwriteWhenFull: boolean().nullable(),
66620
- preRecordSec: number().nullable(),
66621
- postRecordSec: number().nullable(),
67019
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
67020
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66622
67021
  /** Length of one recorded file, in minutes. */
66623
- segmentMinutes: number().nullable(),
67022
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66624
67023
  /** The primary track's weekly windows, flattened. */
66625
67024
  windows: array(RecordWindowSchema),
66626
67025
  /**
@@ -66636,7 +67035,7 @@ var RecordingOnboardStatusSchema = object({
66636
67035
  * provider reporting a non-zero count also reports the schedule as not
66637
67036
  * writable.
66638
67037
  */
66639
- unreadableWindows: number(),
67038
+ unreadableWindows: number().meta(readingMeta("count")),
66640
67039
  /**
66641
67040
  * The camera is scheduled to record and has NO usable storage.
66642
67041
  *
@@ -67481,7 +67880,7 @@ var CamStreamDescriptorSchema = object({
67481
67880
  url: string().optional(),
67482
67881
  codec: string().optional(),
67483
67882
  resolution: CamStreamResolutionSchema.optional(),
67484
- fps: number().positive().optional(),
67883
+ fps: number().positive().optional().meta(readingMeta("rate")),
67485
67884
  label: string().optional(),
67486
67885
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
67487
67886
  deviceFeatures: array(string()).optional(),
@@ -67568,18 +67967,18 @@ var StreamProfileSchema = _enum([
67568
67967
  ]);
67569
67968
  /** The current encoding config of one stream profile. */
67570
67969
  var StreamProfileConfigSchema = object({
67571
- width: number(),
67572
- height: number(),
67970
+ width: number().meta(readingMeta("none")),
67971
+ height: number().meta(readingMeta("none")),
67573
67972
  codec: _enum(["h264", "h265"]),
67574
- framerate: number(),
67575
- bitrate: number(),
67973
+ framerate: number().meta(readingMeta("rate")),
67974
+ bitrate: number().meta(readingMeta("bitrate")),
67576
67975
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
67577
67976
  encoderProfile: _enum([
67578
67977
  "high",
67579
67978
  "main",
67580
67979
  "baseline"
67581
67980
  ]).optional(),
67582
- gop: number().optional(),
67981
+ gop: number().optional().meta(readingMeta("none")),
67583
67982
  /**
67584
67983
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
67585
67984
  *
@@ -67800,12 +68199,7 @@ var switchCapability = {
67800
68199
  label: "On",
67801
68200
  kind: "boolean"
67802
68201
  }
67803
- }] },
67804
- customize: {
67805
- version: 1,
67806
- advanced: "derive",
67807
- targets: []
67808
- }
68202
+ }] }
67809
68203
  };
67810
68204
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67811
68205
  function hasControlCharacter(value) {
@@ -68388,35 +68782,21 @@ var tamperCapability = {
68388
68782
  * entries with `device_class: temperature` and any future native
68389
68783
  * thermometer.
68390
68784
  *
68391
- * Unit normalisation: providers convert to Celsius before storing.
68392
- * The slice value is always Celsius so cross-cap aggregators
68393
- * (climate-control's `currentTemp`, energy analytics) can compose
68394
- * without per-source unit fixups. The canonical display unit (`°C`) is
68395
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68396
- * `sourceInfo`.
68785
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68786
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
68397
68787
  *
68398
68788
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
68399
68789
  * frozen feed (provider hung) distinct from a "temperature hasn't
68400
68790
  * changed" steady state.
68401
68791
  */
68402
68792
  var TemperatureSensorStatusSchema = object({
68403
- /** Current temperature in Celsius. */
68404
- celsius: number().meta({
68405
- unit: "°C",
68793
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68794
+ celsius: number().nullable().meta(readingMeta("temperature", {
68795
+ precision: 1,
68406
68796
  label: "Temperature"
68407
- }),
68797
+ })),
68408
68798
  /** Ms epoch when the slice was last updated (push or poll). */
68409
- lastFetchedAt: number(),
68410
- /** Live display unit from the upstream source (e.g. HA
68411
- * `attributes.unit_of_measurement`). The UI prefers this over the
68412
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
68413
- * Absent → the UI falls back to the role's canonical unit. */
68414
- unit: string().optional(),
68415
- /** Suggested decimal places for numeric display.
68416
- * Populated live from the upstream source when provided (e.g. HA
68417
- * `attributes.suggested_display_precision`). Falls back to
68418
- * auto-formatting when absent. */
68419
- precision: number().int().min(0).max(10).optional()
68799
+ lastFetchedAt: number()
68420
68800
  });
68421
68801
  var temperatureSensorCapability = {
68422
68802
  name: "temperature-sensor",
@@ -68447,12 +68827,7 @@ var temperatureSensorCapability = {
68447
68827
  } } },
68448
68828
  /** Clock fields: written, but excluded from the compare that decides
68449
68829
  * whether persisting is worth a SQLite commit. */
68450
- volatileStateFields: ["lastFetchedAt"],
68451
- customize: {
68452
- version: 1,
68453
- advanced: "derive",
68454
- targets: []
68455
- }
68830
+ volatileStateFields: ["lastFetchedAt"]
68456
68831
  };
68457
68832
  /**
68458
68833
  * toast — system-scoped singleton capability that streams toast
@@ -68735,16 +69110,16 @@ var VacuumControlStatusSchema = object({
68735
69110
  ]),
68736
69111
  /** 0..100 battery percentage. Null when the device has no battery
68737
69112
  * reading. */
68738
- batteryLevel: number().min(0).max(100).nullable(),
69113
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68739
69114
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68740
69115
  * the vacuum has no speed control. */
68741
69116
  fanSpeed: string().nullable(),
68742
69117
  /** Speed tokens the hardware accepts — drives the UI selector. */
68743
69118
  availableFanSpeeds: array(string()),
68744
69119
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68745
- progressPercent: number().min(0).max(100).nullable(),
69120
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68746
69121
  /** Current error code (0 / null = no error). */
68747
- errorCode: number().nullable(),
69122
+ errorCode: number().nullable().meta(readingMeta("none")),
68748
69123
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68749
69124
  errorLabel: string().nullable(),
68750
69125
  /** Ms epoch when the slice was last updated. */
@@ -68822,7 +69197,7 @@ var ValveStatusSchema = object({
68822
69197
  ]),
68823
69198
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68824
69199
  * intermediate position surface. */
68825
- position: number().min(0).max(100).nullable(),
69200
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68826
69201
  /** Ms epoch when the slice was last updated. */
68827
69202
  lastChangedAt: number()
68828
69203
  });
@@ -68930,9 +69305,9 @@ var vibrationCapability = {
68930
69305
  */
68931
69306
  var WaterHeaterStatusSchema = object({
68932
69307
  /** Current measured temperature. Null when not reported. */
68933
- currentTemp: number().nullable(),
69308
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68934
69309
  /** Target temperature setpoint. Null when no setpoint surface. */
68935
- targetTemp: number().nullable(),
69310
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68936
69311
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68937
69312
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68938
69313
  * device reports an unknown state. */
@@ -68943,9 +69318,9 @@ var WaterHeaterStatusSchema = object({
68943
69318
  * has no away surface. */
68944
69319
  away: boolean().nullable(),
68945
69320
  /** HA `min_temp` attribute. Null when not reported. */
68946
- minTemp: number().nullable(),
69321
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68947
69322
  /** HA `max_temp` attribute. Null when not reported. */
68948
- maxTemp: number().nullable(),
69323
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68949
69324
  /** Ms epoch when the slice was last updated. */
68950
69325
  lastChangedAt: number()
68951
69326
  });
@@ -69030,25 +69405,18 @@ var waterHeaterCapability = {
69030
69405
  * Forecast deferred — current conditions only for v1.
69031
69406
  */
69032
69407
  var WeatherStatusSchema = object({
69033
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69034
- * when no condition has been reported yet. */
69408
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69035
69409
  condition: string().nullable(),
69036
- /** Current temperature in the reported unit. Null when not provided. */
69037
- temperature: number().nullable(),
69038
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69039
- temperatureUnit: string().nullable(),
69410
+ /** Current temperature in Celsius. Null when not provided or refused. */
69411
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69040
69412
  /** Relative humidity (0..100). Null when not provided. */
69041
- humidity: number().min(0).max(100).nullable(),
69042
- /** Barometric pressure in the reported unit. Null when not provided. */
69043
- pressure: number().nullable(),
69044
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69045
- pressureUnit: string().nullable(),
69046
- /** Wind speed in the reported unit. Null when not provided. */
69047
- windSpeed: number().nullable(),
69048
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69049
- windSpeedUnit: string().nullable(),
69413
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
69414
+ /** Barometric pressure in hPa. Null when not provided or refused. */
69415
+ pressure: number().nullable().meta(readingMeta("pressure")),
69416
+ /** Wind speed in metres per second. Null when not provided or refused. */
69417
+ windSpeed: number().nullable().meta(readingMeta("speed")),
69050
69418
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69051
- windBearing: number().nullable(),
69419
+ windBearing: number().nullable().meta(readingMeta("angle")),
69052
69420
  /** Ms epoch when the slice was last updated. */
69053
69421
  lastFetchedAt: number()
69054
69422
  });
@@ -69076,12 +69444,12 @@ var weatherCapability = {
69076
69444
  */
69077
69445
  durability: "session",
69078
69446
  /**
69079
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69447
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69080
69448
  */
69081
69449
  history: { fields: {
69082
69450
  temperature: {
69083
69451
  kind: "gauge",
69084
- unitFrom: "temperatureUnit",
69452
+ unit: "°C",
69085
69453
  deadband: .2
69086
69454
  },
69087
69455
  humidity: {
@@ -69091,12 +69459,12 @@ var weatherCapability = {
69091
69459
  },
69092
69460
  pressure: {
69093
69461
  kind: "gauge",
69094
- unitFrom: "pressureUnit",
69462
+ unit: "hPa",
69095
69463
  deadband: .5
69096
69464
  },
69097
69465
  windSpeed: {
69098
69466
  kind: "gauge",
69099
- unitFrom: "windSpeedUnit",
69467
+ unit: "m/s",
69100
69468
  deadband: .5
69101
69469
  }
69102
69470
  } }
@@ -70327,6 +70695,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
70327
70695
  featureProbe: featureProbeCapability,
70328
70696
  flood: floodCapability,
70329
70697
  gas: gasCapability,
70698
+ groupCommands: groupCommandsCapability,
70699
+ groupStatus: groupStatusCapability,
70330
70700
  heatingValve: heatingValveCapability,
70331
70701
  humidifier: humidifierCapability,
70332
70702
  humiditySensor: humiditySensorCapability,
@@ -71703,6 +72073,13 @@ Object.freeze({
71703
72073
  access: "view",
71704
72074
  role: "admin"
71705
72075
  },
72076
+ "addons.getPackageDocs": {
72077
+ capName: "addons",
72078
+ capScope: "system",
72079
+ addonId: null,
72080
+ access: "view",
72081
+ role: "admin"
72082
+ },
71706
72083
  "addons.getVersions": {
71707
72084
  capName: "addons",
71708
72085
  capScope: "system",
@@ -74811,6 +75188,20 @@ Object.freeze({
74811
75188
  access: "view",
74812
75189
  role: "viewer"
74813
75190
  },
75191
+ "groupCommands.getStatus": {
75192
+ capName: "group-commands",
75193
+ capScope: "device",
75194
+ addonId: null,
75195
+ access: "view",
75196
+ role: "viewer"
75197
+ },
75198
+ "groupStatus.getStatus": {
75199
+ capName: "group-status",
75200
+ capScope: "device",
75201
+ addonId: null,
75202
+ access: "view",
75203
+ role: "viewer"
75204
+ },
74814
75205
  "heatingValve.getStatus": {
74815
75206
  capName: "heating-valve",
74816
75207
  capScope: "device",
@@ -84633,6 +85024,37 @@ var SENSOR_CAP_VOCABULARY = {
84633
85024
  icon: "flame"
84634
85025
  }]
84635
85026
  },
85027
+ "group-commands": {
85028
+ scope: "device",
85029
+ noun: t("Group command", "Comando di gruppo")
85030
+ },
85031
+ "group-status": {
85032
+ scope: "device",
85033
+ noun: t("Group", "Gruppo"),
85034
+ state: [
85035
+ {
85036
+ kind: "numeric",
85037
+ field: "included",
85038
+ quantity: t("the members reporting", "i membri che rispondono"),
85039
+ label: t("Reporting", "Rispondono"),
85040
+ icon: "users"
85041
+ },
85042
+ {
85043
+ kind: "numeric",
85044
+ field: "excluded",
85045
+ quantity: t("the members left out", "i membri esclusi"),
85046
+ label: t("Excluded", "Esclusi"),
85047
+ icon: "users"
85048
+ },
85049
+ {
85050
+ kind: "numeric",
85051
+ field: "total",
85052
+ quantity: t("the member count", "il numero di membri"),
85053
+ label: t("Members", "Membri"),
85054
+ icon: "sigma"
85055
+ }
85056
+ ]
85057
+ },
84636
85058
  "humidity-sensor": {
84637
85059
  scope: "device",
84638
85060
  noun: t("Humidity", "Umidità"),
@@ -84666,7 +85088,6 @@ var SENSOR_CAP_VOCABULARY = {
84666
85088
  field: "value",
84667
85089
  quantity: t("the reading", "il valore"),
84668
85090
  label: t("Reading", "Valore"),
84669
- unitField: "unit",
84670
85091
  icon: "gauge"
84671
85092
  }]
84672
85093
  },
@@ -84802,7 +85223,7 @@ var SENSOR_CAP_VOCABULARY = {
84802
85223
  field: "temperature",
84803
85224
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84804
85225
  label: t("Temperature", "Temperatura"),
84805
- unitField: "temperatureUnit",
85226
+ unit: "°C",
84806
85227
  icon: "thermometer"
84807
85228
  },
84808
85229
  {
@@ -84818,7 +85239,7 @@ var SENSOR_CAP_VOCABULARY = {
84818
85239
  field: "windSpeed",
84819
85240
  quantity: t("the wind speed", "la velocità del vento"),
84820
85241
  label: t("Wind", "Vento"),
84821
- unitField: "windSpeedUnit",
85242
+ unit: "m/s",
84822
85243
  icon: "wind"
84823
85244
  },
84824
85245
  {
@@ -84826,7 +85247,7 @@ var SENSOR_CAP_VOCABULARY = {
84826
85247
  field: "pressure",
84827
85248
  quantity: t("the air pressure", "la pressione atmosferica"),
84828
85249
  label: t("Pressure", "Pressione"),
84829
- unitField: "pressureUnit",
85250
+ unit: "hPa",
84830
85251
  icon: "gauge"
84831
85252
  }
84832
85253
  ]
@@ -86099,6 +86520,16 @@ Object.freeze({
86099
86520
  form: "single",
86100
86521
  optional: false
86101
86522
  }],
86523
+ "groupCommands.getStatus": [{
86524
+ name: "deviceId",
86525
+ form: "single",
86526
+ optional: false
86527
+ }],
86528
+ "groupStatus.getStatus": [{
86529
+ name: "deviceId",
86530
+ form: "single",
86531
+ optional: false
86532
+ }],
86102
86533
  "heatingValve.getStatus": [{
86103
86534
  name: "deviceId",
86104
86535
  form: "single",
@@ -88083,6 +88514,8 @@ new Map(Object.entries({
88083
88514
  "detection-blind": "device-events",
88084
88515
  "motion-noisy": "device-events",
88085
88516
  "motion-calm": "device-events",
88517
+ "package-llm-silent": "device-events",
88518
+ "package-llm-answered": "device-events",
88086
88519
  "recording-stalled": "device-events",
88087
88520
  "recording-healthy": "device-events",
88088
88521
  "recording-degraded": "device-events",
@@ -88166,6 +88599,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
88166
88599
  label: "Motion without a subject",
88167
88600
  kinds: ["motion-noisy", "motion-calm"]
88168
88601
  },
88602
+ {
88603
+ id: "package-llm",
88604
+ family: "device-events",
88605
+ label: "Package confirm",
88606
+ kinds: ["package-llm-silent", "package-llm-answered"]
88607
+ },
88169
88608
  {
88170
88609
  id: "recording",
88171
88610
  family: "device-events",
@@ -88347,6 +88786,8 @@ var DEVICE_EVENTS = [
88347
88786
  "detection-blind",
88348
88787
  "motion-noisy",
88349
88788
  "motion-calm",
88789
+ "package-llm-silent",
88790
+ "package-llm-answered",
88350
88791
  "recording-stalled",
88351
88792
  "recording-healthy",
88352
88793
  "recording-degraded",
@@ -88615,6 +89056,15 @@ new Map([
88615
89056
  snapshotLabel: "Cameras with constant motion now",
88616
89057
  snapshotSection: null
88617
89058
  },
89059
+ {
89060
+ id: "package-llm-confirm",
89061
+ family: "device-events",
89062
+ label: "Package confirm (not answering / answering again)",
89063
+ bad: ["package-llm-silent"],
89064
+ recovery: ["package-llm-answered"],
89065
+ snapshotLabel: "Cameras whose package confirm is not answering",
89066
+ snapshotSection: null
89067
+ },
88618
89068
  {
88619
89069
  id: "recording-health",
88620
89070
  family: "device-events",