@camstack/addon-provider-amcrest 0.2.160 → 0.2.161

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -35,7 +35,7 @@ let node_stream_promises = require("node:stream/promises");
35
35
  let node_child_process = require("node:child_process");
36
36
  require("node:zlib");
37
37
  let node_os = require("node:os");
38
- //#region ../types/dist/event-category-DHXxB8t9.mjs
38
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
39
39
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
40
40
  EventCategory["SystemBoot"] = "system.boot";
41
41
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -361,6 +361,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
361
361
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
362
362
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
363
363
  /**
364
+ * A camera's package LLM confirm has failed closed three times in a row
365
+ * (D931, 2026-10-26). One edge per camera until
366
+ * {@link PipelinePackageLlmAnswered}.
367
+ */
368
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
369
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
370
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
371
+ /**
364
372
  * Per-camera pipeline config was mutated by the orchestrator
365
373
  * (a settings change via `setAgentInferenceDevices` /
366
374
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5470,7 +5478,7 @@ var ZodIssueCode = {
5470
5478
  var ZodFirstPartyTypeKind;
5471
5479
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5472
5480
  //#endregion
5473
- //#region ../types/dist/sleep-BErAxhul.mjs
5481
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5474
5482
  /**
5475
5483
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5476
5484
  * window to float samples (D455).
@@ -8024,6 +8032,99 @@ var RecordKindDeclarationSchema = object({
8024
8032
  }).strict();
8025
8033
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8026
8034
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8035
+ var READING_QUANTITIES = [
8036
+ "temperature",
8037
+ "pressure",
8038
+ "speed",
8039
+ "precipitation",
8040
+ "precipitation-rate",
8041
+ "length",
8042
+ "area",
8043
+ "illuminance",
8044
+ "irradiance",
8045
+ "power",
8046
+ "energy",
8047
+ "voltage",
8048
+ "current",
8049
+ "apparent-power",
8050
+ "humidity",
8051
+ "percentage",
8052
+ "concentration-volume",
8053
+ "concentration-mass",
8054
+ "concentration-ppb",
8055
+ "volume",
8056
+ "duration",
8057
+ "duration-ms",
8058
+ "information",
8059
+ "sound-level",
8060
+ "signal-level",
8061
+ "colour-temperature",
8062
+ "angle",
8063
+ "bitrate",
8064
+ "rate",
8065
+ "mass",
8066
+ "index",
8067
+ "ratio",
8068
+ "count",
8069
+ "none",
8070
+ "device"
8071
+ ];
8072
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8073
+ new Set(READING_QUANTITIES);
8074
+ /**
8075
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8076
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8077
+ */
8078
+ var QUANTITY_BASE = {
8079
+ temperature: "°C",
8080
+ pressure: "hPa",
8081
+ speed: "m/s",
8082
+ precipitation: "mm",
8083
+ "precipitation-rate": "mm/h",
8084
+ length: "m",
8085
+ area: "m²",
8086
+ illuminance: "lx",
8087
+ irradiance: "W/m²",
8088
+ power: "W",
8089
+ energy: "kWh",
8090
+ voltage: "V",
8091
+ current: "A",
8092
+ "apparent-power": "VA",
8093
+ humidity: "%",
8094
+ percentage: "%",
8095
+ "concentration-volume": "ppm",
8096
+ "concentration-mass": "µg/m³",
8097
+ "concentration-ppb": "ppb",
8098
+ volume: "L",
8099
+ duration: "s",
8100
+ "duration-ms": "ms",
8101
+ information: "B",
8102
+ "sound-level": "dBFS",
8103
+ "signal-level": "dBm",
8104
+ "colour-temperature": "mired",
8105
+ angle: "°",
8106
+ bitrate: "kbps",
8107
+ rate: "fps",
8108
+ mass: "kg",
8109
+ index: "none",
8110
+ ratio: "none",
8111
+ count: "none",
8112
+ none: "none",
8113
+ device: "device"
8114
+ };
8115
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8116
+ function baseUnitOf(quantity) {
8117
+ const symbol = QUANTITY_BASE[quantity];
8118
+ if (symbol === "none" || symbol === "device") return void 0;
8119
+ return symbol;
8120
+ }
8121
+ function readingMeta(quantity, extras) {
8122
+ return {
8123
+ quantity,
8124
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8125
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8126
+ };
8127
+ }
8027
8128
  /** Shared Zod schemas used across streaming capabilities. */
8028
8129
  var CamProfileSchema = _enum([
8029
8130
  "high",
@@ -8051,8 +8152,8 @@ var CamStreamKindSchema = _enum([
8051
8152
  "derived"
8052
8153
  ]);
8053
8154
  var CamStreamResolutionSchema = object({
8054
- width: number().int().positive(),
8055
- height: number().int().positive()
8155
+ width: number().int().positive().meta(readingMeta("none")),
8156
+ height: number().int().positive().meta(readingMeta("none"))
8056
8157
  });
8057
8158
  var CameraStreamSchema = object({
8058
8159
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8570,6 +8671,8 @@ var CAP_NAMES = Object.freeze([
8570
8671
  "filesystem-browse",
8571
8672
  "flood",
8572
8673
  "gas",
8674
+ "group-commands",
8675
+ "group-status",
8573
8676
  "heating-valve",
8574
8677
  "humidifier",
8575
8678
  "humidity-sensor",
@@ -8699,14 +8802,6 @@ Object.freeze({
8699
8802
  {
8700
8803
  "path": "aqi",
8701
8804
  "valueType": "number"
8702
- },
8703
- {
8704
- "path": "unit",
8705
- "valueType": "string"
8706
- },
8707
- {
8708
- "path": "precision",
8709
- "valueType": "number"
8710
8805
  }
8711
8806
  ],
8712
8807
  "alarm-panel": [
@@ -8742,20 +8837,10 @@ Object.freeze({
8742
8837
  "valueType": "boolean"
8743
8838
  }
8744
8839
  ],
8745
- "ambient-light-sensor": [
8746
- {
8747
- "path": "lux",
8748
- "valueType": "number"
8749
- },
8750
- {
8751
- "path": "unit",
8752
- "valueType": "string"
8753
- },
8754
- {
8755
- "path": "precision",
8756
- "valueType": "number"
8757
- }
8758
- ],
8840
+ "ambient-light-sensor": [{
8841
+ "path": "lux",
8842
+ "valueType": "number"
8843
+ }],
8759
8844
  "audio-metrics": [
8760
8845
  {
8761
8846
  "path": "ts",
@@ -8835,6 +8920,10 @@ Object.freeze({
8835
8920
  "path": "lastUpdated",
8836
8921
  "valueType": "number"
8837
8922
  },
8923
+ {
8924
+ "path": "volts",
8925
+ "valueType": "number"
8926
+ },
8838
8927
  {
8839
8928
  "path": "lastContactAt",
8840
8929
  "valueType": "number"
@@ -9077,10 +9166,6 @@ Object.freeze({
9077
9166
  "datetime"
9078
9167
  ]
9079
9168
  },
9080
- {
9081
- "path": "unit",
9082
- "valueType": "string"
9083
- },
9084
9169
  {
9085
9170
  "path": "min",
9086
9171
  "valueType": "number"
@@ -9092,19 +9177,6 @@ Object.freeze({
9092
9177
  {
9093
9178
  "path": "step",
9094
9179
  "valueType": "number"
9095
- },
9096
- {
9097
- "path": "precision",
9098
- "valueType": "number"
9099
- },
9100
- {
9101
- "path": "format",
9102
- "valueType": "enum",
9103
- "enumValues": [
9104
- "date",
9105
- "time",
9106
- "datetime"
9107
- ]
9108
9180
  }
9109
9181
  ],
9110
9182
  "cover": [
@@ -9345,13 +9417,8 @@ Object.freeze({
9345
9417
  "path": "value",
9346
9418
  "valueType": "string"
9347
9419
  }, {
9348
- "path": "format",
9349
- "valueType": "enum",
9350
- "enumValues": [
9351
- "date",
9352
- "time",
9353
- "datetime"
9354
- ]
9420
+ "path": "timestamp",
9421
+ "valueType": "boolean"
9355
9422
  }],
9356
9423
  "event-emitter": [
9357
9424
  {
@@ -9424,6 +9491,20 @@ Object.freeze({
9424
9491
  "path": "detected",
9425
9492
  "valueType": "boolean"
9426
9493
  }],
9494
+ "group-status": [
9495
+ {
9496
+ "path": "included",
9497
+ "valueType": "number"
9498
+ },
9499
+ {
9500
+ "path": "excluded",
9501
+ "valueType": "number"
9502
+ },
9503
+ {
9504
+ "path": "total",
9505
+ "valueType": "number"
9506
+ }
9507
+ ],
9427
9508
  "heating-valve": [{
9428
9509
  "path": "position",
9429
9510
  "valueType": "number"
@@ -9458,20 +9539,10 @@ Object.freeze({
9458
9539
  "valueType": "number"
9459
9540
  }
9460
9541
  ],
9461
- "humidity-sensor": [
9462
- {
9463
- "path": "percent",
9464
- "valueType": "number"
9465
- },
9466
- {
9467
- "path": "unit",
9468
- "valueType": "string"
9469
- },
9470
- {
9471
- "path": "precision",
9472
- "valueType": "number"
9473
- }
9474
- ],
9542
+ "humidity-sensor": [{
9543
+ "path": "percent",
9544
+ "valueType": "number"
9545
+ }],
9475
9546
  "image": [{
9476
9547
  "path": "url",
9477
9548
  "valueType": "string"
@@ -9810,20 +9881,10 @@ Object.freeze({
9810
9881
  "valueType": "boolean"
9811
9882
  }
9812
9883
  ],
9813
- "numeric-sensor": [
9814
- {
9815
- "path": "value",
9816
- "valueType": "number"
9817
- },
9818
- {
9819
- "path": "unit",
9820
- "valueType": "string"
9821
- },
9822
- {
9823
- "path": "precision",
9824
- "valueType": "number"
9825
- }
9826
- ],
9884
+ "numeric-sensor": [{
9885
+ "path": "value",
9886
+ "valueType": "number"
9887
+ }],
9827
9888
  "pet-feeder": [
9828
9889
  {
9829
9890
  "path": "foodLevel",
@@ -9915,14 +9976,6 @@ Object.freeze({
9915
9976
  {
9916
9977
  "path": "powerFactor",
9917
9978
  "valueType": "number"
9918
- },
9919
- {
9920
- "path": "unit",
9921
- "valueType": "string"
9922
- },
9923
- {
9924
- "path": "precision",
9925
- "valueType": "number"
9926
9979
  }
9927
9980
  ],
9928
9981
  "presence": [
@@ -9951,20 +10004,10 @@ Object.freeze({
9951
10004
  "valueType": "number"
9952
10005
  }
9953
10006
  ],
9954
- "pressure-sensor": [
9955
- {
9956
- "path": "hpa",
9957
- "valueType": "number"
9958
- },
9959
- {
9960
- "path": "unit",
9961
- "valueType": "string"
9962
- },
9963
- {
9964
- "path": "precision",
9965
- "valueType": "number"
9966
- }
9967
- ],
10007
+ "pressure-sensor": [{
10008
+ "path": "hpa",
10009
+ "valueType": "number"
10010
+ }],
9968
10011
  "privacy-mask": [{
9969
10012
  "path": "enabled",
9970
10013
  "valueType": "boolean"
@@ -10275,20 +10318,10 @@ Object.freeze({
10275
10318
  "path": "tampered",
10276
10319
  "valueType": "boolean"
10277
10320
  }],
10278
- "temperature-sensor": [
10279
- {
10280
- "path": "celsius",
10281
- "valueType": "number"
10282
- },
10283
- {
10284
- "path": "unit",
10285
- "valueType": "string"
10286
- },
10287
- {
10288
- "path": "precision",
10289
- "valueType": "number"
10290
- }
10291
- ],
10321
+ "temperature-sensor": [{
10322
+ "path": "celsius",
10323
+ "valueType": "number"
10324
+ }],
10292
10325
  "update": [
10293
10326
  {
10294
10327
  "path": "currentVersion",
@@ -10399,10 +10432,6 @@ Object.freeze({
10399
10432
  "path": "temperature",
10400
10433
  "valueType": "number"
10401
10434
  },
10402
- {
10403
- "path": "temperatureUnit",
10404
- "valueType": "string"
10405
- },
10406
10435
  {
10407
10436
  "path": "humidity",
10408
10437
  "valueType": "number"
@@ -10411,18 +10440,10 @@ Object.freeze({
10411
10440
  "path": "pressure",
10412
10441
  "valueType": "number"
10413
10442
  },
10414
- {
10415
- "path": "pressureUnit",
10416
- "valueType": "string"
10417
- },
10418
10443
  {
10419
10444
  "path": "windSpeed",
10420
10445
  "valueType": "number"
10421
10446
  },
10422
- {
10423
- "path": "windSpeedUnit",
10424
- "valueType": "string"
10425
- },
10426
10447
  {
10427
10448
  "path": "windBearing",
10428
10449
  "valueType": "number"
@@ -10829,7 +10850,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
10829
10850
  return DeviceRole;
10830
10851
  }({});
10831
10852
  //#endregion
10832
- //#region ../types/dist/composition-kEl9mevu.mjs
10853
+ //#region ../types/dist/composition-OhpuespA.mjs
10833
10854
  /**
10834
10855
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10835
10856
  *
@@ -11584,15 +11605,74 @@ function validateExpressionSource(src) {
11584
11605
  }
11585
11606
  return null;
11586
11607
  }
11587
- /** A composed device's stableId is this prefix plus the block id. */
11588
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11589
- /** Longest snippet a `code` source may carry (slice 4). */
11590
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11608
+ /**
11609
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11610
+ * id. Split out so helper schemas can name it without a cycle through
11611
+ * `composition.ts` (D659).
11612
+ */
11591
11613
  var CompositionSourceRefSchema = object({
11592
11614
  /** The owning addon; `stableId` is unique only within it. */
11593
11615
  addonId: string().min(1),
11594
11616
  stableId: string().min(1)
11595
11617
  });
11618
+ var GROUP_TEMPLATE_IDS = [
11619
+ "temperature",
11620
+ "humidity",
11621
+ "pressure",
11622
+ "illuminance",
11623
+ "power",
11624
+ "energy",
11625
+ "battery",
11626
+ "numeric",
11627
+ "motion",
11628
+ "contact",
11629
+ "leak",
11630
+ "smoke",
11631
+ "occupancy",
11632
+ "binary",
11633
+ "switch",
11634
+ "light"
11635
+ ];
11636
+ var NUMERIC_AGGREGATES = [
11637
+ "mean",
11638
+ "median",
11639
+ "min",
11640
+ "max",
11641
+ "sum"
11642
+ ];
11643
+ var BINARY_AGGREGATES = ["any", "all"];
11644
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11645
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11646
+ var AGGREGATE_REDUCERS = [
11647
+ "mean",
11648
+ "median",
11649
+ "min",
11650
+ "max",
11651
+ "sum",
11652
+ "any",
11653
+ "all"
11654
+ ];
11655
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11656
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11657
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11658
+ /** One member. The field is stored so occupancy can name motion or binary. */
11659
+ var HelperMemberSchema = object({
11660
+ source: CompositionSourceRefSchema,
11661
+ cap: string().min(1),
11662
+ fieldPath: string().min(1)
11663
+ });
11664
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11665
+ kind: literal("group"),
11666
+ templateId: GroupTemplateIdSchema,
11667
+ aggregate: AggregateReducerSchema,
11668
+ members: array(HelperMemberSchema).min(1).max(64),
11669
+ /** Actuator groups only. Readings leave this unset. */
11670
+ active: ActiveCriterionSchema.optional()
11671
+ })]);
11672
+ /** A composed device's stableId is this prefix plus the block id. */
11673
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11674
+ /** Longest snippet a `code` source may carry (slice 4). */
11675
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11596
11676
  /** One field of one capability of one source device. */
11597
11677
  var CompositionFieldReadSchema = object({
11598
11678
  source: CompositionSourceRefSchema,
@@ -11636,7 +11716,33 @@ var CompositionCodeSourceSchema = object({
11636
11716
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11637
11717
  CompositionFromSourceSchema,
11638
11718
  CompositionExpressionSourceSchema,
11639
- CompositionCodeSourceSchema
11719
+ CompositionCodeSourceSchema,
11720
+ object({
11721
+ kind: literal("aggregate"),
11722
+ reducer: _enum([
11723
+ "mean",
11724
+ "median",
11725
+ "min",
11726
+ "max",
11727
+ "sum",
11728
+ "any",
11729
+ "all"
11730
+ ]),
11731
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11732
+ /** `value` is the reading; the others are the member counts. */
11733
+ publish: _enum([
11734
+ "value",
11735
+ "included",
11736
+ "excluded",
11737
+ "total"
11738
+ ]),
11739
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11740
+ empty: _enum([
11741
+ "null",
11742
+ "omit",
11743
+ "offline"
11744
+ ])
11745
+ })
11640
11746
  ]);
11641
11747
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11642
11748
  kind: literal("forward"),
@@ -11682,7 +11788,13 @@ var CompositionSchema = object({
11682
11788
  kind: literal("existing"),
11683
11789
  device: CompositionSourceRefSchema
11684
11790
  })]),
11685
- features: array(CompositionFeatureSchema).min(1).max(16)
11791
+ features: array(CompositionFeatureSchema).min(1).max(16),
11792
+ /**
11793
+ * Present when the block is a helper template. Absent on a free
11794
+ * composition (the room example, device 4603). The editor reads this,
11795
+ * never the compiled features.
11796
+ */
11797
+ helper: HelperSpecSchema.optional()
11686
11798
  }).superRefine((composition, ctx) => {
11687
11799
  const seen = /* @__PURE__ */ new Set();
11688
11800
  composition.features.forEach((feature, index) => {
@@ -17928,7 +18040,11 @@ var CompositionProblemSchema = object({
17928
18040
  "item-field-reserved",
17929
18041
  "target-missing",
17930
18042
  "target-unreadable",
17931
- "not-replaceable"
18043
+ "not-replaceable",
18044
+ "native-capability-replace",
18045
+ "mixed-quantity",
18046
+ "helper-mismatch",
18047
+ "member-constraint"
17932
18048
  ]),
17933
18049
  path: string(),
17934
18050
  message: string()
@@ -17949,7 +18065,8 @@ var CompositionUnavailableModeSchema = _enum([
17949
18065
  "null",
17950
18066
  "offline",
17951
18067
  "unknown",
17952
- "release"
18068
+ "release",
18069
+ "omit"
17953
18070
  ]);
17954
18071
  /**
17955
18072
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17986,9 +18103,11 @@ var CompositionFieldPlanSchema = object({
17986
18103
  reads: array(CompositionFieldReadSchema)
17987
18104
  });
17988
18105
  /**
17989
- * What a feature does to its target (D663): `new` builds a capability on a new
17990
- * device; on an existing device it `add`s a capability the device lacks, or
17991
- * `replace`s named fields of one it has natively.
18106
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18107
+ * new device; on an existing device it `add`s a capability the device lacks.
18108
+ * Replacing a capability the device already serves is refused
18109
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18110
+ * wire for a claim the hub already holds.
17992
18111
  */
17993
18112
  var CompositionFeatureModeSchema = _enum([
17994
18113
  "new",
@@ -18161,6 +18280,275 @@ Object.freeze({
18161
18280
  result: ["number", "null"]
18162
18281
  }
18163
18282
  });
18283
+ /**
18284
+ * One catalog row per base reading a group helper can build (D944 §3).
18285
+ * Data, not a form default written into the block.
18286
+ */
18287
+ var NOW = {
18288
+ fieldPath: "lastFetchedAt",
18289
+ expr: "now"
18290
+ };
18291
+ var CHANGED = {
18292
+ fieldPath: "lastChangedAt",
18293
+ expr: "now"
18294
+ };
18295
+ function numeric(input) {
18296
+ return {
18297
+ ...input,
18298
+ family: "numeric",
18299
+ deviceType: DeviceType.Sensor,
18300
+ memberDeviceType: DeviceType.Sensor,
18301
+ commandCaps: [],
18302
+ baseUnit: baseUnitOf(input.quantity),
18303
+ aggregates: NUMERIC_AGGREGATES,
18304
+ accept: [{
18305
+ cap: input.outputCap,
18306
+ fieldPath: input.outputField
18307
+ }]
18308
+ };
18309
+ }
18310
+ function binary(input) {
18311
+ return {
18312
+ ...input,
18313
+ family: "binary",
18314
+ deviceType: DeviceType.Sensor,
18315
+ memberDeviceType: DeviceType.Sensor,
18316
+ commandCaps: [],
18317
+ quantity: "none",
18318
+ baseUnit: void 0,
18319
+ aggregates: BINARY_AGGREGATES,
18320
+ empty: "offline"
18321
+ };
18322
+ }
18323
+ var GROUP_TEMPLATES = [
18324
+ numeric({
18325
+ id: "temperature",
18326
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18327
+ role: DeviceRole.TemperatureSensor,
18328
+ outputCap: "temperature-sensor",
18329
+ outputField: "celsius",
18330
+ quantity: "temperature",
18331
+ defaultAggregate: "mean",
18332
+ empty: "null",
18333
+ companions: [NOW]
18334
+ }),
18335
+ numeric({
18336
+ id: "humidity",
18337
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18338
+ role: DeviceRole.HumiditySensor,
18339
+ outputCap: "humidity-sensor",
18340
+ outputField: "percent",
18341
+ quantity: "humidity",
18342
+ defaultAggregate: "mean",
18343
+ empty: "null",
18344
+ companions: [NOW]
18345
+ }),
18346
+ numeric({
18347
+ id: "pressure",
18348
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18349
+ role: DeviceRole.PressureSensor,
18350
+ outputCap: "pressure-sensor",
18351
+ outputField: "hpa",
18352
+ quantity: "pressure",
18353
+ defaultAggregate: "mean",
18354
+ empty: "null",
18355
+ companions: [NOW]
18356
+ }),
18357
+ numeric({
18358
+ id: "illuminance",
18359
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18360
+ role: DeviceRole.AmbientLightSensor,
18361
+ outputCap: "ambient-light-sensor",
18362
+ outputField: "lux",
18363
+ quantity: "illuminance",
18364
+ defaultAggregate: "mean",
18365
+ empty: "null",
18366
+ companions: [NOW]
18367
+ }),
18368
+ numeric({
18369
+ id: "power",
18370
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18371
+ role: DeviceRole.PowerSensor,
18372
+ outputCap: "power-meter",
18373
+ outputField: "watts",
18374
+ quantity: "power",
18375
+ defaultAggregate: "sum",
18376
+ empty: "omit",
18377
+ companions: [NOW]
18378
+ }),
18379
+ numeric({
18380
+ id: "energy",
18381
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18382
+ role: DeviceRole.EnergySensor,
18383
+ outputCap: "power-meter",
18384
+ outputField: "kwhTotal",
18385
+ quantity: "energy",
18386
+ defaultAggregate: "sum",
18387
+ empty: "omit",
18388
+ companions: [NOW]
18389
+ }),
18390
+ numeric({
18391
+ id: "battery",
18392
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18393
+ role: DeviceRole.BatterySensor,
18394
+ outputCap: "battery",
18395
+ outputField: "percentage",
18396
+ quantity: "percentage",
18397
+ defaultAggregate: "mean",
18398
+ empty: "null",
18399
+ companions: [
18400
+ {
18401
+ fieldPath: "charging",
18402
+ expr: "\"unknown\""
18403
+ },
18404
+ {
18405
+ fieldPath: "sleeping",
18406
+ expr: "false"
18407
+ },
18408
+ {
18409
+ fieldPath: "lastUpdated",
18410
+ expr: "now"
18411
+ }
18412
+ ]
18413
+ }),
18414
+ numeric({
18415
+ id: "numeric",
18416
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18417
+ role: DeviceRole.NumericSensor,
18418
+ outputCap: "numeric-sensor",
18419
+ outputField: "value",
18420
+ quantity: "device",
18421
+ defaultAggregate: "mean",
18422
+ empty: "null",
18423
+ companions: [NOW]
18424
+ }),
18425
+ binary({
18426
+ id: "motion",
18427
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18428
+ role: DeviceRole.MotionSensor,
18429
+ outputCap: "motion",
18430
+ outputField: "detected",
18431
+ defaultAggregate: "any",
18432
+ accept: [{
18433
+ cap: "motion",
18434
+ fieldPath: "detected"
18435
+ }],
18436
+ companions: [{
18437
+ fieldPath: "lastDetectedAt",
18438
+ expr: "null"
18439
+ }, {
18440
+ fieldPath: "autoClearAfterMs",
18441
+ expr: "null"
18442
+ }]
18443
+ }),
18444
+ binary({
18445
+ id: "contact",
18446
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18447
+ role: DeviceRole.ContactSensor,
18448
+ outputCap: "contact",
18449
+ outputField: "entryOpen",
18450
+ defaultAggregate: "any",
18451
+ accept: [{
18452
+ cap: "contact",
18453
+ fieldPath: "entryOpen"
18454
+ }],
18455
+ companions: [CHANGED]
18456
+ }),
18457
+ binary({
18458
+ id: "leak",
18459
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18460
+ role: DeviceRole.LeakSensor,
18461
+ outputCap: "flood",
18462
+ outputField: "flooded",
18463
+ defaultAggregate: "any",
18464
+ accept: [{
18465
+ cap: "flood",
18466
+ fieldPath: "flooded"
18467
+ }],
18468
+ companions: [CHANGED]
18469
+ }),
18470
+ binary({
18471
+ id: "smoke",
18472
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18473
+ role: DeviceRole.SmokeSensor,
18474
+ outputCap: "smoke",
18475
+ outputField: "detected",
18476
+ defaultAggregate: "any",
18477
+ accept: [{
18478
+ cap: "smoke",
18479
+ fieldPath: "detected"
18480
+ }],
18481
+ companions: [CHANGED]
18482
+ }),
18483
+ binary({
18484
+ id: "occupancy",
18485
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18486
+ role: DeviceRole.BinarySensor,
18487
+ outputCap: "binary",
18488
+ outputField: "on",
18489
+ defaultAggregate: "any",
18490
+ accept: [{
18491
+ cap: "motion",
18492
+ fieldPath: "detected"
18493
+ }, {
18494
+ cap: "binary",
18495
+ fieldPath: "on"
18496
+ }],
18497
+ companions: [CHANGED]
18498
+ }),
18499
+ binary({
18500
+ id: "binary",
18501
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18502
+ role: DeviceRole.BinarySensor,
18503
+ outputCap: "binary",
18504
+ outputField: "on",
18505
+ defaultAggregate: "any",
18506
+ accept: [{
18507
+ cap: "binary",
18508
+ fieldPath: "on"
18509
+ }],
18510
+ companions: [CHANGED]
18511
+ }),
18512
+ actuator({
18513
+ id: "switch",
18514
+ 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.",
18515
+ deviceType: DeviceType.Switch,
18516
+ outputCap: "switch",
18517
+ commandCaps: ["switch"]
18518
+ }),
18519
+ actuator({
18520
+ id: "light",
18521
+ 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.",
18522
+ deviceType: DeviceType.Light,
18523
+ outputCap: "switch",
18524
+ commandCaps: [
18525
+ "switch",
18526
+ "brightness",
18527
+ "color"
18528
+ ]
18529
+ })
18530
+ ];
18531
+ function actuator(input) {
18532
+ return {
18533
+ ...input,
18534
+ family: "actuator",
18535
+ role: void 0,
18536
+ memberDeviceType: input.deviceType,
18537
+ outputField: "on",
18538
+ quantity: "none",
18539
+ baseUnit: void 0,
18540
+ aggregates: BINARY_AGGREGATES,
18541
+ defaultAggregate: "any",
18542
+ empty: "offline",
18543
+ accept: [{
18544
+ cap: "switch",
18545
+ fieldPath: "on"
18546
+ }],
18547
+ companions: [CHANGED]
18548
+ };
18549
+ }
18550
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18551
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18164
18552
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18165
18553
  /**
18166
18554
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19987,12 +20375,12 @@ function buildDeviceAdminUrl(parts) {
19987
20375
  * providers detect renames by matching `uniqueId` and patch `id`
19988
20376
  * in-place so dispatch keeps working.
19989
20377
  *
19990
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19991
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19992
- * state push and are the single source of truth. The `device_class` tag is
19993
- * an HA-specific input that is consumed at the discovery boundary
19994
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19995
- * stored after that point.
20378
+ * **Display unit and precision are NOT stored here.** They are a read-time
20379
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20380
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20381
+ * neither is repeated on each sample. The `device_class` tag is an
20382
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20383
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19996
20384
  *
19997
20385
  * The `raw` escape hatch carries provider-specific attributes that don't
19998
20386
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20006,6 +20394,14 @@ var SourceInfoSchema = object({
20006
20394
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20007
20395
  * Used to detect rename when `id` changes. */
20008
20396
  uniqueId: string().optional(),
20397
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20398
+ quantity: ReadingQuantitySchema.optional(),
20399
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20400
+ dateTimeShape: _enum([
20401
+ "date",
20402
+ "time",
20403
+ "datetime"
20404
+ ]).optional(),
20009
20405
  /** Provider-specific extras that don't fit the structured slots.
20010
20406
  * Whitelist what goes here per provider — do NOT dump entire HA
20011
20407
  * attribute blobs (would explode the meta JSON column). */
@@ -41110,7 +41506,8 @@ method(object({
41110
41506
  "enum"
41111
41507
  ]),
41112
41508
  enumValues: array(string()).optional(),
41113
- item: boolean().optional()
41509
+ item: boolean().optional(),
41510
+ quantity: ReadingQuantitySchema.optional()
41114
41511
  })).readonly(),
41115
41512
  itemArray: object({
41116
41513
  path: string(),
@@ -44954,8 +45351,8 @@ var MaskPolygonShapeSchema = object({
44954
45351
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44955
45352
  var MaskGridShapeSchema = object({
44956
45353
  kind: literal("grid"),
44957
- gridWidth: number(),
44958
- gridHeight: number(),
45354
+ gridWidth: number().meta(readingMeta("none")),
45355
+ gridHeight: number().meta(readingMeta("none")),
44959
45356
  cells: array(boolean())
44960
45357
  });
44961
45358
  discriminatedUnion("kind", [
@@ -45067,6 +45464,8 @@ var NcSystemEventKindSchema = _enum([
45067
45464
  "detection-blind",
45068
45465
  "motion-noisy",
45069
45466
  "motion-calm",
45467
+ "package-llm-silent",
45468
+ "package-llm-answered",
45070
45469
  "recording-stalled",
45071
45470
  "recording-healthy",
45072
45471
  "recording-degraded",
@@ -56671,6 +57070,28 @@ var PackageVersionInfoSchema = object({
56671
57070
  deprecated: string().optional(),
56672
57071
  distTags: array(string()).readonly()
56673
57072
  });
57073
+ /**
57074
+ * One published document. `ok` is markdown from the registry. `missing` means
57075
+ * the registry answered and the file was not there. `error` is a registry or
57076
+ * tarball failure — never an empty string standing in for either.
57077
+ */
57078
+ var PackageDocBodySchema = discriminatedUnion("status", [
57079
+ object({
57080
+ status: literal("ok"),
57081
+ markdown: string()
57082
+ }),
57083
+ object({ status: literal("missing") }),
57084
+ object({
57085
+ status: literal("error"),
57086
+ message: string()
57087
+ })
57088
+ ]);
57089
+ var PackageDocsSchema = object({
57090
+ name: string(),
57091
+ version: string(),
57092
+ readme: PackageDocBodySchema,
57093
+ changelog: PackageDocBodySchema
57094
+ });
56674
57095
  var SearchResultSchema = object({
56675
57096
  name: string(),
56676
57097
  version: string(),
@@ -56889,7 +57310,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56889
57310
  }), object({ success: literal(true) }), {
56890
57311
  kind: "mutation",
56891
57312
  auth: "admin"
56892
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57313
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57314
+ name: string().min(1),
57315
+ version: string().min(1).optional()
57316
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56893
57317
  kind: "mutation",
56894
57318
  auth: "admin"
56895
57319
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56948,27 +57372,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56948
57372
  */
56949
57373
  var AirQualitySensorStatusSchema = object({
56950
57374
  /** Carbon dioxide concentration in ppm. */
56951
- co2Ppm: number().min(0).optional(),
57375
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56952
57376
  /** Total volatile organic compounds in ppb. */
56953
- vocPpb: number().min(0).optional(),
57377
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56954
57378
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56955
- pm25: number().min(0).optional(),
57379
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56956
57380
  /** Particulate matter ≤ 10 μm in µg/m³. */
56957
- pm10: number().min(0).optional(),
57381
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56958
57382
  /** Composite AQI value (typically 0..500). */
56959
- aqi: number().optional(),
57383
+ aqi: number().optional().meta(readingMeta("index")),
56960
57384
  /** Ms epoch when the slice was last updated. */
56961
- lastFetchedAt: number(),
56962
- /** Live display unit of the single metric this slice carries (e.g. HA
56963
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56964
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56965
- * per slice is unambiguous. */
56966
- unit: string().optional(),
56967
- /** Suggested decimal places for numeric display.
56968
- * Populated live from the upstream source when provided (e.g. HA
56969
- * `attributes.suggested_display_precision`). Falls back to
56970
- * auto-formatting when absent. */
56971
- precision: number().int().min(0).max(10).optional()
57385
+ lastFetchedAt: number()
56972
57386
  });
56973
57387
  var airQualitySensorCapability = {
56974
57388
  name: "air-quality-sensor",
@@ -57005,12 +57419,12 @@ var airQualitySensorCapability = {
57005
57419
  },
57006
57420
  pm25: {
57007
57421
  kind: "gauge",
57008
- unit: "ug/m3",
57422
+ unit: "µg/m³",
57009
57423
  deadband: 1
57010
57424
  },
57011
57425
  pm10: {
57012
57426
  kind: "gauge",
57013
- unit: "ug/m3",
57427
+ unit: "µg/m³",
57014
57428
  deadband: 1
57015
57429
  },
57016
57430
  aqi: {
@@ -57028,22 +57442,13 @@ var airQualitySensorCapability = {
57028
57442
  * entries with `device_class: illuminance`.
57029
57443
  */
57030
57444
  var AmbientLightSensorStatusSchema = object({
57031
- /** Current illuminance in lux (lx). */
57032
- lux: number().min(0).meta({
57033
- unit: "lx",
57445
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57446
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57447
+ precision: 0,
57034
57448
  label: "Illuminance"
57035
- }),
57449
+ })),
57036
57450
  /** Ms epoch when the slice was last updated. */
57037
- lastFetchedAt: number(),
57038
- /** Live display unit from the upstream source (e.g. HA
57039
- * `attributes.unit_of_measurement`). The UI prefers this over the
57040
- * role's canonical unit. Absent → fall back to the canonical unit. */
57041
- unit: string().optional(),
57042
- /** Suggested decimal places for numeric display.
57043
- * Populated live from the upstream source when provided (e.g. HA
57044
- * `attributes.suggested_display_precision`). Falls back to
57045
- * auto-formatting when absent. */
57046
- precision: number().int().min(0).max(10).optional()
57451
+ lastFetchedAt: number()
57047
57452
  });
57048
57453
  var ambientLightSensorCapability = {
57049
57454
  name: "ambient-light-sensor",
@@ -57075,12 +57480,7 @@ var ambientLightSensorCapability = {
57075
57480
  } } },
57076
57481
  /** Clock fields: written, but excluded from the compare that decides
57077
57482
  * whether persisting is worth a SQLite commit. */
57078
- volatileStateFields: ["lastFetchedAt"],
57079
- customize: {
57080
- version: 1,
57081
- advanced: "derive",
57082
- targets: []
57083
- }
57483
+ volatileStateFields: ["lastFetchedAt"]
57084
57484
  };
57085
57485
  /**
57086
57486
  * Per-class audio metrics aggregated over a sliding window.
@@ -57307,10 +57707,7 @@ var BatteryStatusSchema = object({
57307
57707
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57308
57708
  * declines to band a non-finite reading, which is the correct shape.
57309
57709
  */
57310
- percentage: number().min(0).max(100).nullable().meta({
57311
- unit: "%",
57312
- label: "Charge"
57313
- }),
57710
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57314
57711
  /**
57315
57712
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57316
57713
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57337,7 +57734,14 @@ var BatteryStatusSchema = object({
57337
57734
  */
57338
57735
  sleeping: boolean().meta({ label: "Sleeping" }),
57339
57736
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57340
- lastUpdated: number(),
57737
+ lastUpdated: number().meta(readingMeta("none")),
57738
+ /**
57739
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57740
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57741
+ * that only report charge. A second reading on this cap, not a second
57742
+ * `numeric-sensor` on the same device.
57743
+ */
57744
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57341
57745
  /**
57342
57746
  * Ms epoch of the last time the device PROVED it was reachable — a
57343
57747
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57389,7 +57793,6 @@ var BATTERY_DEVICE_TYPES = [
57389
57793
  /** Generic Customize target for the percentage reading (D936). */
57390
57794
  var batteryCustomize = {
57391
57795
  version: 1,
57392
- advanced: "derive",
57393
57796
  targets: [{
57394
57797
  id: "percentage",
57395
57798
  cardinality: "scalar",
@@ -57398,7 +57801,7 @@ var batteryCustomize = {
57398
57801
  stateField: "percentage"
57399
57802
  },
57400
57803
  deviceTypes: BATTERY_DEVICE_TYPES,
57401
- apply: "add-or-replace",
57804
+ apply: "add",
57402
57805
  path: "percentage",
57403
57806
  sources: [{
57404
57807
  kind: "field",
@@ -57519,10 +57922,16 @@ onStatusChanged: { data: object({
57519
57922
  /**
57520
57923
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57521
57924
  */
57522
- history: { fields: { percentage: {
57523
- kind: "gauge",
57524
- unit: "%"
57525
- } } },
57925
+ history: { fields: {
57926
+ percentage: {
57927
+ kind: "gauge",
57928
+ unit: "%"
57929
+ },
57930
+ volts: {
57931
+ kind: "gauge",
57932
+ unit: "V"
57933
+ }
57934
+ } },
57526
57935
  customize: batteryCustomize,
57527
57936
  /** Clock fields: written, but excluded from the compare that decides
57528
57937
  * whether persisting is worth a SQLite commit. */
@@ -57569,12 +57978,7 @@ var binaryCapability = {
57569
57978
  history: { fields: { on: { kind: "boolean" } } },
57570
57979
  /** Clock fields: written, but excluded from the compare that decides
57571
57980
  * whether persisting is worth a SQLite commit. */
57572
- volatileStateFields: ["lastChangedAt"],
57573
- customize: {
57574
- version: 1,
57575
- advanced: "derive",
57576
- targets: []
57577
- }
57981
+ volatileStateFields: ["lastChangedAt"]
57578
57982
  };
57579
57983
  /**
57580
57984
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57590,7 +57994,7 @@ var binaryCapability = {
57590
57994
  */
57591
57995
  var BrightnessStatusSchema = object({
57592
57996
  /** Current level as 0..100 inclusive. Firmware-reported. */
57593
- percentage: number().min(0).max(100),
57997
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57594
57998
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57595
57999
  lastChangedAt: number()
57596
58000
  });
@@ -58044,21 +58448,21 @@ var ClimateControlStatusSchema = object({
58044
58448
  /** Single setpoint in Celsius. Used by single-target modes
58045
58449
  * (heat / cool / dry). Null when the device is in a range mode
58046
58450
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58047
- target: number().nullable(),
58451
+ target: number().nullable().meta(readingMeta("temperature")),
58048
58452
  /** Upper bound of the dual setpoint in Celsius. Populated only
58049
58453
  * in `heat_cool` mode. */
58050
- targetHigh: number().nullable(),
58454
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58051
58455
  /** Lower bound of the dual setpoint in Celsius. Populated only
58052
58456
  * in `heat_cool` mode. */
58053
- targetLow: number().nullable(),
58457
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58054
58458
  /** Target relative humidity (0..100). Null when the device has no
58055
58459
  * humidity control. */
58056
- targetHumidity: number().min(0).max(100).nullable(),
58460
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58057
58461
  /** Current measured temperature in Celsius. Read-only — pushed by
58058
58462
  * the device's internal sensor. */
58059
- currentTemp: number().nullable(),
58463
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58060
58464
  /** Current measured relative humidity (0..100). Read-only. */
58061
- currentHumidity: number().min(0).max(100).nullable(),
58465
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58062
58466
  /** Vertical louver swing on/off. Null when the device has no vertical
58063
58467
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58064
58468
  swingVertical: boolean().nullable(),
@@ -58066,11 +58470,11 @@ var ClimateControlStatusSchema = object({
58066
58470
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58067
58471
  swingHorizontal: boolean().nullable(),
58068
58472
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58069
- minTemp: number().nullable().optional(),
58473
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58070
58474
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58071
- maxTemp: number().nullable().optional(),
58475
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58072
58476
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58073
- tempStep: number().positive().nullable().optional(),
58477
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58074
58478
  /** Ms epoch when the slice was last updated (push or command). */
58075
58479
  lastFetchedAt: number()
58076
58480
  });
@@ -58212,17 +58616,17 @@ var climateControlCapability = {
58212
58616
  * modes to expose in the picker without a slice fetch.
58213
58617
  */
58214
58618
  var RgbTripletSchema = object({
58215
- r: number().int().min(0).max(255),
58216
- g: number().int().min(0).max(255),
58217
- b: number().int().min(0).max(255)
58619
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58620
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58621
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58218
58622
  });
58219
58623
  var HsvTripletSchema = object({
58220
58624
  /** Hue in degrees, 0..360. */
58221
- h: number().min(0).max(360),
58625
+ h: number().min(0).max(360).meta(readingMeta("none")),
58222
58626
  /** Saturation as 0..100 inclusive. */
58223
- s: number().min(0).max(100),
58627
+ s: number().min(0).max(100).meta(readingMeta("none")),
58224
58628
  /** Value/brightness as 0..100 inclusive. */
58225
- v: number().min(0).max(100)
58629
+ v: number().min(0).max(100).meta(readingMeta("none"))
58226
58630
  });
58227
58631
  var ColorInputSchema = discriminatedUnion("mode", [
58228
58632
  object({
@@ -58251,7 +58655,7 @@ var ColorStatusSchema = object({
58251
58655
  /** Populated when `mode === 'hsv'`. */
58252
58656
  hsv: HsvTripletSchema.optional(),
58253
58657
  /** Populated when `mode === 'mired'`. */
58254
- mireds: number().int().optional(),
58658
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58255
58659
  /** Ms epoch of the last operator-driven change. */
58256
58660
  lastChangedAt: number()
58257
58661
  });
@@ -58446,18 +58850,12 @@ _enum([
58446
58850
  "empty",
58447
58851
  "full"
58448
58852
  ]);
58449
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58450
- * each. */
58451
- var ConsumableRemainingUnitSchema = _enum([
58452
- "days",
58453
- "hours",
58454
- "cycles"
58455
- ]);
58456
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58457
- * days). `value` null = unknown (D393). */
58853
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58854
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58855
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58458
58856
  var ConsumableRemainingSchema = object({
58459
- value: number().min(0).nullable(),
58460
- unit: ConsumableRemainingUnitSchema
58857
+ value: number().min(0).nullable().meta(readingMeta("device")),
58858
+ kind: ConsumableRemainingKindSchema
58461
58859
  });
58462
58860
  /** A single consumable item. Either a continuous `level` (remaining
58463
58861
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58470,7 +58868,7 @@ var ConsumableItemSchema = object({
58470
58868
  /** Display name. */
58471
58869
  label: string().min(1),
58472
58870
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58473
- level: number().min(0).max(100).nullable(),
58871
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58474
58872
  /** The provider's discrete verdict when known (binary mode). */
58475
58873
  status: ConsumableStatusSchema.nullable(),
58476
58874
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58552,8 +58950,8 @@ var consumablesCustomize = {
58552
58950
  transforms: [{ kind: "identity" }]
58553
58951
  }],
58554
58952
  literals: [{
58555
- path: "remaining.unit",
58556
- values: [...ConsumableRemainingUnitSchema.options]
58953
+ path: "remaining.kind",
58954
+ values: [...ConsumableRemainingKindSchema.options]
58557
58955
  }]
58558
58956
  },
58559
58957
  {
@@ -58682,7 +59080,6 @@ reset: method(object({
58682
59080
  },
58683
59081
  "remaining.value": {
58684
59082
  kind: "gauge",
58685
- unitFrom: "remaining.unit",
58686
59083
  deadband: 1,
58687
59084
  displayName: "Remaining"
58688
59085
  }
@@ -58752,10 +59149,9 @@ var contactCapability = {
58752
59149
  * (empty array for the other kinds). It lives in the slice because
58753
59150
  * HA `select.*` entities may update their options at runtime.
58754
59151
  *
58755
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58756
- * `kind === 'numeric'` and are absent (undefined) for all other
58757
- * kinds. They are populated live from HA attributes on each state
58758
- * push — consistent with `options` and the sensor-unit approach.
59152
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59153
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59154
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58759
59155
  */
58760
59156
  var ControlStatusSchema = object({
58761
59157
  kind: _enum([
@@ -58767,39 +59163,17 @@ var ControlStatusSchema = object({
58767
59163
  /** Current value. Numeric kind → number; select/text/datetime → string.
58768
59164
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58769
59165
  * or time-only — HA accepts all three forms). */
58770
- value: union([number(), string()]),
59166
+ value: union([number().meta(readingMeta("device")), string()]),
58771
59167
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58772
59168
  options: array(string()),
58773
59169
  /** Ms epoch when the slice was last updated. */
58774
59170
  lastChangedAt: number(),
58775
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58776
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58777
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58778
- unit: string().optional(),
58779
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58780
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58781
- min: number().optional(),
58782
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58783
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58784
- max: number().optional(),
58785
- /** Step increment. Populated live from HA `attributes.step`.
58786
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58787
- step: number().optional(),
58788
- /** Suggested decimal places for numeric display. Populated live from HA
58789
- * `attributes.suggested_display_precision`, or derived from `step` when the
58790
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58791
- * other kinds. Falls back to auto-formatting when absent. */
58792
- precision: number().int().min(0).max(10).optional(),
58793
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58794
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58795
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58796
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58797
- * `has_date`/`has_time` attributes on each push. */
58798
- format: _enum([
58799
- "date",
58800
- "time",
58801
- "datetime"
58802
- ]).optional()
59171
+ /** Minimum allowed value, in the device quantity's base unit. */
59172
+ min: number().optional().meta(readingMeta("device")),
59173
+ /** Maximum allowed value, in the device quantity's base unit. */
59174
+ max: number().optional().meta(readingMeta("device")),
59175
+ /** Step increment, in the device quantity's base unit. */
59176
+ step: number().optional().meta(readingMeta("device"))
58803
59177
  });
58804
59178
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58805
59179
  object({
@@ -58865,9 +59239,9 @@ var CoverStatusSchema = object({
58865
59239
  ]),
58866
59240
  /** 0 = fully closed, 100 = fully open. Null when the device has
58867
59241
  * no intermediate position surface. */
58868
- position: number().min(0).max(100).nullable(),
59242
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58869
59243
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58870
- tiltPosition: number().min(0).max(100).nullable(),
59244
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58871
59245
  /** Ms epoch when the slice was last updated. */
58872
59246
  lastChangedAt: number()
58873
59247
  });
@@ -58970,9 +59344,9 @@ var NormalizedRangeSchema$1 = object({
58970
59344
  var DayNightStatusSchema = object({
58971
59345
  mode: DayNightModeSchema,
58972
59346
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58973
- sensitivity: number().optional(),
59347
+ sensitivity: number().optional().meta(readingMeta("none")),
58974
59348
  /** Delay before the IR-cut filter flips, in seconds. */
58975
- switchDelaySec: number().optional(),
59349
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58976
59350
  lastFetchedAt: number()
58977
59351
  });
58978
59352
  /**
@@ -59190,11 +59564,11 @@ var DishwasherStatusSchema = object({
59190
59564
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59191
59565
  availablePrograms: array(string()),
59192
59566
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59193
- remainingSeconds: number().int().nonnegative().nullable(),
59567
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59194
59568
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59195
- delayedStartSeconds: number().int().nonnegative().nullable(),
59569
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59196
59570
  /** Cycle completion 0..100, or null when not applicable. */
59197
- progressPercent: number().min(0).max(100).nullable(),
59571
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59198
59572
  /** True when the door is reported open; null when the door state is not known. */
59199
59573
  doorOpen: boolean().nullable(),
59200
59574
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59218,7 +59592,7 @@ var DishwasherStatusSchema = object({
59218
59592
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59219
59593
  machineCareDue: boolean().nullable(),
59220
59594
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59221
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59595
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59222
59596
  /** Option states: null when the appliance does not expose the option or has not said. */
59223
59597
  halfLoad: boolean().nullable(),
59224
59598
  hygienePlus: boolean().nullable(),
@@ -59233,7 +59607,7 @@ var DishwasherStatusSchema = object({
59233
59607
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59234
59608
  activeEvents: array(string()),
59235
59609
  /** Numeric error code when the appliance reports one; null otherwise. */
59236
- errorCode: number().nullable(),
59610
+ errorCode: number().nullable().meta(readingMeta("none")),
59237
59611
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59238
59612
  errorLabel: string().nullable(),
59239
59613
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59464,7 +59838,7 @@ var DoorbellStatusSchema = object({
59464
59838
  /** Ms epoch of the last press. null = never observed since this provider started. */
59465
59839
  lastPressedAt: number().nullable(),
59466
59840
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59467
- pressCountSinceStart: number()
59841
+ pressCountSinceStart: number().meta(readingMeta("count"))
59468
59842
  });
59469
59843
  var DoorbellPressEventSchema = object({
59470
59844
  deviceId: number(),
@@ -59529,17 +59903,7 @@ onPressed: {
59529
59903
  history: "none",
59530
59904
  customize: doorbellCustomize
59531
59905
  };
59532
- /**
59533
- * Enum-state sensor — a string value picked from a finite option set.
59534
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59535
- * states, weather conditions, contact-source channels, mode strings).
59536
- * The option set is stable across observations; it is persisted in the
59537
- * device config blob (via the control slice at adoption) and is NOT
59538
- * stored in `sourceInfo`.
59539
- */
59540
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59541
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59542
- var EnumSensorDateTimeFormatSchema = _enum([
59906
+ _enum([
59543
59907
  "date",
59544
59908
  "time",
59545
59909
  "datetime"
@@ -59547,12 +59911,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59547
59911
  var EnumSensorStatusSchema = object({
59548
59912
  value: string(),
59549
59913
  /**
59550
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59551
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59552
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59553
- * a time-only sensor → `'time'`.
59914
+ * Set when `value` is a timestamp rather than an enum state, so history
59915
+ * does not learn ISO strings as states. The display shape (date / time /
59916
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59554
59917
  */
59555
- format: EnumSensorDateTimeFormatSchema.optional(),
59918
+ timestamp: boolean().optional(),
59556
59919
  /** Ms epoch when the slice was last updated. */
59557
59920
  lastFetchedAt: number()
59558
59921
  });
@@ -59579,11 +59942,11 @@ var enumSensorCapability = {
59579
59942
  * History (D850): the value as an `enum` series (an appliance's program or
59580
59943
  * operation state, an HVAC action). The option set is per device, so the
59581
59944
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59582
- * `format` carries a timestamp, not a state, and is not recorded.
59945
+ * `timestamp` carries a clock, not a state, and is not recorded.
59583
59946
  */
59584
59947
  history: { fields: { value: {
59585
59948
  kind: "enum",
59586
- skipWhenSet: "format"
59949
+ skipWhenSet: "timestamp"
59587
59950
  } } },
59588
59951
  /** Clock fields: written, but excluded from the compare that decides
59589
59952
  * whether persisting is worth a SQLite commit. */
@@ -59603,13 +59966,13 @@ var EventFireSchema = object({
59603
59966
  deviceId: number(),
59604
59967
  eventType: string(),
59605
59968
  data: record(string(), unknown()).nullable(),
59606
- timestamp: number(),
59607
- seq: number()
59969
+ timestamp: number().meta(readingMeta("none")),
59970
+ seq: number().meta(readingMeta("count"))
59608
59971
  });
59609
59972
  var EventEmitterStatusSchema = object({
59610
59973
  eventTypes: array(string()),
59611
59974
  lastEvent: EventFireSchema.nullable(),
59612
- eventCountSinceStart: number()
59975
+ eventCountSinceStart: number().meta(readingMeta("count"))
59613
59976
  });
59614
59977
  var eventEmitterCapability = {
59615
59978
  name: "event-emitter",
@@ -60022,10 +60385,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60022
60385
  var FanControlStatusSchema = object({
60023
60386
  /** Active speed as 0..100 inclusive. Null when the device has no
60024
60387
  * speed surface (single-speed fan). */
60025
- percentage: number().min(0).max(100).nullable(),
60388
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60026
60389
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60027
60390
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60028
- percentageStep: number().positive().optional(),
60391
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60029
60392
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60030
60393
  * — empty string when no preset is active or supported. */
60031
60394
  preset: string(),
@@ -60146,7 +60509,7 @@ var FeatureProbeStatusSchema = object({
60146
60509
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60147
60510
  model: string().nullable(),
60148
60511
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60149
- channelCount: number().nullable(),
60512
+ channelCount: number().nullable().meta(readingMeta("count")),
60150
60513
  /**
60151
60514
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60152
60515
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60269,9 +60632,91 @@ var gasCapability = {
60269
60632
  * whether persisting is worth a SQLite commit. */
60270
60633
  volatileStateFields: ["lastChangedAt"]
60271
60634
  };
60635
+ var GroupCommandsSchema = object({
60636
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60637
+ results: array(object({
60638
+ deviceId: number().int().nonnegative(),
60639
+ cap: string().min(1),
60640
+ method: string().min(1),
60641
+ ok: boolean(),
60642
+ error: string().optional(),
60643
+ /** Ms epoch of the attempt. */
60644
+ at: number()
60645
+ })) });
60646
+ var groupCommandsCapability = {
60647
+ name: "group-commands",
60648
+ scope: "device",
60649
+ deviceNative: true,
60650
+ mode: "singleton",
60651
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
60652
+ methods: {},
60653
+ status: {
60654
+ schema: GroupCommandsSchema,
60655
+ kind: "push"
60656
+ },
60657
+ runtimeState: GroupCommandsSchema,
60658
+ /**
60659
+ * Runtime-state durability: **session** — the last attempt. A restored
60660
+ * failure would outlive the command that caused it.
60661
+ *
60662
+ * See `RuntimeStateDurability`. Enforced by
60663
+ * `scripts/check-runtime-state-durability.ts`.
60664
+ */
60665
+ durability: "session",
60666
+ history: "none"
60667
+ };
60668
+ /**
60669
+ * How many members of a helper group answered (D944). Counts are integers,
60670
+ * not measurements: zero included is a real count, never a stand-in reading.
60671
+ * The reading itself stays on the output capability. History is none — the
60672
+ * series that later statistics read is the output cap's own history block.
60673
+ */
60674
+ var GroupStatusSchema = object({
60675
+ /** Members whose current value entered the aggregate. */
60676
+ included: number().int().nonnegative().meta(readingMeta("count", {
60677
+ precision: 0,
60678
+ label: "Reporting"
60679
+ })),
60680
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60681
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60682
+ precision: 0,
60683
+ label: "Excluded"
60684
+ })),
60685
+ /** `included` + `excluded`. */
60686
+ total: number().int().nonnegative().meta(readingMeta("count", {
60687
+ precision: 0,
60688
+ label: "Members"
60689
+ }))
60690
+ });
60691
+ var groupStatusCapability = {
60692
+ name: "group-status",
60693
+ scope: "device",
60694
+ deviceNative: true,
60695
+ mode: "singleton",
60696
+ deviceTypes: [
60697
+ DeviceType.Sensor,
60698
+ DeviceType.Switch,
60699
+ DeviceType.Light
60700
+ ],
60701
+ methods: {},
60702
+ status: {
60703
+ schema: GroupStatusSchema,
60704
+ kind: "push"
60705
+ },
60706
+ runtimeState: GroupStatusSchema,
60707
+ /**
60708
+ * Runtime-state durability: **session** — recomputed from members on the
60709
+ * next source event; a restored count would outlive the readings it named.
60710
+ *
60711
+ * See `RuntimeStateDurability`. Enforced by
60712
+ * `scripts/check-runtime-state-durability.ts`.
60713
+ */
60714
+ durability: "session",
60715
+ history: "none"
60716
+ };
60272
60717
  var HeatingValveStatusSchema = object({
60273
60718
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60274
- position: number().min(0).max(100).nullable(),
60719
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60275
60720
  /** Ms epoch when the slice was last updated (push or poll). */
60276
60721
  lastFetchedAt: number()
60277
60722
  });
@@ -60314,9 +60759,9 @@ var HumidifierStatusSchema = object({
60314
60759
  /** Whether the humidifier is currently on. */
60315
60760
  on: boolean(),
60316
60761
  /** Current measured relative humidity (0..100). Null when not reported. */
60317
- currentHumidity: number().min(0).max(100).nullable(),
60762
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60318
60763
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60319
- targetHumidity: number().min(0).max(100).nullable(),
60764
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60320
60765
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60321
60766
  * device has no mode surface. */
60322
60767
  mode: string().nullable(),
@@ -60326,9 +60771,9 @@ var HumidifierStatusSchema = object({
60326
60771
  * `idle` / `off`). Null when the device doesn't report it. */
60327
60772
  action: string().nullable(),
60328
60773
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60329
- minHumidity: number().nullable(),
60774
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60330
60775
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60331
- maxHumidity: number().nullable(),
60776
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60332
60777
  /** Ms epoch when the slice was last updated. */
60333
60778
  lastChangedAt: number()
60334
60779
  });
@@ -60398,22 +60843,13 @@ var humidifierCapability = {
60398
60843
  * `sourceInfo`.
60399
60844
  */
60400
60845
  var HumiditySensorStatusSchema = object({
60401
- /** Current relative humidity, 0..100. */
60402
- percent: number().min(0).max(100).meta({
60403
- unit: "%",
60846
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60847
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60848
+ precision: 0,
60404
60849
  label: "Humidity"
60405
- }),
60850
+ })),
60406
60851
  /** Ms epoch when the slice was last updated. */
60407
- lastFetchedAt: number(),
60408
- /** Live display unit from the upstream source (e.g. HA
60409
- * `attributes.unit_of_measurement`). The UI prefers this over the
60410
- * role's canonical unit. Absent → fall back to the canonical unit. */
60411
- unit: string().optional(),
60412
- /** Suggested decimal places for numeric display.
60413
- * Populated live from the upstream source when provided (e.g. HA
60414
- * `attributes.suggested_display_precision`). Falls back to
60415
- * auto-formatting when absent. */
60416
- precision: number().int().min(0).max(10).optional()
60852
+ lastFetchedAt: number()
60417
60853
  });
60418
60854
  var humiditySensorCapability = {
60419
60855
  name: "humidity-sensor",
@@ -60444,12 +60880,7 @@ var humiditySensorCapability = {
60444
60880
  } } },
60445
60881
  /** Clock fields: written, but excluded from the compare that decides
60446
60882
  * whether persisting is worth a SQLite commit. */
60447
- volatileStateFields: ["lastFetchedAt"],
60448
- customize: {
60449
- version: 1,
60450
- advanced: "derive",
60451
- targets: []
60452
- }
60883
+ volatileStateFields: ["lastFetchedAt"]
60453
60884
  };
60454
60885
  /**
60455
60886
  * Image display cap. Models a single still image exposed by an integration —
@@ -60470,7 +60901,7 @@ var ImageStatusSchema = object({
60470
60901
  * entity exposes no `entity_picture` (yet). */
60471
60902
  url: string().nullable(),
60472
60903
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60473
- lastUpdated: number().nullable()
60904
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60474
60905
  });
60475
60906
  var imageCapability = {
60476
60907
  name: "image",
@@ -60558,19 +60989,19 @@ var NormalizedRangeSchema = object({
60558
60989
  */
60559
60990
  var ImageSettingsStatusSchema = object({
60560
60991
  /** Normalized 0–100. */
60561
- brightness: number().optional(),
60992
+ brightness: number().optional().meta(readingMeta("none")),
60562
60993
  /** Normalized 0–100. */
60563
- contrast: number().optional(),
60994
+ contrast: number().optional().meta(readingMeta("none")),
60564
60995
  /** Normalized 0–100. */
60565
- saturation: number().optional(),
60996
+ saturation: number().optional().meta(readingMeta("none")),
60566
60997
  /** Normalized 0–100. */
60567
- sharpness: number().optional(),
60998
+ sharpness: number().optional().meta(readingMeta("none")),
60568
60999
  mirror: boolean().optional(),
60569
61000
  flip: boolean().optional(),
60570
61001
  rotate: ImageRotateSchema.optional(),
60571
61002
  whiteBalance: WhiteBalanceModeSchema.optional(),
60572
61003
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60573
- warmth: number().optional(),
61004
+ warmth: number().optional().meta(readingMeta("none")),
60574
61005
  exposureMode: ExposureModeSchema.optional(),
60575
61006
  backlightMode: BacklightModeSchema.optional(),
60576
61007
  lastFetchedAt: number()
@@ -60849,10 +61280,10 @@ var IntercomAbilitySchema = object({
60849
61280
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60850
61281
  codecs: array(string()),
60851
61282
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60852
- sampleRate: number(),
61283
+ sampleRate: number().meta(readingMeta("none")),
60853
61284
  duplex: _enum(["half", "full"]),
60854
61285
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60855
- maxBacklogMs: number()
61286
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60856
61287
  });
60857
61288
  var IntercomStatusSchema = object({
60858
61289
  /** True while a talk session is open. */
@@ -61079,13 +61510,13 @@ var LawnMowerControlStatusSchema = object({
61079
61510
  activity: LawnMowerActivitySchema,
61080
61511
  /** 0..100 battery percentage. Null when the device has no battery
61081
61512
  * reading. */
61082
- batteryLevel: number().min(0).max(100).nullable(),
61513
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61083
61514
  /** 0..100 mowing-completion percentage of the current task, or null when no
61084
61515
  * task is active / progress is unavailable. */
61085
- progressPercent: number().min(0).max(100).nullable(),
61516
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61086
61517
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61087
61518
  * or null when unknown. */
61088
- currentCode: number().nullable(),
61519
+ currentCode: number().nullable().meta(readingMeta("none")),
61089
61520
  /** Human label for {@link currentCode}, or null when undecodable. */
61090
61521
  currentCodeLabel: string().nullable(),
61091
61522
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61618,7 +62049,7 @@ var MediaPlayerStatusSchema = object({
61618
62049
  state: MediaPlayerStateSchema,
61619
62050
  /** Volume as 0..100 inclusive. Null when the device has no volume
61620
62051
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61621
- volumeLevel: number().min(0).max(100).nullable(),
62052
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61622
62053
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61623
62054
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61624
62055
  isMuted: boolean().nullable(),
@@ -61631,10 +62062,10 @@ var MediaPlayerStatusSchema = object({
61631
62062
  currentMedia: MediaInfoSchema.nullable(),
61632
62063
  /** Current playback position in ms. Null when not seekable or
61633
62064
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61634
- positionMs: number().int().nonnegative().nullable(),
62065
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61635
62066
  /** Total duration of the current media in ms. Null when unknown
61636
62067
  * (live stream) or nothing is playing. */
61637
- durationMs: number().int().nonnegative().nullable(),
62068
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61638
62069
  /** Shuffle toggle. Null when no shuffle surface.
61639
62070
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61640
62071
  shuffle: boolean().nullable(),
@@ -61943,7 +62374,7 @@ var motionCustomize = {
61943
62374
  eventName: "onMotionChanged"
61944
62375
  },
61945
62376
  deviceTypes: [DeviceType.Camera],
61946
- apply: "replace",
62377
+ apply: "add",
61947
62378
  applyAuthority: {
61948
62379
  kind: "camera-sensors",
61949
62380
  timeoutField: "motionCooldownMs"
@@ -62015,7 +62446,7 @@ var MotionStatusSchema = object({
62015
62446
  * it, every provider seeded it at boot, and every session left it
62016
62447
  * `detected: true` after its detach.
62017
62448
  */
62018
- autoClearAfterMs: number().nullable()
62449
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62019
62450
  });
62020
62451
  /**
62021
62452
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62052,12 +62483,18 @@ var motionCapability = {
62052
62483
  methods: {
62053
62484
  /**
62054
62485
  * Pull the current motion state synchronously. Convenience shortcut
62055
- * for consumers that don't need the full status object; equivalent
62056
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62057
- * `getStatus` once the auto-injected status surface lands in every
62058
- * consumer.
62486
+ * for consumers that don't need the full status object. A composed
62487
+ * group answers from its slice (D944): unknown throws, it is not off.
62059
62488
  */
62060
62489
  isDetected: method(object({ deviceId: number() }), boolean()) },
62490
+ /**
62491
+ * A composed motion group serves `isDetected` from `detected` (D944).
62492
+ * Without this the cap stays uncomposable (`has-commands`).
62493
+ */
62494
+ composedMethods: { isDetected: {
62495
+ kind: "slice",
62496
+ field: "detected"
62497
+ } },
62061
62498
  events: {
62062
62499
  /**
62063
62500
  * Fires every time the runner transitions a camera between
@@ -62197,7 +62634,7 @@ var MotionZoneRegionSchema = object({
62197
62634
  * the grid region(s). */
62198
62635
  var MotionZoneStatusSchema = object({
62199
62636
  enabled: boolean(),
62200
- sensitivity: number(),
62637
+ sensitivity: number().meta(readingMeta("none")),
62201
62638
  /** Grid region(s). Today exactly one `grid` shape. */
62202
62639
  regions: array(MotionZoneRegionSchema),
62203
62640
  lastFetchedAt: number()
@@ -62567,7 +63004,7 @@ var NavigationStatusSchema = object({
62567
63004
  following: boolean(),
62568
63005
  flash: boolean(),
62569
63006
  lightMode: NavigationLightModeSchema,
62570
- lightLevel: number().min(40).max(100),
63007
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62571
63008
  /** Ms epoch when the slice was last updated. */
62572
63009
  lastChangedAt: number()
62573
63010
  });
@@ -62701,13 +63138,13 @@ var NetworkLinkStatusSchema = object({
62701
63138
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62702
63139
  * SKIP a null rather than coerce it.
62703
63140
  */
62704
- signalPercent: number().min(0).max(100).nullable(),
63141
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62705
63142
  /** Raw received signal strength in dBm, when the firmware reports one. */
62706
- rssiDbm: number().optional(),
63143
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62707
63144
  /** Network name of a wireless link, when the firmware reports it. */
62708
63145
  ssid: string().optional(),
62709
63146
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62710
- lastUpdated: number()
63147
+ lastUpdated: number().meta(readingMeta("none"))
62711
63148
  });
62712
63149
  var networkLinkCapability = {
62713
63150
  name: "network-link",
@@ -63092,7 +63529,7 @@ var NotifierStatusSchema = object({
63092
63529
  lastError: string().nullable(),
63093
63530
  /** Number of deliveries currently buffered server-side (rate-limited
63094
63531
  * notifiers may queue). 0 when the provider sends synchronously. */
63095
- queueDepth: number().int().nonnegative(),
63532
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63096
63533
  /** Per-feature capability matrix — authoritative source for the
63097
63534
  * compose-form field gating. */
63098
63535
  supports: NotifierSupportsSchema
@@ -63184,11 +63621,9 @@ onSent: { data: object({
63184
63621
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63185
63622
  * for: water flow, distance, weight, frequency, signal strength, …).
63186
63623
  *
63187
- * The `unit` and `precision` fields carry the live display metadata that
63188
- * the upstream source provides with each state push (HA
63189
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63190
- * The UI reads them directly from the slice — they are the single source
63191
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63624
+ * The quantity is not on this slice. It is chosen when the device is
63625
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63626
+ * that quantity's base unit. `null` is a refused sample (D393).
63192
63627
  *
63193
63628
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63194
63629
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63197,15 +63632,8 @@ onSent: { data: object({
63197
63632
  * numeric values land here.
63198
63633
  */
63199
63634
  var NumericSensorStatusSchema = object({
63200
- value: number(),
63201
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63202
- * Populated live from the upstream source on each state push.
63203
- * Absent for unitless sensors. */
63204
- unit: string().optional(),
63205
- /** Suggested decimal places for numeric display.
63206
- * Populated live from the upstream source when provided.
63207
- * Falls back to auto-formatting when absent. */
63208
- precision: number().int().min(0).max(10).optional(),
63635
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63636
+ value: number().nullable().meta(readingMeta("device")),
63209
63637
  /** Ms epoch when the slice was last updated. */
63210
63638
  lastFetchedAt: number()
63211
63639
  });
@@ -63229,11 +63657,11 @@ var numericSensorCapability = {
63229
63657
  */
63230
63658
  durability: "restored",
63231
63659
  /**
63232
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63660
+ * History (D850): recorded series. The unit is the device quantity's base,
63661
+ * resolved when the series is read — the cap has no single unit.
63233
63662
  */
63234
63663
  history: { fields: { value: {
63235
63664
  kind: "gauge",
63236
- unitFrom: "unit",
63237
63665
  minIntervalMs: 3e4
63238
63666
  } } },
63239
63667
  /** Clock fields: written, but excluded from the compare that decides
@@ -63571,23 +63999,26 @@ var PetFeederStatusSchema = object({
63571
63999
  /** Food currently in the bowl (grams). Null when the device has not
63572
64000
  * reported a reading yet. On dual-hopper models this is the combined
63573
64001
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63574
- foodLevel: number().nullable(),
64002
+ foodLevel: number().nullable().meta(readingMeta("none")),
63575
64003
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63576
64004
  * single-hopper models. */
63577
- food1: number().nullable(),
64005
+ food1: number().nullable().meta(readingMeta("none")),
63578
64006
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63579
64007
  * single-hopper models. */
63580
- food2: number().nullable(),
64008
+ food2: number().nullable().meta(readingMeta("none")),
63581
64009
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63582
64010
  * (`device_class: problem`, on = low). True when the bowl is empty /
63583
64011
  * below the feeder's low threshold. */
63584
64012
  lowFood: boolean(),
63585
64013
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63586
64014
  * device has no battery reading. */
63587
- batteryPower: number().min(0).max(100).nullable(),
63588
- /** Days of desiccant life remaining. Null when the model has no
63589
- * desiccant sensor. */
63590
- desiccantLeftDays: number().nullable(),
64015
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64016
+ /**
64017
+ * Desiccant life remaining, stored in seconds (duration base, D944).
64018
+ * Providers convert from the days the feeder reports. Null when the
64019
+ * model has no desiccant sensor.
64020
+ */
64021
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63591
64022
  /** True while a feed is in progress. */
63592
64023
  feeding: boolean(),
63593
64024
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63598,7 +64029,7 @@ var PetFeederStatusSchema = object({
63598
64029
  * with `errorCode` for consumers that want the raw integer. */
63599
64030
  error: string().nullable(),
63600
64031
  /** Raw device error code (0 / null = no error). */
63601
- errorCode: number().nullable(),
64032
+ errorCode: number().nullable().meta(readingMeta("none")),
63602
64033
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63603
64034
  isDualHopper: boolean(),
63604
64035
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63608,7 +64039,7 @@ var PetFeederStatusSchema = object({
63608
64039
  /** Play a chime when dispensing. */
63609
64040
  feedSound: boolean(),
63610
64041
  /** Speaker / prompt volume level (device-scaled integer). */
63611
- volume: number(),
64042
+ volume: number().meta(readingMeta("none")),
63612
64043
  /** Ms epoch when the slice was last refreshed from the cloud. */
63613
64044
  lastFetchedAt: number()
63614
64045
  });
@@ -63727,7 +64158,7 @@ var petFeederCapability = {
63727
64158
  },
63728
64159
  desiccantLeftDays: {
63729
64160
  kind: "gauge",
63730
- unit: "days"
64161
+ unit: "s"
63731
64162
  }
63732
64163
  } }
63733
64164
  };
@@ -64128,36 +64559,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64128
64559
  */
64129
64560
  var PowerMeterStatusSchema = object({
64130
64561
  /** Instantaneous power draw in watts. */
64131
- watts: number().optional().meta({
64132
- unit: "W",
64133
- label: "Power"
64134
- }),
64562
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64135
64563
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64136
- kwhTotal: number().optional().meta({
64137
- unit: "kWh",
64138
- label: "Energy"
64139
- }),
64564
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64140
64565
  /** Voltage in volts. */
64141
- volts: number().optional(),
64566
+ volts: number().optional().meta(readingMeta("voltage")),
64142
64567
  /** Current in amperes. */
64143
- amps: number().optional(),
64144
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64145
- va: number().optional(),
64568
+ amps: number().optional().meta(readingMeta("current")),
64569
+ /** Apparent power in volt-amperes. */
64570
+ va: number().optional().meta(readingMeta("apparent-power")),
64146
64571
  /** Power factor (0..1) if reported. */
64147
- powerFactor: number().min(0).max(1).optional(),
64572
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64148
64573
  /** Ms epoch when the slice was last updated. */
64149
- lastFetchedAt: number(),
64150
- /** Live display unit of the single metric this slice carries (e.g. HA
64151
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64152
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64153
- * unit per slice is unambiguous. The UI prefers this over the role's
64154
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64155
- unit: string().optional(),
64156
- /** Suggested decimal places for numeric display.
64157
- * Populated live from the upstream source when provided (e.g. HA
64158
- * `attributes.suggested_display_precision`). Falls back to
64159
- * auto-formatting when absent. */
64160
- precision: number().int().min(0).max(10).optional()
64574
+ lastFetchedAt: number()
64161
64575
  });
64162
64576
  var powerMeterCapability = {
64163
64577
  name: "power-meter",
@@ -64221,12 +64635,7 @@ var powerMeterCapability = {
64221
64635
  } },
64222
64636
  /** Clock fields: written, but excluded from the compare that decides
64223
64637
  * whether persisting is worth a SQLite commit. */
64224
- volatileStateFields: ["lastFetchedAt"],
64225
- customize: {
64226
- version: 1,
64227
- advanced: "derive",
64228
- targets: []
64229
- }
64638
+ volatileStateFields: ["lastFetchedAt"]
64230
64639
  };
64231
64640
  /**
64232
64641
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64246,11 +64655,11 @@ var powerMeterCapability = {
64246
64655
  */
64247
64656
  var GpsLocationSchema = object({
64248
64657
  /** Latitude in decimal degrees, -90..90. */
64249
- latitude: number().min(-90).max(90),
64658
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64250
64659
  /** Longitude in decimal degrees, -180..180. */
64251
- longitude: number().min(-180).max(180),
64660
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64252
64661
  /** Reported accuracy in meters (lower = better). */
64253
- accuracyMeters: number().nonnegative()
64662
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64254
64663
  });
64255
64664
  var PresenceStatusSchema = object({
64256
64665
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64262,7 +64671,7 @@ var PresenceStatusSchema = object({
64262
64671
  gps: GpsLocationSchema.nullable(),
64263
64672
  /** Optional battery level of the tracking device (0..100). Null
64264
64673
  * when not reported. */
64265
- batteryPercent: number().min(0).max(100).nullable(),
64674
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64266
64675
  /** Ms epoch when the slice was last updated. */
64267
64676
  lastChangedAt: number()
64268
64677
  });
@@ -64312,19 +64721,10 @@ var presenceCapability = {
64312
64721
  * `sourceInfo`.
64313
64722
  */
64314
64723
  var PressureSensorStatusSchema = object({
64315
- /** Current pressure in hPa. */
64316
- hpa: number(),
64724
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64725
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64317
64726
  /** Ms epoch when the slice was last updated. */
64318
- lastFetchedAt: number(),
64319
- /** Live display unit from the upstream source (e.g. HA
64320
- * `attributes.unit_of_measurement`). The UI prefers this over the
64321
- * role's canonical unit. Absent → fall back to the canonical unit. */
64322
- unit: string().optional(),
64323
- /** Suggested decimal places for numeric display.
64324
- * Populated live from the upstream source when provided (e.g. HA
64325
- * `attributes.suggested_display_precision`). Falls back to
64326
- * auto-formatting when absent. */
64327
- precision: number().int().min(0).max(10).optional()
64727
+ lastFetchedAt: number()
64328
64728
  });
64329
64729
  var pressureSensorCapability = {
64330
64730
  name: "pressure-sensor",
@@ -64850,8 +65250,8 @@ var ptzCapability = {
64850
65250
  };
64851
65251
  var PtzAutotrackSettingsSchema = object({
64852
65252
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64853
- stopDelaySeconds: number().int().min(0).max(300),
64854
- disappearDelaySeconds: number().int().min(0).max(300)
65253
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65254
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64855
65255
  });
64856
65256
  /**
64857
65257
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65005,7 +65405,7 @@ var RunningStateStatusSchema = object({
65005
65405
  /** null = unknown — never coerced to false (D393). */
65006
65406
  running: boolean().nullable(),
65007
65407
  /** Epoch ms the current boolean began; null while `running` is null. */
65008
- since: number().nullable(),
65408
+ since: number().nullable().meta(readingMeta("none")),
65009
65409
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65010
65410
  phase: string().nullable(),
65011
65411
  /** null until a reading has been published (D393). */
@@ -65020,7 +65420,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65020
65420
  /** Generic Customize target for the running boolean (D936). */
65021
65421
  var runningCustomize = {
65022
65422
  version: 1,
65023
- advanced: "derive",
65024
65423
  targets: [{
65025
65424
  id: "running",
65026
65425
  cardinality: "scalar",
@@ -65040,7 +65439,7 @@ var runningCustomize = {
65040
65439
  DeviceType.Thermostat,
65041
65440
  DeviceType.PetFeeder
65042
65441
  ],
65043
- apply: "add-or-replace",
65442
+ apply: "add",
65044
65443
  path: "running",
65045
65444
  sources: [
65046
65445
  {
@@ -66308,9 +66707,9 @@ var RecordTriggerSchema = _enum([
66308
66707
  */
66309
66708
  var RecordWindowSchema = object({
66310
66709
  trigger: RecordTriggerSchema,
66311
- day: number().int().min(0).max(6),
66312
- startMinute: number().int().min(0).max(1439),
66313
- endMinute: number().int().min(1).max(1440)
66710
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66711
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66712
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
66314
66713
  });
66315
66714
  /** Status of one physical volume, as the camera itself describes it. */
66316
66715
  var OnboardStorageVolumeSchema = object({
@@ -66386,10 +66785,10 @@ var RecordingOnboardStatusSchema = object({
66386
66785
  /** Master "record to the card at all" switch. */
66387
66786
  enabled: boolean().nullable(),
66388
66787
  overwriteWhenFull: boolean().nullable(),
66389
- preRecordSec: number().nullable(),
66390
- postRecordSec: number().nullable(),
66788
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66789
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66391
66790
  /** Length of one recorded file, in minutes. */
66392
- segmentMinutes: number().nullable(),
66791
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66393
66792
  /** The primary track's weekly windows, flattened. */
66394
66793
  windows: array(RecordWindowSchema),
66395
66794
  /**
@@ -66405,7 +66804,7 @@ var RecordingOnboardStatusSchema = object({
66405
66804
  * provider reporting a non-zero count also reports the schedule as not
66406
66805
  * writable.
66407
66806
  */
66408
- unreadableWindows: number(),
66807
+ unreadableWindows: number().meta(readingMeta("count")),
66409
66808
  /**
66410
66809
  * The camera is scheduled to record and has NO usable storage.
66411
66810
  *
@@ -67352,7 +67751,7 @@ var CamStreamDescriptorSchema = object({
67352
67751
  url: string().optional(),
67353
67752
  codec: string().optional(),
67354
67753
  resolution: CamStreamResolutionSchema.optional(),
67355
- fps: number().positive().optional(),
67754
+ fps: number().positive().optional().meta(readingMeta("rate")),
67356
67755
  label: string().optional(),
67357
67756
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
67358
67757
  deviceFeatures: array(string()).optional(),
@@ -67439,18 +67838,18 @@ var StreamProfileSchema = _enum([
67439
67838
  ]);
67440
67839
  /** The current encoding config of one stream profile. */
67441
67840
  var StreamProfileConfigSchema = object({
67442
- width: number(),
67443
- height: number(),
67841
+ width: number().meta(readingMeta("none")),
67842
+ height: number().meta(readingMeta("none")),
67444
67843
  codec: _enum(["h264", "h265"]),
67445
- framerate: number(),
67446
- bitrate: number(),
67844
+ framerate: number().meta(readingMeta("rate")),
67845
+ bitrate: number().meta(readingMeta("bitrate")),
67447
67846
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
67448
67847
  encoderProfile: _enum([
67449
67848
  "high",
67450
67849
  "main",
67451
67850
  "baseline"
67452
67851
  ]).optional(),
67453
- gop: number().optional(),
67852
+ gop: number().optional().meta(readingMeta("none")),
67454
67853
  /**
67455
67854
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
67456
67855
  *
@@ -67869,12 +68268,7 @@ var switchCapability = {
67869
68268
  label: "On",
67870
68269
  kind: "boolean"
67871
68270
  }
67872
- }] },
67873
- customize: {
67874
- version: 1,
67875
- advanced: "derive",
67876
- targets: []
67877
- }
68271
+ }] }
67878
68272
  };
67879
68273
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67880
68274
  function hasControlCharacter(value) {
@@ -68457,35 +68851,21 @@ var tamperCapability = {
68457
68851
  * entries with `device_class: temperature` and any future native
68458
68852
  * thermometer.
68459
68853
  *
68460
- * Unit normalisation: providers convert to Celsius before storing.
68461
- * The slice value is always Celsius so cross-cap aggregators
68462
- * (climate-control's `currentTemp`, energy analytics) can compose
68463
- * without per-source unit fixups. The canonical display unit (`°C`) is
68464
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68465
- * `sourceInfo`.
68854
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68855
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
68466
68856
  *
68467
68857
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
68468
68858
  * frozen feed (provider hung) distinct from a "temperature hasn't
68469
68859
  * changed" steady state.
68470
68860
  */
68471
68861
  var TemperatureSensorStatusSchema = object({
68472
- /** Current temperature in Celsius. */
68473
- celsius: number().meta({
68474
- unit: "°C",
68862
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68863
+ celsius: number().nullable().meta(readingMeta("temperature", {
68864
+ precision: 1,
68475
68865
  label: "Temperature"
68476
- }),
68866
+ })),
68477
68867
  /** Ms epoch when the slice was last updated (push or poll). */
68478
- lastFetchedAt: number(),
68479
- /** Live display unit from the upstream source (e.g. HA
68480
- * `attributes.unit_of_measurement`). The UI prefers this over the
68481
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
68482
- * Absent → the UI falls back to the role's canonical unit. */
68483
- unit: string().optional(),
68484
- /** Suggested decimal places for numeric display.
68485
- * Populated live from the upstream source when provided (e.g. HA
68486
- * `attributes.suggested_display_precision`). Falls back to
68487
- * auto-formatting when absent. */
68488
- precision: number().int().min(0).max(10).optional()
68868
+ lastFetchedAt: number()
68489
68869
  });
68490
68870
  var temperatureSensorCapability = {
68491
68871
  name: "temperature-sensor",
@@ -68516,12 +68896,7 @@ var temperatureSensorCapability = {
68516
68896
  } } },
68517
68897
  /** Clock fields: written, but excluded from the compare that decides
68518
68898
  * whether persisting is worth a SQLite commit. */
68519
- volatileStateFields: ["lastFetchedAt"],
68520
- customize: {
68521
- version: 1,
68522
- advanced: "derive",
68523
- targets: []
68524
- }
68899
+ volatileStateFields: ["lastFetchedAt"]
68525
68900
  };
68526
68901
  /**
68527
68902
  * toast — system-scoped singleton capability that streams toast
@@ -68804,16 +69179,16 @@ var VacuumControlStatusSchema = object({
68804
69179
  ]),
68805
69180
  /** 0..100 battery percentage. Null when the device has no battery
68806
69181
  * reading. */
68807
- batteryLevel: number().min(0).max(100).nullable(),
69182
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68808
69183
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68809
69184
  * the vacuum has no speed control. */
68810
69185
  fanSpeed: string().nullable(),
68811
69186
  /** Speed tokens the hardware accepts — drives the UI selector. */
68812
69187
  availableFanSpeeds: array(string()),
68813
69188
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68814
- progressPercent: number().min(0).max(100).nullable(),
69189
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68815
69190
  /** Current error code (0 / null = no error). */
68816
- errorCode: number().nullable(),
69191
+ errorCode: number().nullable().meta(readingMeta("none")),
68817
69192
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68818
69193
  errorLabel: string().nullable(),
68819
69194
  /** Ms epoch when the slice was last updated. */
@@ -68891,7 +69266,7 @@ var ValveStatusSchema = object({
68891
69266
  ]),
68892
69267
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68893
69268
  * intermediate position surface. */
68894
- position: number().min(0).max(100).nullable(),
69269
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68895
69270
  /** Ms epoch when the slice was last updated. */
68896
69271
  lastChangedAt: number()
68897
69272
  });
@@ -68999,9 +69374,9 @@ var vibrationCapability = {
68999
69374
  */
69000
69375
  var WaterHeaterStatusSchema = object({
69001
69376
  /** Current measured temperature. Null when not reported. */
69002
- currentTemp: number().nullable(),
69377
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
69003
69378
  /** Target temperature setpoint. Null when no setpoint surface. */
69004
- targetTemp: number().nullable(),
69379
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
69005
69380
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
69006
69381
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
69007
69382
  * device reports an unknown state. */
@@ -69012,9 +69387,9 @@ var WaterHeaterStatusSchema = object({
69012
69387
  * has no away surface. */
69013
69388
  away: boolean().nullable(),
69014
69389
  /** HA `min_temp` attribute. Null when not reported. */
69015
- minTemp: number().nullable(),
69390
+ minTemp: number().nullable().meta(readingMeta("temperature")),
69016
69391
  /** HA `max_temp` attribute. Null when not reported. */
69017
- maxTemp: number().nullable(),
69392
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
69018
69393
  /** Ms epoch when the slice was last updated. */
69019
69394
  lastChangedAt: number()
69020
69395
  });
@@ -69099,25 +69474,18 @@ var waterHeaterCapability = {
69099
69474
  * Forecast deferred — current conditions only for v1.
69100
69475
  */
69101
69476
  var WeatherStatusSchema = object({
69102
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69103
- * when no condition has been reported yet. */
69477
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69104
69478
  condition: string().nullable(),
69105
- /** Current temperature in the reported unit. Null when not provided. */
69106
- temperature: number().nullable(),
69107
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69108
- temperatureUnit: string().nullable(),
69479
+ /** Current temperature in Celsius. Null when not provided or refused. */
69480
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69109
69481
  /** Relative humidity (0..100). Null when not provided. */
69110
- humidity: number().min(0).max(100).nullable(),
69111
- /** Barometric pressure in the reported unit. Null when not provided. */
69112
- pressure: number().nullable(),
69113
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69114
- pressureUnit: string().nullable(),
69115
- /** Wind speed in the reported unit. Null when not provided. */
69116
- windSpeed: number().nullable(),
69117
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69118
- windSpeedUnit: string().nullable(),
69482
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
69483
+ /** Barometric pressure in hPa. Null when not provided or refused. */
69484
+ pressure: number().nullable().meta(readingMeta("pressure")),
69485
+ /** Wind speed in metres per second. Null when not provided or refused. */
69486
+ windSpeed: number().nullable().meta(readingMeta("speed")),
69119
69487
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69120
- windBearing: number().nullable(),
69488
+ windBearing: number().nullable().meta(readingMeta("angle")),
69121
69489
  /** Ms epoch when the slice was last updated. */
69122
69490
  lastFetchedAt: number()
69123
69491
  });
@@ -69145,12 +69513,12 @@ var weatherCapability = {
69145
69513
  */
69146
69514
  durability: "session",
69147
69515
  /**
69148
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69516
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69149
69517
  */
69150
69518
  history: { fields: {
69151
69519
  temperature: {
69152
69520
  kind: "gauge",
69153
- unitFrom: "temperatureUnit",
69521
+ unit: "°C",
69154
69522
  deadband: .2
69155
69523
  },
69156
69524
  humidity: {
@@ -69160,12 +69528,12 @@ var weatherCapability = {
69160
69528
  },
69161
69529
  pressure: {
69162
69530
  kind: "gauge",
69163
- unitFrom: "pressureUnit",
69531
+ unit: "hPa",
69164
69532
  deadband: .5
69165
69533
  },
69166
69534
  windSpeed: {
69167
69535
  kind: "gauge",
69168
- unitFrom: "windSpeedUnit",
69536
+ unit: "m/s",
69169
69537
  deadband: .5
69170
69538
  }
69171
69539
  } }
@@ -70529,6 +70897,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
70529
70897
  featureProbe: featureProbeCapability,
70530
70898
  flood: floodCapability,
70531
70899
  gas: gasCapability,
70900
+ groupCommands: groupCommandsCapability,
70901
+ groupStatus: groupStatusCapability,
70532
70902
  heatingValve: heatingValveCapability,
70533
70903
  humidifier: humidifierCapability,
70534
70904
  humiditySensor: humiditySensorCapability,
@@ -71909,6 +72279,13 @@ Object.freeze({
71909
72279
  access: "view",
71910
72280
  role: "admin"
71911
72281
  },
72282
+ "addons.getPackageDocs": {
72283
+ capName: "addons",
72284
+ capScope: "system",
72285
+ addonId: null,
72286
+ access: "view",
72287
+ role: "admin"
72288
+ },
71912
72289
  "addons.getVersions": {
71913
72290
  capName: "addons",
71914
72291
  capScope: "system",
@@ -75017,6 +75394,20 @@ Object.freeze({
75017
75394
  access: "view",
75018
75395
  role: "viewer"
75019
75396
  },
75397
+ "groupCommands.getStatus": {
75398
+ capName: "group-commands",
75399
+ capScope: "device",
75400
+ addonId: null,
75401
+ access: "view",
75402
+ role: "viewer"
75403
+ },
75404
+ "groupStatus.getStatus": {
75405
+ capName: "group-status",
75406
+ capScope: "device",
75407
+ addonId: null,
75408
+ access: "view",
75409
+ role: "viewer"
75410
+ },
75020
75411
  "heatingValve.getStatus": {
75021
75412
  capName: "heating-valve",
75022
75413
  capScope: "device",
@@ -84839,6 +85230,37 @@ var SENSOR_CAP_VOCABULARY = {
84839
85230
  icon: "flame"
84840
85231
  }]
84841
85232
  },
85233
+ "group-commands": {
85234
+ scope: "device",
85235
+ noun: t("Group command", "Comando di gruppo")
85236
+ },
85237
+ "group-status": {
85238
+ scope: "device",
85239
+ noun: t("Group", "Gruppo"),
85240
+ state: [
85241
+ {
85242
+ kind: "numeric",
85243
+ field: "included",
85244
+ quantity: t("the members reporting", "i membri che rispondono"),
85245
+ label: t("Reporting", "Rispondono"),
85246
+ icon: "users"
85247
+ },
85248
+ {
85249
+ kind: "numeric",
85250
+ field: "excluded",
85251
+ quantity: t("the members left out", "i membri esclusi"),
85252
+ label: t("Excluded", "Esclusi"),
85253
+ icon: "users"
85254
+ },
85255
+ {
85256
+ kind: "numeric",
85257
+ field: "total",
85258
+ quantity: t("the member count", "il numero di membri"),
85259
+ label: t("Members", "Membri"),
85260
+ icon: "sigma"
85261
+ }
85262
+ ]
85263
+ },
84842
85264
  "humidity-sensor": {
84843
85265
  scope: "device",
84844
85266
  noun: t("Humidity", "Umidità"),
@@ -84872,7 +85294,6 @@ var SENSOR_CAP_VOCABULARY = {
84872
85294
  field: "value",
84873
85295
  quantity: t("the reading", "il valore"),
84874
85296
  label: t("Reading", "Valore"),
84875
- unitField: "unit",
84876
85297
  icon: "gauge"
84877
85298
  }]
84878
85299
  },
@@ -85008,7 +85429,7 @@ var SENSOR_CAP_VOCABULARY = {
85008
85429
  field: "temperature",
85009
85430
  quantity: t("the outdoor temperature", "la temperatura esterna"),
85010
85431
  label: t("Temperature", "Temperatura"),
85011
- unitField: "temperatureUnit",
85432
+ unit: "°C",
85012
85433
  icon: "thermometer"
85013
85434
  },
85014
85435
  {
@@ -85024,7 +85445,7 @@ var SENSOR_CAP_VOCABULARY = {
85024
85445
  field: "windSpeed",
85025
85446
  quantity: t("the wind speed", "la velocità del vento"),
85026
85447
  label: t("Wind", "Vento"),
85027
- unitField: "windSpeedUnit",
85448
+ unit: "m/s",
85028
85449
  icon: "wind"
85029
85450
  },
85030
85451
  {
@@ -85032,7 +85453,7 @@ var SENSOR_CAP_VOCABULARY = {
85032
85453
  field: "pressure",
85033
85454
  quantity: t("the air pressure", "la pressione atmosferica"),
85034
85455
  label: t("Pressure", "Pressione"),
85035
- unitField: "pressureUnit",
85456
+ unit: "hPa",
85036
85457
  icon: "gauge"
85037
85458
  }
85038
85459
  ]
@@ -86511,6 +86932,16 @@ Object.freeze({
86511
86932
  form: "single",
86512
86933
  optional: false
86513
86934
  }],
86935
+ "groupCommands.getStatus": [{
86936
+ name: "deviceId",
86937
+ form: "single",
86938
+ optional: false
86939
+ }],
86940
+ "groupStatus.getStatus": [{
86941
+ name: "deviceId",
86942
+ form: "single",
86943
+ optional: false
86944
+ }],
86514
86945
  "heatingValve.getStatus": [{
86515
86946
  name: "deviceId",
86516
86947
  form: "single",
@@ -88495,6 +88926,8 @@ new Map(Object.entries({
88495
88926
  "detection-blind": "device-events",
88496
88927
  "motion-noisy": "device-events",
88497
88928
  "motion-calm": "device-events",
88929
+ "package-llm-silent": "device-events",
88930
+ "package-llm-answered": "device-events",
88498
88931
  "recording-stalled": "device-events",
88499
88932
  "recording-healthy": "device-events",
88500
88933
  "recording-degraded": "device-events",
@@ -88578,6 +89011,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
88578
89011
  label: "Motion without a subject",
88579
89012
  kinds: ["motion-noisy", "motion-calm"]
88580
89013
  },
89014
+ {
89015
+ id: "package-llm",
89016
+ family: "device-events",
89017
+ label: "Package confirm",
89018
+ kinds: ["package-llm-silent", "package-llm-answered"]
89019
+ },
88581
89020
  {
88582
89021
  id: "recording",
88583
89022
  family: "device-events",
@@ -88759,6 +89198,8 @@ var DEVICE_EVENTS = [
88759
89198
  "detection-blind",
88760
89199
  "motion-noisy",
88761
89200
  "motion-calm",
89201
+ "package-llm-silent",
89202
+ "package-llm-answered",
88762
89203
  "recording-stalled",
88763
89204
  "recording-healthy",
88764
89205
  "recording-degraded",
@@ -89027,6 +89468,15 @@ new Map([
89027
89468
  snapshotLabel: "Cameras with constant motion now",
89028
89469
  snapshotSection: null
89029
89470
  },
89471
+ {
89472
+ id: "package-llm-confirm",
89473
+ family: "device-events",
89474
+ label: "Package confirm (not answering / answering again)",
89475
+ bad: ["package-llm-silent"],
89476
+ recovery: ["package-llm-answered"],
89477
+ snapshotLabel: "Cameras whose package confirm is not answering",
89478
+ snapshotSection: null
89479
+ },
89030
89480
  {
89031
89481
  id: "recording-health",
89032
89482
  family: "device-events",