@camstack/addon-provider-reolink 1.2.210 → 1.2.211

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.mjs CHANGED
@@ -28,7 +28,7 @@ import { URL as URL$1 } from "url";
28
28
  import { spawn as spawn$1 } from "node:child_process";
29
29
  import os, { tmpdir } from "node:os";
30
30
  import { createReadStream } from "node:fs";
31
- //#region ../types/dist/event-category-DHXxB8t9.mjs
31
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
32
32
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
33
33
  EventCategory["SystemBoot"] = "system.boot";
34
34
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -354,6 +354,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
354
354
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
355
355
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
356
356
  /**
357
+ * A camera's package LLM confirm has failed closed three times in a row
358
+ * (D931, 2026-10-26). One edge per camera until
359
+ * {@link PipelinePackageLlmAnswered}.
360
+ */
361
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
362
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
363
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
364
+ /**
357
365
  * Per-camera pipeline config was mutated by the orchestrator
358
366
  * (a settings change via `setAgentInferenceDevices` /
359
367
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5463,7 +5471,7 @@ var ZodIssueCode = {
5463
5471
  var ZodFirstPartyTypeKind;
5464
5472
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5465
5473
  //#endregion
5466
- //#region ../types/dist/sleep-BErAxhul.mjs
5474
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5467
5475
  /**
5468
5476
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5469
5477
  * window to float samples (D455).
@@ -8021,6 +8029,99 @@ var RecordKindDeclarationSchema = object({
8021
8029
  }).strict();
8022
8030
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8023
8031
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8032
+ var READING_QUANTITIES = [
8033
+ "temperature",
8034
+ "pressure",
8035
+ "speed",
8036
+ "precipitation",
8037
+ "precipitation-rate",
8038
+ "length",
8039
+ "area",
8040
+ "illuminance",
8041
+ "irradiance",
8042
+ "power",
8043
+ "energy",
8044
+ "voltage",
8045
+ "current",
8046
+ "apparent-power",
8047
+ "humidity",
8048
+ "percentage",
8049
+ "concentration-volume",
8050
+ "concentration-mass",
8051
+ "concentration-ppb",
8052
+ "volume",
8053
+ "duration",
8054
+ "duration-ms",
8055
+ "information",
8056
+ "sound-level",
8057
+ "signal-level",
8058
+ "colour-temperature",
8059
+ "angle",
8060
+ "bitrate",
8061
+ "rate",
8062
+ "mass",
8063
+ "index",
8064
+ "ratio",
8065
+ "count",
8066
+ "none",
8067
+ "device"
8068
+ ];
8069
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8070
+ new Set(READING_QUANTITIES);
8071
+ /**
8072
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8073
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8074
+ */
8075
+ var QUANTITY_BASE = {
8076
+ temperature: "°C",
8077
+ pressure: "hPa",
8078
+ speed: "m/s",
8079
+ precipitation: "mm",
8080
+ "precipitation-rate": "mm/h",
8081
+ length: "m",
8082
+ area: "m²",
8083
+ illuminance: "lx",
8084
+ irradiance: "W/m²",
8085
+ power: "W",
8086
+ energy: "kWh",
8087
+ voltage: "V",
8088
+ current: "A",
8089
+ "apparent-power": "VA",
8090
+ humidity: "%",
8091
+ percentage: "%",
8092
+ "concentration-volume": "ppm",
8093
+ "concentration-mass": "µg/m³",
8094
+ "concentration-ppb": "ppb",
8095
+ volume: "L",
8096
+ duration: "s",
8097
+ "duration-ms": "ms",
8098
+ information: "B",
8099
+ "sound-level": "dBFS",
8100
+ "signal-level": "dBm",
8101
+ "colour-temperature": "mired",
8102
+ angle: "°",
8103
+ bitrate: "kbps",
8104
+ rate: "fps",
8105
+ mass: "kg",
8106
+ index: "none",
8107
+ ratio: "none",
8108
+ count: "none",
8109
+ none: "none",
8110
+ device: "device"
8111
+ };
8112
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8113
+ function baseUnitOf(quantity) {
8114
+ const symbol = QUANTITY_BASE[quantity];
8115
+ if (symbol === "none" || symbol === "device") return void 0;
8116
+ return symbol;
8117
+ }
8118
+ function readingMeta(quantity, extras) {
8119
+ return {
8120
+ quantity,
8121
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8122
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8123
+ };
8124
+ }
8024
8125
  /** Shared Zod schemas used across streaming capabilities. */
8025
8126
  var CamProfileSchema = _enum([
8026
8127
  "high",
@@ -8048,8 +8149,8 @@ var CamStreamKindSchema = _enum([
8048
8149
  "derived"
8049
8150
  ]);
8050
8151
  var CamStreamResolutionSchema = object({
8051
- width: number().int().positive(),
8052
- height: number().int().positive()
8152
+ width: number().int().positive().meta(readingMeta("none")),
8153
+ height: number().int().positive().meta(readingMeta("none"))
8053
8154
  });
8054
8155
  var CameraStreamSchema = object({
8055
8156
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8601,6 +8702,8 @@ var CAP_NAMES = Object.freeze([
8601
8702
  "filesystem-browse",
8602
8703
  "flood",
8603
8704
  "gas",
8705
+ "group-commands",
8706
+ "group-status",
8604
8707
  "heating-valve",
8605
8708
  "humidifier",
8606
8709
  "humidity-sensor",
@@ -8730,14 +8833,6 @@ Object.freeze({
8730
8833
  {
8731
8834
  "path": "aqi",
8732
8835
  "valueType": "number"
8733
- },
8734
- {
8735
- "path": "unit",
8736
- "valueType": "string"
8737
- },
8738
- {
8739
- "path": "precision",
8740
- "valueType": "number"
8741
8836
  }
8742
8837
  ],
8743
8838
  "alarm-panel": [
@@ -8773,20 +8868,10 @@ Object.freeze({
8773
8868
  "valueType": "boolean"
8774
8869
  }
8775
8870
  ],
8776
- "ambient-light-sensor": [
8777
- {
8778
- "path": "lux",
8779
- "valueType": "number"
8780
- },
8781
- {
8782
- "path": "unit",
8783
- "valueType": "string"
8784
- },
8785
- {
8786
- "path": "precision",
8787
- "valueType": "number"
8788
- }
8789
- ],
8871
+ "ambient-light-sensor": [{
8872
+ "path": "lux",
8873
+ "valueType": "number"
8874
+ }],
8790
8875
  "audio-metrics": [
8791
8876
  {
8792
8877
  "path": "ts",
@@ -8866,6 +8951,10 @@ Object.freeze({
8866
8951
  "path": "lastUpdated",
8867
8952
  "valueType": "number"
8868
8953
  },
8954
+ {
8955
+ "path": "volts",
8956
+ "valueType": "number"
8957
+ },
8869
8958
  {
8870
8959
  "path": "lastContactAt",
8871
8960
  "valueType": "number"
@@ -9108,10 +9197,6 @@ Object.freeze({
9108
9197
  "datetime"
9109
9198
  ]
9110
9199
  },
9111
- {
9112
- "path": "unit",
9113
- "valueType": "string"
9114
- },
9115
9200
  {
9116
9201
  "path": "min",
9117
9202
  "valueType": "number"
@@ -9123,19 +9208,6 @@ Object.freeze({
9123
9208
  {
9124
9209
  "path": "step",
9125
9210
  "valueType": "number"
9126
- },
9127
- {
9128
- "path": "precision",
9129
- "valueType": "number"
9130
- },
9131
- {
9132
- "path": "format",
9133
- "valueType": "enum",
9134
- "enumValues": [
9135
- "date",
9136
- "time",
9137
- "datetime"
9138
- ]
9139
9211
  }
9140
9212
  ],
9141
9213
  "cover": [
@@ -9376,13 +9448,8 @@ Object.freeze({
9376
9448
  "path": "value",
9377
9449
  "valueType": "string"
9378
9450
  }, {
9379
- "path": "format",
9380
- "valueType": "enum",
9381
- "enumValues": [
9382
- "date",
9383
- "time",
9384
- "datetime"
9385
- ]
9451
+ "path": "timestamp",
9452
+ "valueType": "boolean"
9386
9453
  }],
9387
9454
  "event-emitter": [
9388
9455
  {
@@ -9455,6 +9522,20 @@ Object.freeze({
9455
9522
  "path": "detected",
9456
9523
  "valueType": "boolean"
9457
9524
  }],
9525
+ "group-status": [
9526
+ {
9527
+ "path": "included",
9528
+ "valueType": "number"
9529
+ },
9530
+ {
9531
+ "path": "excluded",
9532
+ "valueType": "number"
9533
+ },
9534
+ {
9535
+ "path": "total",
9536
+ "valueType": "number"
9537
+ }
9538
+ ],
9458
9539
  "heating-valve": [{
9459
9540
  "path": "position",
9460
9541
  "valueType": "number"
@@ -9489,20 +9570,10 @@ Object.freeze({
9489
9570
  "valueType": "number"
9490
9571
  }
9491
9572
  ],
9492
- "humidity-sensor": [
9493
- {
9494
- "path": "percent",
9495
- "valueType": "number"
9496
- },
9497
- {
9498
- "path": "unit",
9499
- "valueType": "string"
9500
- },
9501
- {
9502
- "path": "precision",
9503
- "valueType": "number"
9504
- }
9505
- ],
9573
+ "humidity-sensor": [{
9574
+ "path": "percent",
9575
+ "valueType": "number"
9576
+ }],
9506
9577
  "image": [{
9507
9578
  "path": "url",
9508
9579
  "valueType": "string"
@@ -9841,20 +9912,10 @@ Object.freeze({
9841
9912
  "valueType": "boolean"
9842
9913
  }
9843
9914
  ],
9844
- "numeric-sensor": [
9845
- {
9846
- "path": "value",
9847
- "valueType": "number"
9848
- },
9849
- {
9850
- "path": "unit",
9851
- "valueType": "string"
9852
- },
9853
- {
9854
- "path": "precision",
9855
- "valueType": "number"
9856
- }
9857
- ],
9915
+ "numeric-sensor": [{
9916
+ "path": "value",
9917
+ "valueType": "number"
9918
+ }],
9858
9919
  "pet-feeder": [
9859
9920
  {
9860
9921
  "path": "foodLevel",
@@ -9946,14 +10007,6 @@ Object.freeze({
9946
10007
  {
9947
10008
  "path": "powerFactor",
9948
10009
  "valueType": "number"
9949
- },
9950
- {
9951
- "path": "unit",
9952
- "valueType": "string"
9953
- },
9954
- {
9955
- "path": "precision",
9956
- "valueType": "number"
9957
10010
  }
9958
10011
  ],
9959
10012
  "presence": [
@@ -9982,20 +10035,10 @@ Object.freeze({
9982
10035
  "valueType": "number"
9983
10036
  }
9984
10037
  ],
9985
- "pressure-sensor": [
9986
- {
9987
- "path": "hpa",
9988
- "valueType": "number"
9989
- },
9990
- {
9991
- "path": "unit",
9992
- "valueType": "string"
9993
- },
9994
- {
9995
- "path": "precision",
9996
- "valueType": "number"
9997
- }
9998
- ],
10038
+ "pressure-sensor": [{
10039
+ "path": "hpa",
10040
+ "valueType": "number"
10041
+ }],
9999
10042
  "privacy-mask": [{
10000
10043
  "path": "enabled",
10001
10044
  "valueType": "boolean"
@@ -10306,20 +10349,10 @@ Object.freeze({
10306
10349
  "path": "tampered",
10307
10350
  "valueType": "boolean"
10308
10351
  }],
10309
- "temperature-sensor": [
10310
- {
10311
- "path": "celsius",
10312
- "valueType": "number"
10313
- },
10314
- {
10315
- "path": "unit",
10316
- "valueType": "string"
10317
- },
10318
- {
10319
- "path": "precision",
10320
- "valueType": "number"
10321
- }
10322
- ],
10352
+ "temperature-sensor": [{
10353
+ "path": "celsius",
10354
+ "valueType": "number"
10355
+ }],
10323
10356
  "update": [
10324
10357
  {
10325
10358
  "path": "currentVersion",
@@ -10430,10 +10463,6 @@ Object.freeze({
10430
10463
  "path": "temperature",
10431
10464
  "valueType": "number"
10432
10465
  },
10433
- {
10434
- "path": "temperatureUnit",
10435
- "valueType": "string"
10436
- },
10437
10466
  {
10438
10467
  "path": "humidity",
10439
10468
  "valueType": "number"
@@ -10442,18 +10471,10 @@ Object.freeze({
10442
10471
  "path": "pressure",
10443
10472
  "valueType": "number"
10444
10473
  },
10445
- {
10446
- "path": "pressureUnit",
10447
- "valueType": "string"
10448
- },
10449
10474
  {
10450
10475
  "path": "windSpeed",
10451
10476
  "valueType": "number"
10452
10477
  },
10453
- {
10454
- "path": "windSpeedUnit",
10455
- "valueType": "string"
10456
- },
10457
10478
  {
10458
10479
  "path": "windBearing",
10459
10480
  "valueType": "number"
@@ -10884,7 +10905,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
10884
10905
  return DeviceRole;
10885
10906
  }({});
10886
10907
  //#endregion
10887
- //#region ../types/dist/composition-kEl9mevu.mjs
10908
+ //#region ../types/dist/composition-OhpuespA.mjs
10888
10909
  /**
10889
10910
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10890
10911
  *
@@ -11639,15 +11660,74 @@ function validateExpressionSource(src) {
11639
11660
  }
11640
11661
  return null;
11641
11662
  }
11642
- /** A composed device's stableId is this prefix plus the block id. */
11643
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11644
- /** Longest snippet a `code` source may carry (slice 4). */
11645
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11663
+ /**
11664
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11665
+ * id. Split out so helper schemas can name it without a cycle through
11666
+ * `composition.ts` (D659).
11667
+ */
11646
11668
  var CompositionSourceRefSchema = object({
11647
11669
  /** The owning addon; `stableId` is unique only within it. */
11648
11670
  addonId: string().min(1),
11649
11671
  stableId: string().min(1)
11650
11672
  });
11673
+ var GROUP_TEMPLATE_IDS = [
11674
+ "temperature",
11675
+ "humidity",
11676
+ "pressure",
11677
+ "illuminance",
11678
+ "power",
11679
+ "energy",
11680
+ "battery",
11681
+ "numeric",
11682
+ "motion",
11683
+ "contact",
11684
+ "leak",
11685
+ "smoke",
11686
+ "occupancy",
11687
+ "binary",
11688
+ "switch",
11689
+ "light"
11690
+ ];
11691
+ var NUMERIC_AGGREGATES = [
11692
+ "mean",
11693
+ "median",
11694
+ "min",
11695
+ "max",
11696
+ "sum"
11697
+ ];
11698
+ var BINARY_AGGREGATES = ["any", "all"];
11699
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11700
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11701
+ var AGGREGATE_REDUCERS = [
11702
+ "mean",
11703
+ "median",
11704
+ "min",
11705
+ "max",
11706
+ "sum",
11707
+ "any",
11708
+ "all"
11709
+ ];
11710
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11711
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11712
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11713
+ /** One member. The field is stored so occupancy can name motion or binary. */
11714
+ var HelperMemberSchema = object({
11715
+ source: CompositionSourceRefSchema,
11716
+ cap: string().min(1),
11717
+ fieldPath: string().min(1)
11718
+ });
11719
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11720
+ kind: literal("group"),
11721
+ templateId: GroupTemplateIdSchema,
11722
+ aggregate: AggregateReducerSchema,
11723
+ members: array(HelperMemberSchema).min(1).max(64),
11724
+ /** Actuator groups only. Readings leave this unset. */
11725
+ active: ActiveCriterionSchema.optional()
11726
+ })]);
11727
+ /** A composed device's stableId is this prefix plus the block id. */
11728
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11729
+ /** Longest snippet a `code` source may carry (slice 4). */
11730
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11651
11731
  /** One field of one capability of one source device. */
11652
11732
  var CompositionFieldReadSchema = object({
11653
11733
  source: CompositionSourceRefSchema,
@@ -11691,7 +11771,33 @@ var CompositionCodeSourceSchema = object({
11691
11771
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11692
11772
  CompositionFromSourceSchema,
11693
11773
  CompositionExpressionSourceSchema,
11694
- CompositionCodeSourceSchema
11774
+ CompositionCodeSourceSchema,
11775
+ object({
11776
+ kind: literal("aggregate"),
11777
+ reducer: _enum([
11778
+ "mean",
11779
+ "median",
11780
+ "min",
11781
+ "max",
11782
+ "sum",
11783
+ "any",
11784
+ "all"
11785
+ ]),
11786
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11787
+ /** `value` is the reading; the others are the member counts. */
11788
+ publish: _enum([
11789
+ "value",
11790
+ "included",
11791
+ "excluded",
11792
+ "total"
11793
+ ]),
11794
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11795
+ empty: _enum([
11796
+ "null",
11797
+ "omit",
11798
+ "offline"
11799
+ ])
11800
+ })
11695
11801
  ]);
11696
11802
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11697
11803
  kind: literal("forward"),
@@ -11737,7 +11843,13 @@ var CompositionSchema = object({
11737
11843
  kind: literal("existing"),
11738
11844
  device: CompositionSourceRefSchema
11739
11845
  })]),
11740
- features: array(CompositionFeatureSchema).min(1).max(16)
11846
+ features: array(CompositionFeatureSchema).min(1).max(16),
11847
+ /**
11848
+ * Present when the block is a helper template. Absent on a free
11849
+ * composition (the room example, device 4603). The editor reads this,
11850
+ * never the compiled features.
11851
+ */
11852
+ helper: HelperSpecSchema.optional()
11741
11853
  }).superRefine((composition, ctx) => {
11742
11854
  const seen = /* @__PURE__ */ new Set();
11743
11855
  composition.features.forEach((feature, index) => {
@@ -18515,7 +18627,11 @@ var CompositionProblemSchema = object({
18515
18627
  "item-field-reserved",
18516
18628
  "target-missing",
18517
18629
  "target-unreadable",
18518
- "not-replaceable"
18630
+ "not-replaceable",
18631
+ "native-capability-replace",
18632
+ "mixed-quantity",
18633
+ "helper-mismatch",
18634
+ "member-constraint"
18519
18635
  ]),
18520
18636
  path: string(),
18521
18637
  message: string()
@@ -18536,7 +18652,8 @@ var CompositionUnavailableModeSchema = _enum([
18536
18652
  "null",
18537
18653
  "offline",
18538
18654
  "unknown",
18539
- "release"
18655
+ "release",
18656
+ "omit"
18540
18657
  ]);
18541
18658
  /**
18542
18659
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18573,9 +18690,11 @@ var CompositionFieldPlanSchema = object({
18573
18690
  reads: array(CompositionFieldReadSchema)
18574
18691
  });
18575
18692
  /**
18576
- * What a feature does to its target (D663): `new` builds a capability on a new
18577
- * device; on an existing device it `add`s a capability the device lacks, or
18578
- * `replace`s named fields of one it has natively.
18693
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18694
+ * new device; on an existing device it `add`s a capability the device lacks.
18695
+ * Replacing a capability the device already serves is refused
18696
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18697
+ * wire for a claim the hub already holds.
18579
18698
  */
18580
18699
  var CompositionFeatureModeSchema = _enum([
18581
18700
  "new",
@@ -18748,6 +18867,275 @@ Object.freeze({
18748
18867
  result: ["number", "null"]
18749
18868
  }
18750
18869
  });
18870
+ /**
18871
+ * One catalog row per base reading a group helper can build (D944 §3).
18872
+ * Data, not a form default written into the block.
18873
+ */
18874
+ var NOW = {
18875
+ fieldPath: "lastFetchedAt",
18876
+ expr: "now"
18877
+ };
18878
+ var CHANGED = {
18879
+ fieldPath: "lastChangedAt",
18880
+ expr: "now"
18881
+ };
18882
+ function numeric(input) {
18883
+ return {
18884
+ ...input,
18885
+ family: "numeric",
18886
+ deviceType: DeviceType.Sensor,
18887
+ memberDeviceType: DeviceType.Sensor,
18888
+ commandCaps: [],
18889
+ baseUnit: baseUnitOf(input.quantity),
18890
+ aggregates: NUMERIC_AGGREGATES,
18891
+ accept: [{
18892
+ cap: input.outputCap,
18893
+ fieldPath: input.outputField
18894
+ }]
18895
+ };
18896
+ }
18897
+ function binary(input) {
18898
+ return {
18899
+ ...input,
18900
+ family: "binary",
18901
+ deviceType: DeviceType.Sensor,
18902
+ memberDeviceType: DeviceType.Sensor,
18903
+ commandCaps: [],
18904
+ quantity: "none",
18905
+ baseUnit: void 0,
18906
+ aggregates: BINARY_AGGREGATES,
18907
+ empty: "offline"
18908
+ };
18909
+ }
18910
+ var GROUP_TEMPLATES = [
18911
+ numeric({
18912
+ id: "temperature",
18913
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18914
+ role: DeviceRole.TemperatureSensor,
18915
+ outputCap: "temperature-sensor",
18916
+ outputField: "celsius",
18917
+ quantity: "temperature",
18918
+ defaultAggregate: "mean",
18919
+ empty: "null",
18920
+ companions: [NOW]
18921
+ }),
18922
+ numeric({
18923
+ id: "humidity",
18924
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18925
+ role: DeviceRole.HumiditySensor,
18926
+ outputCap: "humidity-sensor",
18927
+ outputField: "percent",
18928
+ quantity: "humidity",
18929
+ defaultAggregate: "mean",
18930
+ empty: "null",
18931
+ companions: [NOW]
18932
+ }),
18933
+ numeric({
18934
+ id: "pressure",
18935
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18936
+ role: DeviceRole.PressureSensor,
18937
+ outputCap: "pressure-sensor",
18938
+ outputField: "hpa",
18939
+ quantity: "pressure",
18940
+ defaultAggregate: "mean",
18941
+ empty: "null",
18942
+ companions: [NOW]
18943
+ }),
18944
+ numeric({
18945
+ id: "illuminance",
18946
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18947
+ role: DeviceRole.AmbientLightSensor,
18948
+ outputCap: "ambient-light-sensor",
18949
+ outputField: "lux",
18950
+ quantity: "illuminance",
18951
+ defaultAggregate: "mean",
18952
+ empty: "null",
18953
+ companions: [NOW]
18954
+ }),
18955
+ numeric({
18956
+ id: "power",
18957
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18958
+ role: DeviceRole.PowerSensor,
18959
+ outputCap: "power-meter",
18960
+ outputField: "watts",
18961
+ quantity: "power",
18962
+ defaultAggregate: "sum",
18963
+ empty: "omit",
18964
+ companions: [NOW]
18965
+ }),
18966
+ numeric({
18967
+ id: "energy",
18968
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18969
+ role: DeviceRole.EnergySensor,
18970
+ outputCap: "power-meter",
18971
+ outputField: "kwhTotal",
18972
+ quantity: "energy",
18973
+ defaultAggregate: "sum",
18974
+ empty: "omit",
18975
+ companions: [NOW]
18976
+ }),
18977
+ numeric({
18978
+ id: "battery",
18979
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18980
+ role: DeviceRole.BatterySensor,
18981
+ outputCap: "battery",
18982
+ outputField: "percentage",
18983
+ quantity: "percentage",
18984
+ defaultAggregate: "mean",
18985
+ empty: "null",
18986
+ companions: [
18987
+ {
18988
+ fieldPath: "charging",
18989
+ expr: "\"unknown\""
18990
+ },
18991
+ {
18992
+ fieldPath: "sleeping",
18993
+ expr: "false"
18994
+ },
18995
+ {
18996
+ fieldPath: "lastUpdated",
18997
+ expr: "now"
18998
+ }
18999
+ ]
19000
+ }),
19001
+ numeric({
19002
+ id: "numeric",
19003
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
19004
+ role: DeviceRole.NumericSensor,
19005
+ outputCap: "numeric-sensor",
19006
+ outputField: "value",
19007
+ quantity: "device",
19008
+ defaultAggregate: "mean",
19009
+ empty: "null",
19010
+ companions: [NOW]
19011
+ }),
19012
+ binary({
19013
+ id: "motion",
19014
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
19015
+ role: DeviceRole.MotionSensor,
19016
+ outputCap: "motion",
19017
+ outputField: "detected",
19018
+ defaultAggregate: "any",
19019
+ accept: [{
19020
+ cap: "motion",
19021
+ fieldPath: "detected"
19022
+ }],
19023
+ companions: [{
19024
+ fieldPath: "lastDetectedAt",
19025
+ expr: "null"
19026
+ }, {
19027
+ fieldPath: "autoClearAfterMs",
19028
+ expr: "null"
19029
+ }]
19030
+ }),
19031
+ binary({
19032
+ id: "contact",
19033
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
19034
+ role: DeviceRole.ContactSensor,
19035
+ outputCap: "contact",
19036
+ outputField: "entryOpen",
19037
+ defaultAggregate: "any",
19038
+ accept: [{
19039
+ cap: "contact",
19040
+ fieldPath: "entryOpen"
19041
+ }],
19042
+ companions: [CHANGED]
19043
+ }),
19044
+ binary({
19045
+ id: "leak",
19046
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
19047
+ role: DeviceRole.LeakSensor,
19048
+ outputCap: "flood",
19049
+ outputField: "flooded",
19050
+ defaultAggregate: "any",
19051
+ accept: [{
19052
+ cap: "flood",
19053
+ fieldPath: "flooded"
19054
+ }],
19055
+ companions: [CHANGED]
19056
+ }),
19057
+ binary({
19058
+ id: "smoke",
19059
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
19060
+ role: DeviceRole.SmokeSensor,
19061
+ outputCap: "smoke",
19062
+ outputField: "detected",
19063
+ defaultAggregate: "any",
19064
+ accept: [{
19065
+ cap: "smoke",
19066
+ fieldPath: "detected"
19067
+ }],
19068
+ companions: [CHANGED]
19069
+ }),
19070
+ binary({
19071
+ id: "occupancy",
19072
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
19073
+ role: DeviceRole.BinarySensor,
19074
+ outputCap: "binary",
19075
+ outputField: "on",
19076
+ defaultAggregate: "any",
19077
+ accept: [{
19078
+ cap: "motion",
19079
+ fieldPath: "detected"
19080
+ }, {
19081
+ cap: "binary",
19082
+ fieldPath: "on"
19083
+ }],
19084
+ companions: [CHANGED]
19085
+ }),
19086
+ binary({
19087
+ id: "binary",
19088
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
19089
+ role: DeviceRole.BinarySensor,
19090
+ outputCap: "binary",
19091
+ outputField: "on",
19092
+ defaultAggregate: "any",
19093
+ accept: [{
19094
+ cap: "binary",
19095
+ fieldPath: "on"
19096
+ }],
19097
+ companions: [CHANGED]
19098
+ }),
19099
+ actuator({
19100
+ id: "switch",
19101
+ 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.",
19102
+ deviceType: DeviceType.Switch,
19103
+ outputCap: "switch",
19104
+ commandCaps: ["switch"]
19105
+ }),
19106
+ actuator({
19107
+ id: "light",
19108
+ 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.",
19109
+ deviceType: DeviceType.Light,
19110
+ outputCap: "switch",
19111
+ commandCaps: [
19112
+ "switch",
19113
+ "brightness",
19114
+ "color"
19115
+ ]
19116
+ })
19117
+ ];
19118
+ function actuator(input) {
19119
+ return {
19120
+ ...input,
19121
+ family: "actuator",
19122
+ role: void 0,
19123
+ memberDeviceType: input.deviceType,
19124
+ outputField: "on",
19125
+ quantity: "none",
19126
+ baseUnit: void 0,
19127
+ aggregates: BINARY_AGGREGATES,
19128
+ defaultAggregate: "any",
19129
+ empty: "offline",
19130
+ accept: [{
19131
+ cap: "switch",
19132
+ fieldPath: "on"
19133
+ }],
19134
+ companions: [CHANGED]
19135
+ };
19136
+ }
19137
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
19138
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18751
19139
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18752
19140
  /**
18753
19141
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20574,12 +20962,12 @@ function buildDeviceAdminUrl(parts) {
20574
20962
  * providers detect renames by matching `uniqueId` and patch `id`
20575
20963
  * in-place so dispatch keeps working.
20576
20964
  *
20577
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20578
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20579
- * state push and are the single source of truth. The `device_class` tag is
20580
- * an HA-specific input that is consumed at the discovery boundary
20581
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20582
- * stored after that point.
20965
+ * **Display unit and precision are NOT stored here.** They are a read-time
20966
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20967
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20968
+ * neither is repeated on each sample. The `device_class` tag is an
20969
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20970
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20583
20971
  *
20584
20972
  * The `raw` escape hatch carries provider-specific attributes that don't
20585
20973
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20593,6 +20981,14 @@ var SourceInfoSchema = object({
20593
20981
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20594
20982
  * Used to detect rename when `id` changes. */
20595
20983
  uniqueId: string().optional(),
20984
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20985
+ quantity: ReadingQuantitySchema.optional(),
20986
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20987
+ dateTimeShape: _enum([
20988
+ "date",
20989
+ "time",
20990
+ "datetime"
20991
+ ]).optional(),
20596
20992
  /** Provider-specific extras that don't fit the structured slots.
20597
20993
  * Whitelist what goes here per provider — do NOT dump entire HA
20598
20994
  * attribute blobs (would explode the meta JSON column). */
@@ -41697,7 +42093,8 @@ method(object({
41697
42093
  "enum"
41698
42094
  ]),
41699
42095
  enumValues: array(string()).optional(),
41700
- item: boolean().optional()
42096
+ item: boolean().optional(),
42097
+ quantity: ReadingQuantitySchema.optional()
41701
42098
  })).readonly(),
41702
42099
  itemArray: object({
41703
42100
  path: string(),
@@ -45581,8 +45978,8 @@ var MaskPolygonShapeSchema = object({
45581
45978
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
45582
45979
  var MaskGridShapeSchema = object({
45583
45980
  kind: literal("grid"),
45584
- gridWidth: number(),
45585
- gridHeight: number(),
45981
+ gridWidth: number().meta(readingMeta("none")),
45982
+ gridHeight: number().meta(readingMeta("none")),
45586
45983
  cells: array(boolean())
45587
45984
  });
45588
45985
  discriminatedUnion("kind", [
@@ -45694,6 +46091,8 @@ var NcSystemEventKindSchema = _enum([
45694
46091
  "detection-blind",
45695
46092
  "motion-noisy",
45696
46093
  "motion-calm",
46094
+ "package-llm-silent",
46095
+ "package-llm-answered",
45697
46096
  "recording-stalled",
45698
46097
  "recording-healthy",
45699
46098
  "recording-degraded",
@@ -57307,6 +57706,28 @@ var PackageVersionInfoSchema = object({
57307
57706
  deprecated: string().optional(),
57308
57707
  distTags: array(string()).readonly()
57309
57708
  });
57709
+ /**
57710
+ * One published document. `ok` is markdown from the registry. `missing` means
57711
+ * the registry answered and the file was not there. `error` is a registry or
57712
+ * tarball failure — never an empty string standing in for either.
57713
+ */
57714
+ var PackageDocBodySchema = discriminatedUnion("status", [
57715
+ object({
57716
+ status: literal("ok"),
57717
+ markdown: string()
57718
+ }),
57719
+ object({ status: literal("missing") }),
57720
+ object({
57721
+ status: literal("error"),
57722
+ message: string()
57723
+ })
57724
+ ]);
57725
+ var PackageDocsSchema = object({
57726
+ name: string(),
57727
+ version: string(),
57728
+ readme: PackageDocBodySchema,
57729
+ changelog: PackageDocBodySchema
57730
+ });
57310
57731
  var SearchResultSchema = object({
57311
57732
  name: string(),
57312
57733
  version: string(),
@@ -57525,7 +57946,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57525
57946
  }), object({ success: literal(true) }), {
57526
57947
  kind: "mutation",
57527
57948
  auth: "admin"
57528
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57949
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57950
+ name: string().min(1),
57951
+ version: string().min(1).optional()
57952
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
57529
57953
  kind: "mutation",
57530
57954
  auth: "admin"
57531
57955
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -57584,27 +58008,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
57584
58008
  */
57585
58009
  var AirQualitySensorStatusSchema = object({
57586
58010
  /** Carbon dioxide concentration in ppm. */
57587
- co2Ppm: number().min(0).optional(),
58011
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
57588
58012
  /** Total volatile organic compounds in ppb. */
57589
- vocPpb: number().min(0).optional(),
58013
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
57590
58014
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
57591
- pm25: number().min(0).optional(),
58015
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
57592
58016
  /** Particulate matter ≤ 10 μm in µg/m³. */
57593
- pm10: number().min(0).optional(),
58017
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
57594
58018
  /** Composite AQI value (typically 0..500). */
57595
- aqi: number().optional(),
58019
+ aqi: number().optional().meta(readingMeta("index")),
57596
58020
  /** Ms epoch when the slice was last updated. */
57597
- lastFetchedAt: number(),
57598
- /** Live display unit of the single metric this slice carries (e.g. HA
57599
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
57600
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
57601
- * per slice is unambiguous. */
57602
- unit: string().optional(),
57603
- /** Suggested decimal places for numeric display.
57604
- * Populated live from the upstream source when provided (e.g. HA
57605
- * `attributes.suggested_display_precision`). Falls back to
57606
- * auto-formatting when absent. */
57607
- precision: number().int().min(0).max(10).optional()
58021
+ lastFetchedAt: number()
57608
58022
  });
57609
58023
  var airQualitySensorCapability = {
57610
58024
  name: "air-quality-sensor",
@@ -57641,12 +58055,12 @@ var airQualitySensorCapability = {
57641
58055
  },
57642
58056
  pm25: {
57643
58057
  kind: "gauge",
57644
- unit: "ug/m3",
58058
+ unit: "µg/m³",
57645
58059
  deadband: 1
57646
58060
  },
57647
58061
  pm10: {
57648
58062
  kind: "gauge",
57649
- unit: "ug/m3",
58063
+ unit: "µg/m³",
57650
58064
  deadband: 1
57651
58065
  },
57652
58066
  aqi: {
@@ -57664,22 +58078,13 @@ var airQualitySensorCapability = {
57664
58078
  * entries with `device_class: illuminance`.
57665
58079
  */
57666
58080
  var AmbientLightSensorStatusSchema = object({
57667
- /** Current illuminance in lux (lx). */
57668
- lux: number().min(0).meta({
57669
- unit: "lx",
58081
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
58082
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
58083
+ precision: 0,
57670
58084
  label: "Illuminance"
57671
- }),
58085
+ })),
57672
58086
  /** Ms epoch when the slice was last updated. */
57673
- lastFetchedAt: number(),
57674
- /** Live display unit from the upstream source (e.g. HA
57675
- * `attributes.unit_of_measurement`). The UI prefers this over the
57676
- * role's canonical unit. Absent → fall back to the canonical unit. */
57677
- unit: string().optional(),
57678
- /** Suggested decimal places for numeric display.
57679
- * Populated live from the upstream source when provided (e.g. HA
57680
- * `attributes.suggested_display_precision`). Falls back to
57681
- * auto-formatting when absent. */
57682
- precision: number().int().min(0).max(10).optional()
58087
+ lastFetchedAt: number()
57683
58088
  });
57684
58089
  var ambientLightSensorCapability = {
57685
58090
  name: "ambient-light-sensor",
@@ -57711,12 +58116,7 @@ var ambientLightSensorCapability = {
57711
58116
  } } },
57712
58117
  /** Clock fields: written, but excluded from the compare that decides
57713
58118
  * whether persisting is worth a SQLite commit. */
57714
- volatileStateFields: ["lastFetchedAt"],
57715
- customize: {
57716
- version: 1,
57717
- advanced: "derive",
57718
- targets: []
57719
- }
58119
+ volatileStateFields: ["lastFetchedAt"]
57720
58120
  };
57721
58121
  /**
57722
58122
  * Per-class audio metrics aggregated over a sliding window.
@@ -57943,10 +58343,7 @@ var BatteryStatusSchema = object({
57943
58343
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57944
58344
  * declines to band a non-finite reading, which is the correct shape.
57945
58345
  */
57946
- percentage: number().min(0).max(100).nullable().meta({
57947
- unit: "%",
57948
- label: "Charge"
57949
- }),
58346
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57950
58347
  /**
57951
58348
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57952
58349
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57973,7 +58370,14 @@ var BatteryStatusSchema = object({
57973
58370
  */
57974
58371
  sleeping: boolean().meta({ label: "Sleeping" }),
57975
58372
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57976
- lastUpdated: number(),
58373
+ lastUpdated: number().meta(readingMeta("none")),
58374
+ /**
58375
+ * Battery voltage in volts, for a cell the firmware reports as voltage
58376
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
58377
+ * that only report charge. A second reading on this cap, not a second
58378
+ * `numeric-sensor` on the same device.
58379
+ */
58380
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57977
58381
  /**
57978
58382
  * Ms epoch of the last time the device PROVED it was reachable — a
57979
58383
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -58025,7 +58429,6 @@ var BATTERY_DEVICE_TYPES = [
58025
58429
  /** Generic Customize target for the percentage reading (D936). */
58026
58430
  var batteryCustomize = {
58027
58431
  version: 1,
58028
- advanced: "derive",
58029
58432
  targets: [{
58030
58433
  id: "percentage",
58031
58434
  cardinality: "scalar",
@@ -58034,7 +58437,7 @@ var batteryCustomize = {
58034
58437
  stateField: "percentage"
58035
58438
  },
58036
58439
  deviceTypes: BATTERY_DEVICE_TYPES,
58037
- apply: "add-or-replace",
58440
+ apply: "add",
58038
58441
  path: "percentage",
58039
58442
  sources: [{
58040
58443
  kind: "field",
@@ -58155,10 +58558,16 @@ onStatusChanged: { data: object({
58155
58558
  /**
58156
58559
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
58157
58560
  */
58158
- history: { fields: { percentage: {
58159
- kind: "gauge",
58160
- unit: "%"
58161
- } } },
58561
+ history: { fields: {
58562
+ percentage: {
58563
+ kind: "gauge",
58564
+ unit: "%"
58565
+ },
58566
+ volts: {
58567
+ kind: "gauge",
58568
+ unit: "V"
58569
+ }
58570
+ } },
58162
58571
  customize: batteryCustomize,
58163
58572
  /** Clock fields: written, but excluded from the compare that decides
58164
58573
  * whether persisting is worth a SQLite commit. */
@@ -58205,12 +58614,7 @@ var binaryCapability = {
58205
58614
  history: { fields: { on: { kind: "boolean" } } },
58206
58615
  /** Clock fields: written, but excluded from the compare that decides
58207
58616
  * whether persisting is worth a SQLite commit. */
58208
- volatileStateFields: ["lastChangedAt"],
58209
- customize: {
58210
- version: 1,
58211
- advanced: "derive",
58212
- targets: []
58213
- }
58617
+ volatileStateFields: ["lastChangedAt"]
58214
58618
  };
58215
58619
  /**
58216
58620
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -58226,7 +58630,7 @@ var binaryCapability = {
58226
58630
  */
58227
58631
  var BrightnessStatusSchema = object({
58228
58632
  /** Current level as 0..100 inclusive. Firmware-reported. */
58229
- percentage: number().min(0).max(100),
58633
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
58230
58634
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
58231
58635
  lastChangedAt: number()
58232
58636
  });
@@ -58680,21 +59084,21 @@ var ClimateControlStatusSchema = object({
58680
59084
  /** Single setpoint in Celsius. Used by single-target modes
58681
59085
  * (heat / cool / dry). Null when the device is in a range mode
58682
59086
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58683
- target: number().nullable(),
59087
+ target: number().nullable().meta(readingMeta("temperature")),
58684
59088
  /** Upper bound of the dual setpoint in Celsius. Populated only
58685
59089
  * in `heat_cool` mode. */
58686
- targetHigh: number().nullable(),
59090
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58687
59091
  /** Lower bound of the dual setpoint in Celsius. Populated only
58688
59092
  * in `heat_cool` mode. */
58689
- targetLow: number().nullable(),
59093
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58690
59094
  /** Target relative humidity (0..100). Null when the device has no
58691
59095
  * humidity control. */
58692
- targetHumidity: number().min(0).max(100).nullable(),
59096
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58693
59097
  /** Current measured temperature in Celsius. Read-only — pushed by
58694
59098
  * the device's internal sensor. */
58695
- currentTemp: number().nullable(),
59099
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58696
59100
  /** Current measured relative humidity (0..100). Read-only. */
58697
- currentHumidity: number().min(0).max(100).nullable(),
59101
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58698
59102
  /** Vertical louver swing on/off. Null when the device has no vertical
58699
59103
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58700
59104
  swingVertical: boolean().nullable(),
@@ -58702,11 +59106,11 @@ var ClimateControlStatusSchema = object({
58702
59106
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58703
59107
  swingHorizontal: boolean().nullable(),
58704
59108
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58705
- minTemp: number().nullable().optional(),
59109
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58706
59110
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58707
- maxTemp: number().nullable().optional(),
59111
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58708
59112
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58709
- tempStep: number().positive().nullable().optional(),
59113
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58710
59114
  /** Ms epoch when the slice was last updated (push or command). */
58711
59115
  lastFetchedAt: number()
58712
59116
  });
@@ -58848,17 +59252,17 @@ var climateControlCapability = {
58848
59252
  * modes to expose in the picker without a slice fetch.
58849
59253
  */
58850
59254
  var RgbTripletSchema = object({
58851
- r: number().int().min(0).max(255),
58852
- g: number().int().min(0).max(255),
58853
- b: number().int().min(0).max(255)
59255
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
59256
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
59257
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58854
59258
  });
58855
59259
  var HsvTripletSchema = object({
58856
59260
  /** Hue in degrees, 0..360. */
58857
- h: number().min(0).max(360),
59261
+ h: number().min(0).max(360).meta(readingMeta("none")),
58858
59262
  /** Saturation as 0..100 inclusive. */
58859
- s: number().min(0).max(100),
59263
+ s: number().min(0).max(100).meta(readingMeta("none")),
58860
59264
  /** Value/brightness as 0..100 inclusive. */
58861
- v: number().min(0).max(100)
59265
+ v: number().min(0).max(100).meta(readingMeta("none"))
58862
59266
  });
58863
59267
  var ColorInputSchema = discriminatedUnion("mode", [
58864
59268
  object({
@@ -58887,7 +59291,7 @@ var ColorStatusSchema = object({
58887
59291
  /** Populated when `mode === 'hsv'`. */
58888
59292
  hsv: HsvTripletSchema.optional(),
58889
59293
  /** Populated when `mode === 'mired'`. */
58890
- mireds: number().int().optional(),
59294
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58891
59295
  /** Ms epoch of the last operator-driven change. */
58892
59296
  lastChangedAt: number()
58893
59297
  });
@@ -59082,18 +59486,12 @@ _enum([
59082
59486
  "empty",
59083
59487
  "full"
59084
59488
  ]);
59085
- /** Units a remaining-life counter is reported in. Closed: the panel renders
59086
- * each. */
59087
- var ConsumableRemainingUnitSchema = _enum([
59088
- "days",
59089
- "hours",
59090
- "cycles"
59091
- ]);
59092
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
59093
- * days). `value` null = unknown (D393). */
59489
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
59490
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
59491
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
59094
59492
  var ConsumableRemainingSchema = object({
59095
- value: number().min(0).nullable(),
59096
- unit: ConsumableRemainingUnitSchema
59493
+ value: number().min(0).nullable().meta(readingMeta("device")),
59494
+ kind: ConsumableRemainingKindSchema
59097
59495
  });
59098
59496
  /** A single consumable item. Either a continuous `level` (remaining
59099
59497
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -59106,7 +59504,7 @@ var ConsumableItemSchema = object({
59106
59504
  /** Display name. */
59107
59505
  label: string().min(1),
59108
59506
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
59109
- level: number().min(0).max(100).nullable(),
59507
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59110
59508
  /** The provider's discrete verdict when known (binary mode). */
59111
59509
  status: ConsumableStatusSchema.nullable(),
59112
59510
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -59188,8 +59586,8 @@ var consumablesCustomize = {
59188
59586
  transforms: [{ kind: "identity" }]
59189
59587
  }],
59190
59588
  literals: [{
59191
- path: "remaining.unit",
59192
- values: [...ConsumableRemainingUnitSchema.options]
59589
+ path: "remaining.kind",
59590
+ values: [...ConsumableRemainingKindSchema.options]
59193
59591
  }]
59194
59592
  },
59195
59593
  {
@@ -59318,7 +59716,6 @@ reset: method(object({
59318
59716
  },
59319
59717
  "remaining.value": {
59320
59718
  kind: "gauge",
59321
- unitFrom: "remaining.unit",
59322
59719
  deadband: 1,
59323
59720
  displayName: "Remaining"
59324
59721
  }
@@ -59388,10 +59785,9 @@ var contactCapability = {
59388
59785
  * (empty array for the other kinds). It lives in the slice because
59389
59786
  * HA `select.*` entities may update their options at runtime.
59390
59787
  *
59391
- * The `unit`/`min`/`max`/`step` fields are meaningful for
59392
- * `kind === 'numeric'` and are absent (undefined) for all other
59393
- * kinds. They are populated live from HA attributes on each state
59394
- * push — consistent with `options` and the sensor-unit approach.
59788
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59789
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59790
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
59395
59791
  */
59396
59792
  var ControlStatusSchema = object({
59397
59793
  kind: _enum([
@@ -59403,39 +59799,17 @@ var ControlStatusSchema = object({
59403
59799
  /** Current value. Numeric kind → number; select/text/datetime → string.
59404
59800
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
59405
59801
  * or time-only — HA accepts all three forms). */
59406
- value: union([number(), string()]),
59802
+ value: union([number().meta(readingMeta("device")), string()]),
59407
59803
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
59408
59804
  options: array(string()),
59409
59805
  /** Ms epoch when the slice was last updated. */
59410
59806
  lastChangedAt: number(),
59411
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
59412
- * Populated live from HA `attributes.unit_of_measurement` on each push.
59413
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59414
- unit: string().optional(),
59415
- /** Minimum allowed value. Populated live from HA `attributes.min`.
59416
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59417
- min: number().optional(),
59418
- /** Maximum allowed value. Populated live from HA `attributes.max`.
59419
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59420
- max: number().optional(),
59421
- /** Step increment. Populated live from HA `attributes.step`.
59422
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
59423
- step: number().optional(),
59424
- /** Suggested decimal places for numeric display. Populated live from HA
59425
- * `attributes.suggested_display_precision`, or derived from `step` when the
59426
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
59427
- * other kinds. Falls back to auto-formatting when absent. */
59428
- precision: number().int().min(0).max(10).optional(),
59429
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
59430
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
59431
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
59432
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
59433
- * `has_date`/`has_time` attributes on each push. */
59434
- format: _enum([
59435
- "date",
59436
- "time",
59437
- "datetime"
59438
- ]).optional()
59807
+ /** Minimum allowed value, in the device quantity's base unit. */
59808
+ min: number().optional().meta(readingMeta("device")),
59809
+ /** Maximum allowed value, in the device quantity's base unit. */
59810
+ max: number().optional().meta(readingMeta("device")),
59811
+ /** Step increment, in the device quantity's base unit. */
59812
+ step: number().optional().meta(readingMeta("device"))
59439
59813
  });
59440
59814
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
59441
59815
  object({
@@ -59501,9 +59875,9 @@ var CoverStatusSchema = object({
59501
59875
  ]),
59502
59876
  /** 0 = fully closed, 100 = fully open. Null when the device has
59503
59877
  * no intermediate position surface. */
59504
- position: number().min(0).max(100).nullable(),
59878
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59505
59879
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
59506
- tiltPosition: number().min(0).max(100).nullable(),
59880
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59507
59881
  /** Ms epoch when the slice was last updated. */
59508
59882
  lastChangedAt: number()
59509
59883
  });
@@ -59606,9 +59980,9 @@ var NormalizedRangeSchema$1 = object({
59606
59980
  var DayNightStatusSchema = object({
59607
59981
  mode: DayNightModeSchema,
59608
59982
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
59609
- sensitivity: number().optional(),
59983
+ sensitivity: number().optional().meta(readingMeta("none")),
59610
59984
  /** Delay before the IR-cut filter flips, in seconds. */
59611
- switchDelaySec: number().optional(),
59985
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
59612
59986
  lastFetchedAt: number()
59613
59987
  });
59614
59988
  /**
@@ -59826,11 +60200,11 @@ var DishwasherStatusSchema = object({
59826
60200
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59827
60201
  availablePrograms: array(string()),
59828
60202
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59829
- remainingSeconds: number().int().nonnegative().nullable(),
60203
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59830
60204
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59831
- delayedStartSeconds: number().int().nonnegative().nullable(),
60205
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59832
60206
  /** Cycle completion 0..100, or null when not applicable. */
59833
- progressPercent: number().min(0).max(100).nullable(),
60207
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59834
60208
  /** True when the door is reported open; null when the door state is not known. */
59835
60209
  doorOpen: boolean().nullable(),
59836
60210
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59854,7 +60228,7 @@ var DishwasherStatusSchema = object({
59854
60228
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59855
60229
  machineCareDue: boolean().nullable(),
59856
60230
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59857
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
60231
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59858
60232
  /** Option states: null when the appliance does not expose the option or has not said. */
59859
60233
  halfLoad: boolean().nullable(),
59860
60234
  hygienePlus: boolean().nullable(),
@@ -59869,7 +60243,7 @@ var DishwasherStatusSchema = object({
59869
60243
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59870
60244
  activeEvents: array(string()),
59871
60245
  /** Numeric error code when the appliance reports one; null otherwise. */
59872
- errorCode: number().nullable(),
60246
+ errorCode: number().nullable().meta(readingMeta("none")),
59873
60247
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59874
60248
  errorLabel: string().nullable(),
59875
60249
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -60100,7 +60474,7 @@ var DoorbellStatusSchema = object({
60100
60474
  /** Ms epoch of the last press. null = never observed since this provider started. */
60101
60475
  lastPressedAt: number().nullable(),
60102
60476
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
60103
- pressCountSinceStart: number()
60477
+ pressCountSinceStart: number().meta(readingMeta("count"))
60104
60478
  });
60105
60479
  var DoorbellPressEventSchema = object({
60106
60480
  deviceId: number(),
@@ -60165,17 +60539,7 @@ onPressed: {
60165
60539
  history: "none",
60166
60540
  customize: doorbellCustomize
60167
60541
  };
60168
- /**
60169
- * Enum-state sensor — a string value picked from a finite option set.
60170
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
60171
- * states, weather conditions, contact-source channels, mode strings).
60172
- * The option set is stable across observations; it is persisted in the
60173
- * device config blob (via the control slice at adoption) and is NOT
60174
- * stored in `sourceInfo`.
60175
- */
60176
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
60177
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
60178
- var EnumSensorDateTimeFormatSchema = _enum([
60542
+ _enum([
60179
60543
  "date",
60180
60544
  "time",
60181
60545
  "datetime"
@@ -60183,12 +60547,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
60183
60547
  var EnumSensorStatusSchema = object({
60184
60548
  value: string(),
60185
60549
  /**
60186
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
60187
- * as a locale date/time/datetime; absent for plain enum sensors. HA
60188
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
60189
- * a time-only sensor → `'time'`.
60550
+ * Set when `value` is a timestamp rather than an enum state, so history
60551
+ * does not learn ISO strings as states. The display shape (date / time /
60552
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
60190
60553
  */
60191
- format: EnumSensorDateTimeFormatSchema.optional(),
60554
+ timestamp: boolean().optional(),
60192
60555
  /** Ms epoch when the slice was last updated. */
60193
60556
  lastFetchedAt: number()
60194
60557
  });
@@ -60215,11 +60578,11 @@ var enumSensorCapability = {
60215
60578
  * History (D850): the value as an `enum` series (an appliance's program or
60216
60579
  * operation state, an HVAC action). The option set is per device, so the
60217
60580
  * value list is LEARNED as values are first seen (bounded). A sensor with a
60218
- * `format` carries a timestamp, not a state, and is not recorded.
60581
+ * `timestamp` carries a clock, not a state, and is not recorded.
60219
60582
  */
60220
60583
  history: { fields: { value: {
60221
60584
  kind: "enum",
60222
- skipWhenSet: "format"
60585
+ skipWhenSet: "timestamp"
60223
60586
  } } },
60224
60587
  /** Clock fields: written, but excluded from the compare that decides
60225
60588
  * whether persisting is worth a SQLite commit. */
@@ -60239,13 +60602,13 @@ var EventFireSchema = object({
60239
60602
  deviceId: number(),
60240
60603
  eventType: string(),
60241
60604
  data: record(string(), unknown()).nullable(),
60242
- timestamp: number(),
60243
- seq: number()
60605
+ timestamp: number().meta(readingMeta("none")),
60606
+ seq: number().meta(readingMeta("count"))
60244
60607
  });
60245
60608
  var EventEmitterStatusSchema = object({
60246
60609
  eventTypes: array(string()),
60247
60610
  lastEvent: EventFireSchema.nullable(),
60248
- eventCountSinceStart: number()
60611
+ eventCountSinceStart: number().meta(readingMeta("count"))
60249
60612
  });
60250
60613
  var eventEmitterCapability = {
60251
60614
  name: "event-emitter",
@@ -60658,10 +61021,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60658
61021
  var FanControlStatusSchema = object({
60659
61022
  /** Active speed as 0..100 inclusive. Null when the device has no
60660
61023
  * speed surface (single-speed fan). */
60661
- percentage: number().min(0).max(100).nullable(),
61024
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60662
61025
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60663
61026
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60664
- percentageStep: number().positive().optional(),
61027
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60665
61028
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60666
61029
  * — empty string when no preset is active or supported. */
60667
61030
  preset: string(),
@@ -60782,7 +61145,7 @@ var FeatureProbeStatusSchema = object({
60782
61145
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60783
61146
  model: string().nullable(),
60784
61147
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60785
- channelCount: number().nullable(),
61148
+ channelCount: number().nullable().meta(readingMeta("count")),
60786
61149
  /**
60787
61150
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60788
61151
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60905,9 +61268,91 @@ var gasCapability = {
60905
61268
  * whether persisting is worth a SQLite commit. */
60906
61269
  volatileStateFields: ["lastChangedAt"]
60907
61270
  };
61271
+ var GroupCommandsSchema = object({
61272
+ /** One row per member that has been commanded. A later command replaces that member's row. */
61273
+ results: array(object({
61274
+ deviceId: number().int().nonnegative(),
61275
+ cap: string().min(1),
61276
+ method: string().min(1),
61277
+ ok: boolean(),
61278
+ error: string().optional(),
61279
+ /** Ms epoch of the attempt. */
61280
+ at: number()
61281
+ })) });
61282
+ var groupCommandsCapability = {
61283
+ name: "group-commands",
61284
+ scope: "device",
61285
+ deviceNative: true,
61286
+ mode: "singleton",
61287
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
61288
+ methods: {},
61289
+ status: {
61290
+ schema: GroupCommandsSchema,
61291
+ kind: "push"
61292
+ },
61293
+ runtimeState: GroupCommandsSchema,
61294
+ /**
61295
+ * Runtime-state durability: **session** — the last attempt. A restored
61296
+ * failure would outlive the command that caused it.
61297
+ *
61298
+ * See `RuntimeStateDurability`. Enforced by
61299
+ * `scripts/check-runtime-state-durability.ts`.
61300
+ */
61301
+ durability: "session",
61302
+ history: "none"
61303
+ };
61304
+ /**
61305
+ * How many members of a helper group answered (D944). Counts are integers,
61306
+ * not measurements: zero included is a real count, never a stand-in reading.
61307
+ * The reading itself stays on the output capability. History is none — the
61308
+ * series that later statistics read is the output cap's own history block.
61309
+ */
61310
+ var GroupStatusSchema = object({
61311
+ /** Members whose current value entered the aggregate. */
61312
+ included: number().int().nonnegative().meta(readingMeta("count", {
61313
+ precision: 0,
61314
+ label: "Reporting"
61315
+ })),
61316
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
61317
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
61318
+ precision: 0,
61319
+ label: "Excluded"
61320
+ })),
61321
+ /** `included` + `excluded`. */
61322
+ total: number().int().nonnegative().meta(readingMeta("count", {
61323
+ precision: 0,
61324
+ label: "Members"
61325
+ }))
61326
+ });
61327
+ var groupStatusCapability = {
61328
+ name: "group-status",
61329
+ scope: "device",
61330
+ deviceNative: true,
61331
+ mode: "singleton",
61332
+ deviceTypes: [
61333
+ DeviceType.Sensor,
61334
+ DeviceType.Switch,
61335
+ DeviceType.Light
61336
+ ],
61337
+ methods: {},
61338
+ status: {
61339
+ schema: GroupStatusSchema,
61340
+ kind: "push"
61341
+ },
61342
+ runtimeState: GroupStatusSchema,
61343
+ /**
61344
+ * Runtime-state durability: **session** — recomputed from members on the
61345
+ * next source event; a restored count would outlive the readings it named.
61346
+ *
61347
+ * See `RuntimeStateDurability`. Enforced by
61348
+ * `scripts/check-runtime-state-durability.ts`.
61349
+ */
61350
+ durability: "session",
61351
+ history: "none"
61352
+ };
60908
61353
  var HeatingValveStatusSchema = object({
60909
61354
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60910
- position: number().min(0).max(100).nullable(),
61355
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60911
61356
  /** Ms epoch when the slice was last updated (push or poll). */
60912
61357
  lastFetchedAt: number()
60913
61358
  });
@@ -60950,9 +61395,9 @@ var HumidifierStatusSchema = object({
60950
61395
  /** Whether the humidifier is currently on. */
60951
61396
  on: boolean(),
60952
61397
  /** Current measured relative humidity (0..100). Null when not reported. */
60953
- currentHumidity: number().min(0).max(100).nullable(),
61398
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60954
61399
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60955
- targetHumidity: number().min(0).max(100).nullable(),
61400
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60956
61401
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60957
61402
  * device has no mode surface. */
60958
61403
  mode: string().nullable(),
@@ -60962,9 +61407,9 @@ var HumidifierStatusSchema = object({
60962
61407
  * `idle` / `off`). Null when the device doesn't report it. */
60963
61408
  action: string().nullable(),
60964
61409
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60965
- minHumidity: number().nullable(),
61410
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60966
61411
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60967
- maxHumidity: number().nullable(),
61412
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60968
61413
  /** Ms epoch when the slice was last updated. */
60969
61414
  lastChangedAt: number()
60970
61415
  });
@@ -61034,22 +61479,13 @@ var humidifierCapability = {
61034
61479
  * `sourceInfo`.
61035
61480
  */
61036
61481
  var HumiditySensorStatusSchema = object({
61037
- /** Current relative humidity, 0..100. */
61038
- percent: number().min(0).max(100).meta({
61039
- unit: "%",
61482
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
61483
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
61484
+ precision: 0,
61040
61485
  label: "Humidity"
61041
- }),
61486
+ })),
61042
61487
  /** Ms epoch when the slice was last updated. */
61043
- lastFetchedAt: number(),
61044
- /** Live display unit from the upstream source (e.g. HA
61045
- * `attributes.unit_of_measurement`). The UI prefers this over the
61046
- * role's canonical unit. Absent → fall back to the canonical unit. */
61047
- unit: string().optional(),
61048
- /** Suggested decimal places for numeric display.
61049
- * Populated live from the upstream source when provided (e.g. HA
61050
- * `attributes.suggested_display_precision`). Falls back to
61051
- * auto-formatting when absent. */
61052
- precision: number().int().min(0).max(10).optional()
61488
+ lastFetchedAt: number()
61053
61489
  });
61054
61490
  var humiditySensorCapability = {
61055
61491
  name: "humidity-sensor",
@@ -61080,12 +61516,7 @@ var humiditySensorCapability = {
61080
61516
  } } },
61081
61517
  /** Clock fields: written, but excluded from the compare that decides
61082
61518
  * whether persisting is worth a SQLite commit. */
61083
- volatileStateFields: ["lastFetchedAt"],
61084
- customize: {
61085
- version: 1,
61086
- advanced: "derive",
61087
- targets: []
61088
- }
61519
+ volatileStateFields: ["lastFetchedAt"]
61089
61520
  };
61090
61521
  /**
61091
61522
  * Image display cap. Models a single still image exposed by an integration —
@@ -61106,7 +61537,7 @@ var ImageStatusSchema = object({
61106
61537
  * entity exposes no `entity_picture` (yet). */
61107
61538
  url: string().nullable(),
61108
61539
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
61109
- lastUpdated: number().nullable()
61540
+ lastUpdated: number().nullable().meta(readingMeta("none"))
61110
61541
  });
61111
61542
  var imageCapability = {
61112
61543
  name: "image",
@@ -61194,19 +61625,19 @@ var NormalizedRangeSchema = object({
61194
61625
  */
61195
61626
  var ImageSettingsStatusSchema = object({
61196
61627
  /** Normalized 0–100. */
61197
- brightness: number().optional(),
61628
+ brightness: number().optional().meta(readingMeta("none")),
61198
61629
  /** Normalized 0–100. */
61199
- contrast: number().optional(),
61630
+ contrast: number().optional().meta(readingMeta("none")),
61200
61631
  /** Normalized 0–100. */
61201
- saturation: number().optional(),
61632
+ saturation: number().optional().meta(readingMeta("none")),
61202
61633
  /** Normalized 0–100. */
61203
- sharpness: number().optional(),
61634
+ sharpness: number().optional().meta(readingMeta("none")),
61204
61635
  mirror: boolean().optional(),
61205
61636
  flip: boolean().optional(),
61206
61637
  rotate: ImageRotateSchema.optional(),
61207
61638
  whiteBalance: WhiteBalanceModeSchema.optional(),
61208
61639
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
61209
- warmth: number().optional(),
61640
+ warmth: number().optional().meta(readingMeta("none")),
61210
61641
  exposureMode: ExposureModeSchema.optional(),
61211
61642
  backlightMode: BacklightModeSchema.optional(),
61212
61643
  lastFetchedAt: number()
@@ -61485,10 +61916,10 @@ var IntercomAbilitySchema = object({
61485
61916
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
61486
61917
  codecs: array(string()),
61487
61918
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
61488
- sampleRate: number(),
61919
+ sampleRate: number().meta(readingMeta("none")),
61489
61920
  duplex: _enum(["half", "full"]),
61490
61921
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
61491
- maxBacklogMs: number()
61922
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
61492
61923
  });
61493
61924
  var IntercomStatusSchema = object({
61494
61925
  /** True while a talk session is open. */
@@ -61715,13 +62146,13 @@ var LawnMowerControlStatusSchema = object({
61715
62146
  activity: LawnMowerActivitySchema,
61716
62147
  /** 0..100 battery percentage. Null when the device has no battery
61717
62148
  * reading. */
61718
- batteryLevel: number().min(0).max(100).nullable(),
62149
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61719
62150
  /** 0..100 mowing-completion percentage of the current task, or null when no
61720
62151
  * task is active / progress is unavailable. */
61721
- progressPercent: number().min(0).max(100).nullable(),
62152
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61722
62153
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61723
62154
  * or null when unknown. */
61724
- currentCode: number().nullable(),
62155
+ currentCode: number().nullable().meta(readingMeta("none")),
61725
62156
  /** Human label for {@link currentCode}, or null when undecodable. */
61726
62157
  currentCodeLabel: string().nullable(),
61727
62158
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -62254,7 +62685,7 @@ var MediaPlayerStatusSchema = object({
62254
62685
  state: MediaPlayerStateSchema,
62255
62686
  /** Volume as 0..100 inclusive. Null when the device has no volume
62256
62687
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
62257
- volumeLevel: number().min(0).max(100).nullable(),
62688
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62258
62689
  /** Mute toggle distinct from volume=0. Null when no mute surface.
62259
62690
  * Pair with `DeviceFeature.MediaPlayerMute`. */
62260
62691
  isMuted: boolean().nullable(),
@@ -62267,10 +62698,10 @@ var MediaPlayerStatusSchema = object({
62267
62698
  currentMedia: MediaInfoSchema.nullable(),
62268
62699
  /** Current playback position in ms. Null when not seekable or
62269
62700
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
62270
- positionMs: number().int().nonnegative().nullable(),
62701
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62271
62702
  /** Total duration of the current media in ms. Null when unknown
62272
62703
  * (live stream) or nothing is playing. */
62273
- durationMs: number().int().nonnegative().nullable(),
62704
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
62274
62705
  /** Shuffle toggle. Null when no shuffle surface.
62275
62706
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
62276
62707
  shuffle: boolean().nullable(),
@@ -62579,7 +63010,7 @@ var motionCustomize = {
62579
63010
  eventName: "onMotionChanged"
62580
63011
  },
62581
63012
  deviceTypes: [DeviceType.Camera],
62582
- apply: "replace",
63013
+ apply: "add",
62583
63014
  applyAuthority: {
62584
63015
  kind: "camera-sensors",
62585
63016
  timeoutField: "motionCooldownMs"
@@ -62651,7 +63082,7 @@ var MotionStatusSchema = object({
62651
63082
  * it, every provider seeded it at boot, and every session left it
62652
63083
  * `detected: true` after its detach.
62653
63084
  */
62654
- autoClearAfterMs: number().nullable()
63085
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62655
63086
  });
62656
63087
  /**
62657
63088
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62688,12 +63119,18 @@ var motionCapability = {
62688
63119
  methods: {
62689
63120
  /**
62690
63121
  * Pull the current motion state synchronously. Convenience shortcut
62691
- * for consumers that don't need the full status object; equivalent
62692
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62693
- * `getStatus` once the auto-injected status surface lands in every
62694
- * consumer.
63122
+ * for consumers that don't need the full status object. A composed
63123
+ * group answers from its slice (D944): unknown throws, it is not off.
62695
63124
  */
62696
63125
  isDetected: method(object({ deviceId: number() }), boolean()) },
63126
+ /**
63127
+ * A composed motion group serves `isDetected` from `detected` (D944).
63128
+ * Without this the cap stays uncomposable (`has-commands`).
63129
+ */
63130
+ composedMethods: { isDetected: {
63131
+ kind: "slice",
63132
+ field: "detected"
63133
+ } },
62697
63134
  events: {
62698
63135
  /**
62699
63136
  * Fires every time the runner transitions a camera between
@@ -62833,7 +63270,7 @@ var MotionZoneRegionSchema = object({
62833
63270
  * the grid region(s). */
62834
63271
  var MotionZoneStatusSchema = object({
62835
63272
  enabled: boolean(),
62836
- sensitivity: number(),
63273
+ sensitivity: number().meta(readingMeta("none")),
62837
63274
  /** Grid region(s). Today exactly one `grid` shape. */
62838
63275
  regions: array(MotionZoneRegionSchema),
62839
63276
  lastFetchedAt: number()
@@ -63203,7 +63640,7 @@ var NavigationStatusSchema = object({
63203
63640
  following: boolean(),
63204
63641
  flash: boolean(),
63205
63642
  lightMode: NavigationLightModeSchema,
63206
- lightLevel: number().min(40).max(100),
63643
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
63207
63644
  /** Ms epoch when the slice was last updated. */
63208
63645
  lastChangedAt: number()
63209
63646
  });
@@ -63337,13 +63774,13 @@ var NetworkLinkStatusSchema = object({
63337
63774
  * one whose reading has not landed must not be drawn at 0 %. Consumers
63338
63775
  * SKIP a null rather than coerce it.
63339
63776
  */
63340
- signalPercent: number().min(0).max(100).nullable(),
63777
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63341
63778
  /** Raw received signal strength in dBm, when the firmware reports one. */
63342
- rssiDbm: number().optional(),
63779
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
63343
63780
  /** Network name of a wireless link, when the firmware reports it. */
63344
63781
  ssid: string().optional(),
63345
63782
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
63346
- lastUpdated: number()
63783
+ lastUpdated: number().meta(readingMeta("none"))
63347
63784
  });
63348
63785
  /** The slice a provider seeds before its first read: nothing is known yet. */
63349
63786
  var NETWORK_LINK_UNKNOWN = {
@@ -63748,7 +64185,7 @@ var NotifierStatusSchema = object({
63748
64185
  lastError: string().nullable(),
63749
64186
  /** Number of deliveries currently buffered server-side (rate-limited
63750
64187
  * notifiers may queue). 0 when the provider sends synchronously. */
63751
- queueDepth: number().int().nonnegative(),
64188
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63752
64189
  /** Per-feature capability matrix — authoritative source for the
63753
64190
  * compose-form field gating. */
63754
64191
  supports: NotifierSupportsSchema
@@ -63840,11 +64277,9 @@ onSent: { data: object({
63840
64277
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63841
64278
  * for: water flow, distance, weight, frequency, signal strength, …).
63842
64279
  *
63843
- * The `unit` and `precision` fields carry the live display metadata that
63844
- * the upstream source provides with each state push (HA
63845
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63846
- * The UI reads them directly from the slice — they are the single source
63847
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
64280
+ * The quantity is not on this slice. It is chosen when the device is
64281
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
64282
+ * that quantity's base unit. `null` is a refused sample (D393).
63848
64283
  *
63849
64284
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63850
64285
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63853,15 +64288,8 @@ onSent: { data: object({
63853
64288
  * numeric values land here.
63854
64289
  */
63855
64290
  var NumericSensorStatusSchema = object({
63856
- value: number(),
63857
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63858
- * Populated live from the upstream source on each state push.
63859
- * Absent for unitless sensors. */
63860
- unit: string().optional(),
63861
- /** Suggested decimal places for numeric display.
63862
- * Populated live from the upstream source when provided.
63863
- * Falls back to auto-formatting when absent. */
63864
- precision: number().int().min(0).max(10).optional(),
64291
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
64292
+ value: number().nullable().meta(readingMeta("device")),
63865
64293
  /** Ms epoch when the slice was last updated. */
63866
64294
  lastFetchedAt: number()
63867
64295
  });
@@ -63885,11 +64313,11 @@ var numericSensorCapability = {
63885
64313
  */
63886
64314
  durability: "restored",
63887
64315
  /**
63888
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
64316
+ * History (D850): recorded series. The unit is the device quantity's base,
64317
+ * resolved when the series is read — the cap has no single unit.
63889
64318
  */
63890
64319
  history: { fields: { value: {
63891
64320
  kind: "gauge",
63892
- unitFrom: "unit",
63893
64321
  minIntervalMs: 3e4
63894
64322
  } } },
63895
64323
  /** Clock fields: written, but excluded from the compare that decides
@@ -64227,23 +64655,26 @@ var PetFeederStatusSchema = object({
64227
64655
  /** Food currently in the bowl (grams). Null when the device has not
64228
64656
  * reported a reading yet. On dual-hopper models this is the combined
64229
64657
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
64230
- foodLevel: number().nullable(),
64658
+ foodLevel: number().nullable().meta(readingMeta("none")),
64231
64659
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
64232
64660
  * single-hopper models. */
64233
- food1: number().nullable(),
64661
+ food1: number().nullable().meta(readingMeta("none")),
64234
64662
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
64235
64663
  * single-hopper models. */
64236
- food2: number().nullable(),
64664
+ food2: number().nullable().meta(readingMeta("none")),
64237
64665
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
64238
64666
  * (`device_class: problem`, on = low). True when the bowl is empty /
64239
64667
  * below the feeder's low threshold. */
64240
64668
  lowFood: boolean(),
64241
64669
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
64242
64670
  * device has no battery reading. */
64243
- batteryPower: number().min(0).max(100).nullable(),
64244
- /** Days of desiccant life remaining. Null when the model has no
64245
- * desiccant sensor. */
64246
- desiccantLeftDays: number().nullable(),
64671
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64672
+ /**
64673
+ * Desiccant life remaining, stored in seconds (duration base, D944).
64674
+ * Providers convert from the days the feeder reports. Null when the
64675
+ * model has no desiccant sensor.
64676
+ */
64677
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
64247
64678
  /** True while a feed is in progress. */
64248
64679
  feeding: boolean(),
64249
64680
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -64254,7 +64685,7 @@ var PetFeederStatusSchema = object({
64254
64685
  * with `errorCode` for consumers that want the raw integer. */
64255
64686
  error: string().nullable(),
64256
64687
  /** Raw device error code (0 / null = no error). */
64257
- errorCode: number().nullable(),
64688
+ errorCode: number().nullable().meta(readingMeta("none")),
64258
64689
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
64259
64690
  isDualHopper: boolean(),
64260
64691
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -64264,7 +64695,7 @@ var PetFeederStatusSchema = object({
64264
64695
  /** Play a chime when dispensing. */
64265
64696
  feedSound: boolean(),
64266
64697
  /** Speaker / prompt volume level (device-scaled integer). */
64267
- volume: number(),
64698
+ volume: number().meta(readingMeta("none")),
64268
64699
  /** Ms epoch when the slice was last refreshed from the cloud. */
64269
64700
  lastFetchedAt: number()
64270
64701
  });
@@ -64383,7 +64814,7 @@ var petFeederCapability = {
64383
64814
  },
64384
64815
  desiccantLeftDays: {
64385
64816
  kind: "gauge",
64386
- unit: "days"
64817
+ unit: "s"
64387
64818
  }
64388
64819
  } }
64389
64820
  };
@@ -64784,36 +65215,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64784
65215
  */
64785
65216
  var PowerMeterStatusSchema = object({
64786
65217
  /** Instantaneous power draw in watts. */
64787
- watts: number().optional().meta({
64788
- unit: "W",
64789
- label: "Power"
64790
- }),
65218
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64791
65219
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64792
- kwhTotal: number().optional().meta({
64793
- unit: "kWh",
64794
- label: "Energy"
64795
- }),
65220
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64796
65221
  /** Voltage in volts. */
64797
- volts: number().optional(),
65222
+ volts: number().optional().meta(readingMeta("voltage")),
64798
65223
  /** Current in amperes. */
64799
- amps: number().optional(),
64800
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64801
- va: number().optional(),
65224
+ amps: number().optional().meta(readingMeta("current")),
65225
+ /** Apparent power in volt-amperes. */
65226
+ va: number().optional().meta(readingMeta("apparent-power")),
64802
65227
  /** Power factor (0..1) if reported. */
64803
- powerFactor: number().min(0).max(1).optional(),
65228
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64804
65229
  /** Ms epoch when the slice was last updated. */
64805
- lastFetchedAt: number(),
64806
- /** Live display unit of the single metric this slice carries (e.g. HA
64807
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64808
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64809
- * unit per slice is unambiguous. The UI prefers this over the role's
64810
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64811
- unit: string().optional(),
64812
- /** Suggested decimal places for numeric display.
64813
- * Populated live from the upstream source when provided (e.g. HA
64814
- * `attributes.suggested_display_precision`). Falls back to
64815
- * auto-formatting when absent. */
64816
- precision: number().int().min(0).max(10).optional()
65230
+ lastFetchedAt: number()
64817
65231
  });
64818
65232
  var powerMeterCapability = {
64819
65233
  name: "power-meter",
@@ -64877,12 +65291,7 @@ var powerMeterCapability = {
64877
65291
  } },
64878
65292
  /** Clock fields: written, but excluded from the compare that decides
64879
65293
  * whether persisting is worth a SQLite commit. */
64880
- volatileStateFields: ["lastFetchedAt"],
64881
- customize: {
64882
- version: 1,
64883
- advanced: "derive",
64884
- targets: []
64885
- }
65294
+ volatileStateFields: ["lastFetchedAt"]
64886
65295
  };
64887
65296
  /**
64888
65297
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64902,11 +65311,11 @@ var powerMeterCapability = {
64902
65311
  */
64903
65312
  var GpsLocationSchema = object({
64904
65313
  /** Latitude in decimal degrees, -90..90. */
64905
- latitude: number().min(-90).max(90),
65314
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64906
65315
  /** Longitude in decimal degrees, -180..180. */
64907
- longitude: number().min(-180).max(180),
65316
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64908
65317
  /** Reported accuracy in meters (lower = better). */
64909
- accuracyMeters: number().nonnegative()
65318
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64910
65319
  });
64911
65320
  var PresenceStatusSchema = object({
64912
65321
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64918,7 +65327,7 @@ var PresenceStatusSchema = object({
64918
65327
  gps: GpsLocationSchema.nullable(),
64919
65328
  /** Optional battery level of the tracking device (0..100). Null
64920
65329
  * when not reported. */
64921
- batteryPercent: number().min(0).max(100).nullable(),
65330
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64922
65331
  /** Ms epoch when the slice was last updated. */
64923
65332
  lastChangedAt: number()
64924
65333
  });
@@ -64968,19 +65377,10 @@ var presenceCapability = {
64968
65377
  * `sourceInfo`.
64969
65378
  */
64970
65379
  var PressureSensorStatusSchema = object({
64971
- /** Current pressure in hPa. */
64972
- hpa: number(),
65380
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
65381
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64973
65382
  /** Ms epoch when the slice was last updated. */
64974
- lastFetchedAt: number(),
64975
- /** Live display unit from the upstream source (e.g. HA
64976
- * `attributes.unit_of_measurement`). The UI prefers this over the
64977
- * role's canonical unit. Absent → fall back to the canonical unit. */
64978
- unit: string().optional(),
64979
- /** Suggested decimal places for numeric display.
64980
- * Populated live from the upstream source when provided (e.g. HA
64981
- * `attributes.suggested_display_precision`). Falls back to
64982
- * auto-formatting when absent. */
64983
- precision: number().int().min(0).max(10).optional()
65383
+ lastFetchedAt: number()
64984
65384
  });
64985
65385
  var pressureSensorCapability = {
64986
65386
  name: "pressure-sensor",
@@ -65506,8 +65906,8 @@ var ptzCapability = {
65506
65906
  };
65507
65907
  var PtzAutotrackSettingsSchema = object({
65508
65908
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
65509
- stopDelaySeconds: number().int().min(0).max(300),
65510
- disappearDelaySeconds: number().int().min(0).max(300)
65909
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65910
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
65511
65911
  });
65512
65912
  /**
65513
65913
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65661,7 +66061,7 @@ var RunningStateStatusSchema = object({
65661
66061
  /** null = unknown — never coerced to false (D393). */
65662
66062
  running: boolean().nullable(),
65663
66063
  /** Epoch ms the current boolean began; null while `running` is null. */
65664
- since: number().nullable(),
66064
+ since: number().nullable().meta(readingMeta("none")),
65665
66065
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65666
66066
  phase: string().nullable(),
65667
66067
  /** null until a reading has been published (D393). */
@@ -65676,7 +66076,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65676
66076
  /** Generic Customize target for the running boolean (D936). */
65677
66077
  var runningCustomize = {
65678
66078
  version: 1,
65679
- advanced: "derive",
65680
66079
  targets: [{
65681
66080
  id: "running",
65682
66081
  cardinality: "scalar",
@@ -65696,7 +66095,7 @@ var runningCustomize = {
65696
66095
  DeviceType.Thermostat,
65697
66096
  DeviceType.PetFeeder
65698
66097
  ],
65699
- apply: "add-or-replace",
66098
+ apply: "add",
65700
66099
  path: "running",
65701
66100
  sources: [
65702
66101
  {
@@ -66989,9 +67388,9 @@ var RecordTriggerSchema = _enum([
66989
67388
  */
66990
67389
  var RecordWindowSchema = object({
66991
67390
  trigger: RecordTriggerSchema,
66992
- day: number().int().min(0).max(6),
66993
- startMinute: number().int().min(0).max(1439),
66994
- endMinute: number().int().min(1).max(1440)
67391
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
67392
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
67393
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
66995
67394
  });
66996
67395
  /** Status of one physical volume, as the camera itself describes it. */
66997
67396
  var OnboardStorageVolumeSchema = object({
@@ -67067,10 +67466,10 @@ var RecordingOnboardStatusSchema = object({
67067
67466
  /** Master "record to the card at all" switch. */
67068
67467
  enabled: boolean().nullable(),
67069
67468
  overwriteWhenFull: boolean().nullable(),
67070
- preRecordSec: number().nullable(),
67071
- postRecordSec: number().nullable(),
67469
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
67470
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
67072
67471
  /** Length of one recorded file, in minutes. */
67073
- segmentMinutes: number().nullable(),
67472
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
67074
67473
  /** The primary track's weekly windows, flattened. */
67075
67474
  windows: array(RecordWindowSchema),
67076
67475
  /**
@@ -67086,7 +67485,7 @@ var RecordingOnboardStatusSchema = object({
67086
67485
  * provider reporting a non-zero count also reports the schedule as not
67087
67486
  * writable.
67088
67487
  */
67089
- unreadableWindows: number(),
67488
+ unreadableWindows: number().meta(readingMeta("count")),
67090
67489
  /**
67091
67490
  * The camera is scheduled to record and has NO usable storage.
67092
67491
  *
@@ -68096,7 +68495,7 @@ var CamStreamDescriptorSchema = object({
68096
68495
  url: string().optional(),
68097
68496
  codec: string().optional(),
68098
68497
  resolution: CamStreamResolutionSchema.optional(),
68099
- fps: number().positive().optional(),
68498
+ fps: number().positive().optional().meta(readingMeta("rate")),
68100
68499
  label: string().optional(),
68101
68500
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
68102
68501
  deviceFeatures: array(string()).optional(),
@@ -68183,18 +68582,18 @@ var StreamProfileSchema = _enum([
68183
68582
  ]);
68184
68583
  /** The current encoding config of one stream profile. */
68185
68584
  var StreamProfileConfigSchema = object({
68186
- width: number(),
68187
- height: number(),
68585
+ width: number().meta(readingMeta("none")),
68586
+ height: number().meta(readingMeta("none")),
68188
68587
  codec: _enum(["h264", "h265"]),
68189
- framerate: number(),
68190
- bitrate: number(),
68588
+ framerate: number().meta(readingMeta("rate")),
68589
+ bitrate: number().meta(readingMeta("bitrate")),
68191
68590
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
68192
68591
  encoderProfile: _enum([
68193
68592
  "high",
68194
68593
  "main",
68195
68594
  "baseline"
68196
68595
  ]).optional(),
68197
- gop: number().optional(),
68596
+ gop: number().optional().meta(readingMeta("none")),
68198
68597
  /**
68199
68598
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
68200
68599
  *
@@ -68613,12 +69012,7 @@ var switchCapability = {
68613
69012
  label: "On",
68614
69013
  kind: "boolean"
68615
69014
  }
68616
- }] },
68617
- customize: {
68618
- version: 1,
68619
- advanced: "derive",
68620
- targets: []
68621
- }
69015
+ }] }
68622
69016
  };
68623
69017
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
68624
69018
  function hasControlCharacter(value) {
@@ -69201,35 +69595,21 @@ var tamperCapability = {
69201
69595
  * entries with `device_class: temperature` and any future native
69202
69596
  * thermometer.
69203
69597
  *
69204
- * Unit normalisation: providers convert to Celsius before storing.
69205
- * The slice value is always Celsius so cross-cap aggregators
69206
- * (climate-control's `currentTemp`, energy analytics) can compose
69207
- * without per-source unit fixups. The canonical display unit (`°C`) is
69208
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
69209
- * `sourceInfo`.
69598
+ * The number is Celsius (D944). Providers convert on ingest. A failed
69599
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
69210
69600
  *
69211
69601
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
69212
69602
  * frozen feed (provider hung) distinct from a "temperature hasn't
69213
69603
  * changed" steady state.
69214
69604
  */
69215
69605
  var TemperatureSensorStatusSchema = object({
69216
- /** Current temperature in Celsius. */
69217
- celsius: number().meta({
69218
- unit: "°C",
69606
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
69607
+ celsius: number().nullable().meta(readingMeta("temperature", {
69608
+ precision: 1,
69219
69609
  label: "Temperature"
69220
- }),
69610
+ })),
69221
69611
  /** Ms epoch when the slice was last updated (push or poll). */
69222
- lastFetchedAt: number(),
69223
- /** Live display unit from the upstream source (e.g. HA
69224
- * `attributes.unit_of_measurement`). The UI prefers this over the
69225
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
69226
- * Absent → the UI falls back to the role's canonical unit. */
69227
- unit: string().optional(),
69228
- /** Suggested decimal places for numeric display.
69229
- * Populated live from the upstream source when provided (e.g. HA
69230
- * `attributes.suggested_display_precision`). Falls back to
69231
- * auto-formatting when absent. */
69232
- precision: number().int().min(0).max(10).optional()
69612
+ lastFetchedAt: number()
69233
69613
  });
69234
69614
  var temperatureSensorCapability = {
69235
69615
  name: "temperature-sensor",
@@ -69260,12 +69640,7 @@ var temperatureSensorCapability = {
69260
69640
  } } },
69261
69641
  /** Clock fields: written, but excluded from the compare that decides
69262
69642
  * whether persisting is worth a SQLite commit. */
69263
- volatileStateFields: ["lastFetchedAt"],
69264
- customize: {
69265
- version: 1,
69266
- advanced: "derive",
69267
- targets: []
69268
- }
69643
+ volatileStateFields: ["lastFetchedAt"]
69269
69644
  };
69270
69645
  /**
69271
69646
  * toast — system-scoped singleton capability that streams toast
@@ -69548,16 +69923,16 @@ var VacuumControlStatusSchema = object({
69548
69923
  ]),
69549
69924
  /** 0..100 battery percentage. Null when the device has no battery
69550
69925
  * reading. */
69551
- batteryLevel: number().min(0).max(100).nullable(),
69926
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69552
69927
  /** Current fan-speed token (provider-verbatim). Null when unknown or
69553
69928
  * the vacuum has no speed control. */
69554
69929
  fanSpeed: string().nullable(),
69555
69930
  /** Speed tokens the hardware accepts — drives the UI selector. */
69556
69931
  availableFanSpeeds: array(string()),
69557
69932
  /** 0..100 cleaning-completion percentage of the current task, or null. */
69558
- progressPercent: number().min(0).max(100).nullable(),
69933
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69559
69934
  /** Current error code (0 / null = no error). */
69560
- errorCode: number().nullable(),
69935
+ errorCode: number().nullable().meta(readingMeta("none")),
69561
69936
  /** Human label for {@link errorCode}, or null when none / undecodable. */
69562
69937
  errorLabel: string().nullable(),
69563
69938
  /** Ms epoch when the slice was last updated. */
@@ -69635,7 +70010,7 @@ var ValveStatusSchema = object({
69635
70010
  ]),
69636
70011
  /** 0 = fully closed, 100 = fully open. Null when the device has no
69637
70012
  * intermediate position surface. */
69638
- position: number().min(0).max(100).nullable(),
70013
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
69639
70014
  /** Ms epoch when the slice was last updated. */
69640
70015
  lastChangedAt: number()
69641
70016
  });
@@ -69743,9 +70118,9 @@ var vibrationCapability = {
69743
70118
  */
69744
70119
  var WaterHeaterStatusSchema = object({
69745
70120
  /** Current measured temperature. Null when not reported. */
69746
- currentTemp: number().nullable(),
70121
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
69747
70122
  /** Target temperature setpoint. Null when no setpoint surface. */
69748
- targetTemp: number().nullable(),
70123
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
69749
70124
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
69750
70125
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
69751
70126
  * device reports an unknown state. */
@@ -69756,9 +70131,9 @@ var WaterHeaterStatusSchema = object({
69756
70131
  * has no away surface. */
69757
70132
  away: boolean().nullable(),
69758
70133
  /** HA `min_temp` attribute. Null when not reported. */
69759
- minTemp: number().nullable(),
70134
+ minTemp: number().nullable().meta(readingMeta("temperature")),
69760
70135
  /** HA `max_temp` attribute. Null when not reported. */
69761
- maxTemp: number().nullable(),
70136
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
69762
70137
  /** Ms epoch when the slice was last updated. */
69763
70138
  lastChangedAt: number()
69764
70139
  });
@@ -69843,25 +70218,18 @@ var waterHeaterCapability = {
69843
70218
  * Forecast deferred — current conditions only for v1.
69844
70219
  */
69845
70220
  var WeatherStatusSchema = object({
69846
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69847
- * when no condition has been reported yet. */
70221
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69848
70222
  condition: string().nullable(),
69849
- /** Current temperature in the reported unit. Null when not provided. */
69850
- temperature: number().nullable(),
69851
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69852
- temperatureUnit: string().nullable(),
70223
+ /** Current temperature in Celsius. Null when not provided or refused. */
70224
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69853
70225
  /** Relative humidity (0..100). Null when not provided. */
69854
- humidity: number().min(0).max(100).nullable(),
69855
- /** Barometric pressure in the reported unit. Null when not provided. */
69856
- pressure: number().nullable(),
69857
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69858
- pressureUnit: string().nullable(),
69859
- /** Wind speed in the reported unit. Null when not provided. */
69860
- windSpeed: number().nullable(),
69861
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69862
- windSpeedUnit: string().nullable(),
70226
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
70227
+ /** Barometric pressure in hPa. Null when not provided or refused. */
70228
+ pressure: number().nullable().meta(readingMeta("pressure")),
70229
+ /** Wind speed in metres per second. Null when not provided or refused. */
70230
+ windSpeed: number().nullable().meta(readingMeta("speed")),
69863
70231
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69864
- windBearing: number().nullable(),
70232
+ windBearing: number().nullable().meta(readingMeta("angle")),
69865
70233
  /** Ms epoch when the slice was last updated. */
69866
70234
  lastFetchedAt: number()
69867
70235
  });
@@ -69889,12 +70257,12 @@ var weatherCapability = {
69889
70257
  */
69890
70258
  durability: "session",
69891
70259
  /**
69892
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
70260
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69893
70261
  */
69894
70262
  history: { fields: {
69895
70263
  temperature: {
69896
70264
  kind: "gauge",
69897
- unitFrom: "temperatureUnit",
70265
+ unit: "°C",
69898
70266
  deadband: .2
69899
70267
  },
69900
70268
  humidity: {
@@ -69904,12 +70272,12 @@ var weatherCapability = {
69904
70272
  },
69905
70273
  pressure: {
69906
70274
  kind: "gauge",
69907
- unitFrom: "pressureUnit",
70275
+ unit: "hPa",
69908
70276
  deadband: .5
69909
70277
  },
69910
70278
  windSpeed: {
69911
70279
  kind: "gauge",
69912
- unitFrom: "windSpeedUnit",
70280
+ unit: "m/s",
69913
70281
  deadband: .5
69914
70282
  }
69915
70283
  } }
@@ -71288,6 +71656,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
71288
71656
  featureProbe: featureProbeCapability,
71289
71657
  flood: floodCapability,
71290
71658
  gas: gasCapability,
71659
+ groupCommands: groupCommandsCapability,
71660
+ groupStatus: groupStatusCapability,
71291
71661
  heatingValve: heatingValveCapability,
71292
71662
  humidifier: humidifierCapability,
71293
71663
  humiditySensor: humiditySensorCapability,
@@ -72664,6 +73034,13 @@ Object.freeze({
72664
73034
  access: "view",
72665
73035
  role: "admin"
72666
73036
  },
73037
+ "addons.getPackageDocs": {
73038
+ capName: "addons",
73039
+ capScope: "system",
73040
+ addonId: null,
73041
+ access: "view",
73042
+ role: "admin"
73043
+ },
72667
73044
  "addons.getVersions": {
72668
73045
  capName: "addons",
72669
73046
  capScope: "system",
@@ -75772,6 +76149,20 @@ Object.freeze({
75772
76149
  access: "view",
75773
76150
  role: "viewer"
75774
76151
  },
76152
+ "groupCommands.getStatus": {
76153
+ capName: "group-commands",
76154
+ capScope: "device",
76155
+ addonId: null,
76156
+ access: "view",
76157
+ role: "viewer"
76158
+ },
76159
+ "groupStatus.getStatus": {
76160
+ capName: "group-status",
76161
+ capScope: "device",
76162
+ addonId: null,
76163
+ access: "view",
76164
+ role: "viewer"
76165
+ },
75775
76166
  "heatingValve.getStatus": {
75776
76167
  capName: "heating-valve",
75777
76168
  capScope: "device",
@@ -85594,6 +85985,37 @@ var SENSOR_CAP_VOCABULARY = {
85594
85985
  icon: "flame"
85595
85986
  }]
85596
85987
  },
85988
+ "group-commands": {
85989
+ scope: "device",
85990
+ noun: t("Group command", "Comando di gruppo")
85991
+ },
85992
+ "group-status": {
85993
+ scope: "device",
85994
+ noun: t("Group", "Gruppo"),
85995
+ state: [
85996
+ {
85997
+ kind: "numeric",
85998
+ field: "included",
85999
+ quantity: t("the members reporting", "i membri che rispondono"),
86000
+ label: t("Reporting", "Rispondono"),
86001
+ icon: "users"
86002
+ },
86003
+ {
86004
+ kind: "numeric",
86005
+ field: "excluded",
86006
+ quantity: t("the members left out", "i membri esclusi"),
86007
+ label: t("Excluded", "Esclusi"),
86008
+ icon: "users"
86009
+ },
86010
+ {
86011
+ kind: "numeric",
86012
+ field: "total",
86013
+ quantity: t("the member count", "il numero di membri"),
86014
+ label: t("Members", "Membri"),
86015
+ icon: "sigma"
86016
+ }
86017
+ ]
86018
+ },
85597
86019
  "humidity-sensor": {
85598
86020
  scope: "device",
85599
86021
  noun: t("Humidity", "Umidità"),
@@ -85627,7 +86049,6 @@ var SENSOR_CAP_VOCABULARY = {
85627
86049
  field: "value",
85628
86050
  quantity: t("the reading", "il valore"),
85629
86051
  label: t("Reading", "Valore"),
85630
- unitField: "unit",
85631
86052
  icon: "gauge"
85632
86053
  }]
85633
86054
  },
@@ -85763,7 +86184,7 @@ var SENSOR_CAP_VOCABULARY = {
85763
86184
  field: "temperature",
85764
86185
  quantity: t("the outdoor temperature", "la temperatura esterna"),
85765
86186
  label: t("Temperature", "Temperatura"),
85766
- unitField: "temperatureUnit",
86187
+ unit: "°C",
85767
86188
  icon: "thermometer"
85768
86189
  },
85769
86190
  {
@@ -85779,7 +86200,7 @@ var SENSOR_CAP_VOCABULARY = {
85779
86200
  field: "windSpeed",
85780
86201
  quantity: t("the wind speed", "la velocità del vento"),
85781
86202
  label: t("Wind", "Vento"),
85782
- unitField: "windSpeedUnit",
86203
+ unit: "m/s",
85783
86204
  icon: "wind"
85784
86205
  },
85785
86206
  {
@@ -85787,7 +86208,7 @@ var SENSOR_CAP_VOCABULARY = {
85787
86208
  field: "pressure",
85788
86209
  quantity: t("the air pressure", "la pressione atmosferica"),
85789
86210
  label: t("Pressure", "Pressione"),
85790
- unitField: "pressureUnit",
86211
+ unit: "hPa",
85791
86212
  icon: "gauge"
85792
86213
  }
85793
86214
  ]
@@ -87212,6 +87633,16 @@ Object.freeze({
87212
87633
  form: "single",
87213
87634
  optional: false
87214
87635
  }],
87636
+ "groupCommands.getStatus": [{
87637
+ name: "deviceId",
87638
+ form: "single",
87639
+ optional: false
87640
+ }],
87641
+ "groupStatus.getStatus": [{
87642
+ name: "deviceId",
87643
+ form: "single",
87644
+ optional: false
87645
+ }],
87215
87646
  "heatingValve.getStatus": [{
87216
87647
  name: "deviceId",
87217
87648
  form: "single",
@@ -89196,6 +89627,8 @@ new Map(Object.entries({
89196
89627
  "detection-blind": "device-events",
89197
89628
  "motion-noisy": "device-events",
89198
89629
  "motion-calm": "device-events",
89630
+ "package-llm-silent": "device-events",
89631
+ "package-llm-answered": "device-events",
89199
89632
  "recording-stalled": "device-events",
89200
89633
  "recording-healthy": "device-events",
89201
89634
  "recording-degraded": "device-events",
@@ -89279,6 +89712,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
89279
89712
  label: "Motion without a subject",
89280
89713
  kinds: ["motion-noisy", "motion-calm"]
89281
89714
  },
89715
+ {
89716
+ id: "package-llm",
89717
+ family: "device-events",
89718
+ label: "Package confirm",
89719
+ kinds: ["package-llm-silent", "package-llm-answered"]
89720
+ },
89282
89721
  {
89283
89722
  id: "recording",
89284
89723
  family: "device-events",
@@ -89460,6 +89899,8 @@ var DEVICE_EVENTS = [
89460
89899
  "detection-blind",
89461
89900
  "motion-noisy",
89462
89901
  "motion-calm",
89902
+ "package-llm-silent",
89903
+ "package-llm-answered",
89463
89904
  "recording-stalled",
89464
89905
  "recording-healthy",
89465
89906
  "recording-degraded",
@@ -89728,6 +90169,15 @@ new Map([
89728
90169
  snapshotLabel: "Cameras with constant motion now",
89729
90170
  snapshotSection: null
89730
90171
  },
90172
+ {
90173
+ id: "package-llm-confirm",
90174
+ family: "device-events",
90175
+ label: "Package confirm (not answering / answering again)",
90176
+ bad: ["package-llm-silent"],
90177
+ recovery: ["package-llm-answered"],
90178
+ snapshotLabel: "Cameras whose package confirm is not answering",
90179
+ snapshotSection: null
90180
+ },
89731
90181
  {
89732
90182
  id: "recording-health",
89733
90183
  family: "device-events",