@camstack/addon-provider-amcrest 0.2.159 → 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. */
@@ -59445,6 +59819,7 @@ var doorbellCustomize = {
59445
59819
  applyAuthority: { kind: "composition-claim" },
59446
59820
  eventName: "onPressed",
59447
59821
  edges: ["rising"],
59822
+ defaultEdge: "rising",
59448
59823
  timestampPath: "lastPressedAt",
59449
59824
  counterPath: "pressCountSinceStart",
59450
59825
  highWater: "composer-advance",
@@ -59463,7 +59838,7 @@ var DoorbellStatusSchema = object({
59463
59838
  /** Ms epoch of the last press. null = never observed since this provider started. */
59464
59839
  lastPressedAt: number().nullable(),
59465
59840
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59466
- pressCountSinceStart: number()
59841
+ pressCountSinceStart: number().meta(readingMeta("count"))
59467
59842
  });
59468
59843
  var DoorbellPressEventSchema = object({
59469
59844
  deviceId: number(),
@@ -59528,17 +59903,7 @@ onPressed: {
59528
59903
  history: "none",
59529
59904
  customize: doorbellCustomize
59530
59905
  };
59531
- /**
59532
- * Enum-state sensor — a string value picked from a finite option set.
59533
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59534
- * states, weather conditions, contact-source channels, mode strings).
59535
- * The option set is stable across observations; it is persisted in the
59536
- * device config blob (via the control slice at adoption) and is NOT
59537
- * stored in `sourceInfo`.
59538
- */
59539
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59540
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59541
- var EnumSensorDateTimeFormatSchema = _enum([
59906
+ _enum([
59542
59907
  "date",
59543
59908
  "time",
59544
59909
  "datetime"
@@ -59546,12 +59911,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59546
59911
  var EnumSensorStatusSchema = object({
59547
59912
  value: string(),
59548
59913
  /**
59549
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59550
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59551
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59552
- * 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.
59553
59917
  */
59554
- format: EnumSensorDateTimeFormatSchema.optional(),
59918
+ timestamp: boolean().optional(),
59555
59919
  /** Ms epoch when the slice was last updated. */
59556
59920
  lastFetchedAt: number()
59557
59921
  });
@@ -59578,11 +59942,11 @@ var enumSensorCapability = {
59578
59942
  * History (D850): the value as an `enum` series (an appliance's program or
59579
59943
  * operation state, an HVAC action). The option set is per device, so the
59580
59944
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59581
- * `format` carries a timestamp, not a state, and is not recorded.
59945
+ * `timestamp` carries a clock, not a state, and is not recorded.
59582
59946
  */
59583
59947
  history: { fields: { value: {
59584
59948
  kind: "enum",
59585
- skipWhenSet: "format"
59949
+ skipWhenSet: "timestamp"
59586
59950
  } } },
59587
59951
  /** Clock fields: written, but excluded from the compare that decides
59588
59952
  * whether persisting is worth a SQLite commit. */
@@ -59602,13 +59966,13 @@ var EventFireSchema = object({
59602
59966
  deviceId: number(),
59603
59967
  eventType: string(),
59604
59968
  data: record(string(), unknown()).nullable(),
59605
- timestamp: number(),
59606
- seq: number()
59969
+ timestamp: number().meta(readingMeta("none")),
59970
+ seq: number().meta(readingMeta("count"))
59607
59971
  });
59608
59972
  var EventEmitterStatusSchema = object({
59609
59973
  eventTypes: array(string()),
59610
59974
  lastEvent: EventFireSchema.nullable(),
59611
- eventCountSinceStart: number()
59975
+ eventCountSinceStart: number().meta(readingMeta("count"))
59612
59976
  });
59613
59977
  var eventEmitterCapability = {
59614
59978
  name: "event-emitter",
@@ -60021,10 +60385,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60021
60385
  var FanControlStatusSchema = object({
60022
60386
  /** Active speed as 0..100 inclusive. Null when the device has no
60023
60387
  * speed surface (single-speed fan). */
60024
- percentage: number().min(0).max(100).nullable(),
60388
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60025
60389
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60026
60390
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60027
- percentageStep: number().positive().optional(),
60391
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60028
60392
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60029
60393
  * — empty string when no preset is active or supported. */
60030
60394
  preset: string(),
@@ -60145,7 +60509,7 @@ var FeatureProbeStatusSchema = object({
60145
60509
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60146
60510
  model: string().nullable(),
60147
60511
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60148
- channelCount: number().nullable(),
60512
+ channelCount: number().nullable().meta(readingMeta("count")),
60149
60513
  /**
60150
60514
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60151
60515
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60268,9 +60632,91 @@ var gasCapability = {
60268
60632
  * whether persisting is worth a SQLite commit. */
60269
60633
  volatileStateFields: ["lastChangedAt"]
60270
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
+ };
60271
60717
  var HeatingValveStatusSchema = object({
60272
60718
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60273
- position: number().min(0).max(100).nullable(),
60719
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60274
60720
  /** Ms epoch when the slice was last updated (push or poll). */
60275
60721
  lastFetchedAt: number()
60276
60722
  });
@@ -60313,9 +60759,9 @@ var HumidifierStatusSchema = object({
60313
60759
  /** Whether the humidifier is currently on. */
60314
60760
  on: boolean(),
60315
60761
  /** Current measured relative humidity (0..100). Null when not reported. */
60316
- currentHumidity: number().min(0).max(100).nullable(),
60762
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60317
60763
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60318
- targetHumidity: number().min(0).max(100).nullable(),
60764
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60319
60765
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60320
60766
  * device has no mode surface. */
60321
60767
  mode: string().nullable(),
@@ -60325,9 +60771,9 @@ var HumidifierStatusSchema = object({
60325
60771
  * `idle` / `off`). Null when the device doesn't report it. */
60326
60772
  action: string().nullable(),
60327
60773
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60328
- minHumidity: number().nullable(),
60774
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60329
60775
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60330
- maxHumidity: number().nullable(),
60776
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60331
60777
  /** Ms epoch when the slice was last updated. */
60332
60778
  lastChangedAt: number()
60333
60779
  });
@@ -60397,22 +60843,13 @@ var humidifierCapability = {
60397
60843
  * `sourceInfo`.
60398
60844
  */
60399
60845
  var HumiditySensorStatusSchema = object({
60400
- /** Current relative humidity, 0..100. */
60401
- percent: number().min(0).max(100).meta({
60402
- 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,
60403
60849
  label: "Humidity"
60404
- }),
60850
+ })),
60405
60851
  /** Ms epoch when the slice was last updated. */
60406
- lastFetchedAt: number(),
60407
- /** Live display unit from the upstream source (e.g. HA
60408
- * `attributes.unit_of_measurement`). The UI prefers this over the
60409
- * role's canonical unit. Absent → fall back to the canonical unit. */
60410
- unit: string().optional(),
60411
- /** Suggested decimal places for numeric display.
60412
- * Populated live from the upstream source when provided (e.g. HA
60413
- * `attributes.suggested_display_precision`). Falls back to
60414
- * auto-formatting when absent. */
60415
- precision: number().int().min(0).max(10).optional()
60852
+ lastFetchedAt: number()
60416
60853
  });
60417
60854
  var humiditySensorCapability = {
60418
60855
  name: "humidity-sensor",
@@ -60443,12 +60880,7 @@ var humiditySensorCapability = {
60443
60880
  } } },
60444
60881
  /** Clock fields: written, but excluded from the compare that decides
60445
60882
  * whether persisting is worth a SQLite commit. */
60446
- volatileStateFields: ["lastFetchedAt"],
60447
- customize: {
60448
- version: 1,
60449
- advanced: "derive",
60450
- targets: []
60451
- }
60883
+ volatileStateFields: ["lastFetchedAt"]
60452
60884
  };
60453
60885
  /**
60454
60886
  * Image display cap. Models a single still image exposed by an integration —
@@ -60469,7 +60901,7 @@ var ImageStatusSchema = object({
60469
60901
  * entity exposes no `entity_picture` (yet). */
60470
60902
  url: string().nullable(),
60471
60903
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60472
- lastUpdated: number().nullable()
60904
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60473
60905
  });
60474
60906
  var imageCapability = {
60475
60907
  name: "image",
@@ -60557,19 +60989,19 @@ var NormalizedRangeSchema = object({
60557
60989
  */
60558
60990
  var ImageSettingsStatusSchema = object({
60559
60991
  /** Normalized 0–100. */
60560
- brightness: number().optional(),
60992
+ brightness: number().optional().meta(readingMeta("none")),
60561
60993
  /** Normalized 0–100. */
60562
- contrast: number().optional(),
60994
+ contrast: number().optional().meta(readingMeta("none")),
60563
60995
  /** Normalized 0–100. */
60564
- saturation: number().optional(),
60996
+ saturation: number().optional().meta(readingMeta("none")),
60565
60997
  /** Normalized 0–100. */
60566
- sharpness: number().optional(),
60998
+ sharpness: number().optional().meta(readingMeta("none")),
60567
60999
  mirror: boolean().optional(),
60568
61000
  flip: boolean().optional(),
60569
61001
  rotate: ImageRotateSchema.optional(),
60570
61002
  whiteBalance: WhiteBalanceModeSchema.optional(),
60571
61003
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60572
- warmth: number().optional(),
61004
+ warmth: number().optional().meta(readingMeta("none")),
60573
61005
  exposureMode: ExposureModeSchema.optional(),
60574
61006
  backlightMode: BacklightModeSchema.optional(),
60575
61007
  lastFetchedAt: number()
@@ -60848,10 +61280,10 @@ var IntercomAbilitySchema = object({
60848
61280
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60849
61281
  codecs: array(string()),
60850
61282
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60851
- sampleRate: number(),
61283
+ sampleRate: number().meta(readingMeta("none")),
60852
61284
  duplex: _enum(["half", "full"]),
60853
61285
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60854
- maxBacklogMs: number()
61286
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60855
61287
  });
60856
61288
  var IntercomStatusSchema = object({
60857
61289
  /** True while a talk session is open. */
@@ -61078,13 +61510,13 @@ var LawnMowerControlStatusSchema = object({
61078
61510
  activity: LawnMowerActivitySchema,
61079
61511
  /** 0..100 battery percentage. Null when the device has no battery
61080
61512
  * reading. */
61081
- batteryLevel: number().min(0).max(100).nullable(),
61513
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61082
61514
  /** 0..100 mowing-completion percentage of the current task, or null when no
61083
61515
  * task is active / progress is unavailable. */
61084
- progressPercent: number().min(0).max(100).nullable(),
61516
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61085
61517
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61086
61518
  * or null when unknown. */
61087
- currentCode: number().nullable(),
61519
+ currentCode: number().nullable().meta(readingMeta("none")),
61088
61520
  /** Human label for {@link currentCode}, or null when undecodable. */
61089
61521
  currentCodeLabel: string().nullable(),
61090
61522
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61617,7 +62049,7 @@ var MediaPlayerStatusSchema = object({
61617
62049
  state: MediaPlayerStateSchema,
61618
62050
  /** Volume as 0..100 inclusive. Null when the device has no volume
61619
62051
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61620
- volumeLevel: number().min(0).max(100).nullable(),
62052
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61621
62053
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61622
62054
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61623
62055
  isMuted: boolean().nullable(),
@@ -61630,10 +62062,10 @@ var MediaPlayerStatusSchema = object({
61630
62062
  currentMedia: MediaInfoSchema.nullable(),
61631
62063
  /** Current playback position in ms. Null when not seekable or
61632
62064
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61633
- positionMs: number().int().nonnegative().nullable(),
62065
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61634
62066
  /** Total duration of the current media in ms. Null when unknown
61635
62067
  * (live stream) or nothing is playing. */
61636
- durationMs: number().int().nonnegative().nullable(),
62068
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61637
62069
  /** Shuffle toggle. Null when no shuffle surface.
61638
62070
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61639
62071
  shuffle: boolean().nullable(),
@@ -61942,7 +62374,7 @@ var motionCustomize = {
61942
62374
  eventName: "onMotionChanged"
61943
62375
  },
61944
62376
  deviceTypes: [DeviceType.Camera],
61945
- apply: "replace",
62377
+ apply: "add",
61946
62378
  applyAuthority: {
61947
62379
  kind: "camera-sensors",
61948
62380
  timeoutField: "motionCooldownMs"
@@ -62014,7 +62446,7 @@ var MotionStatusSchema = object({
62014
62446
  * it, every provider seeded it at boot, and every session left it
62015
62447
  * `detected: true` after its detach.
62016
62448
  */
62017
- autoClearAfterMs: number().nullable()
62449
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
62018
62450
  });
62019
62451
  /**
62020
62452
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62051,12 +62483,18 @@ var motionCapability = {
62051
62483
  methods: {
62052
62484
  /**
62053
62485
  * Pull the current motion state synchronously. Convenience shortcut
62054
- * for consumers that don't need the full status object; equivalent
62055
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62056
- * `getStatus` once the auto-injected status surface lands in every
62057
- * 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.
62058
62488
  */
62059
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
+ } },
62060
62498
  events: {
62061
62499
  /**
62062
62500
  * Fires every time the runner transitions a camera between
@@ -62196,7 +62634,7 @@ var MotionZoneRegionSchema = object({
62196
62634
  * the grid region(s). */
62197
62635
  var MotionZoneStatusSchema = object({
62198
62636
  enabled: boolean(),
62199
- sensitivity: number(),
62637
+ sensitivity: number().meta(readingMeta("none")),
62200
62638
  /** Grid region(s). Today exactly one `grid` shape. */
62201
62639
  regions: array(MotionZoneRegionSchema),
62202
62640
  lastFetchedAt: number()
@@ -62566,7 +63004,7 @@ var NavigationStatusSchema = object({
62566
63004
  following: boolean(),
62567
63005
  flash: boolean(),
62568
63006
  lightMode: NavigationLightModeSchema,
62569
- lightLevel: number().min(40).max(100),
63007
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62570
63008
  /** Ms epoch when the slice was last updated. */
62571
63009
  lastChangedAt: number()
62572
63010
  });
@@ -62700,13 +63138,13 @@ var NetworkLinkStatusSchema = object({
62700
63138
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62701
63139
  * SKIP a null rather than coerce it.
62702
63140
  */
62703
- signalPercent: number().min(0).max(100).nullable(),
63141
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62704
63142
  /** Raw received signal strength in dBm, when the firmware reports one. */
62705
- rssiDbm: number().optional(),
63143
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62706
63144
  /** Network name of a wireless link, when the firmware reports it. */
62707
63145
  ssid: string().optional(),
62708
63146
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62709
- lastUpdated: number()
63147
+ lastUpdated: number().meta(readingMeta("none"))
62710
63148
  });
62711
63149
  var networkLinkCapability = {
62712
63150
  name: "network-link",
@@ -63091,7 +63529,7 @@ var NotifierStatusSchema = object({
63091
63529
  lastError: string().nullable(),
63092
63530
  /** Number of deliveries currently buffered server-side (rate-limited
63093
63531
  * notifiers may queue). 0 when the provider sends synchronously. */
63094
- queueDepth: number().int().nonnegative(),
63532
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63095
63533
  /** Per-feature capability matrix — authoritative source for the
63096
63534
  * compose-form field gating. */
63097
63535
  supports: NotifierSupportsSchema
@@ -63183,11 +63621,9 @@ onSent: { data: object({
63183
63621
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63184
63622
  * for: water flow, distance, weight, frequency, signal strength, …).
63185
63623
  *
63186
- * The `unit` and `precision` fields carry the live display metadata that
63187
- * the upstream source provides with each state push (HA
63188
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63189
- * The UI reads them directly from the slice — they are the single source
63190
- * 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).
63191
63627
  *
63192
63628
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63193
63629
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63196,15 +63632,8 @@ onSent: { data: object({
63196
63632
  * numeric values land here.
63197
63633
  */
63198
63634
  var NumericSensorStatusSchema = object({
63199
- value: number(),
63200
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63201
- * Populated live from the upstream source on each state push.
63202
- * Absent for unitless sensors. */
63203
- unit: string().optional(),
63204
- /** Suggested decimal places for numeric display.
63205
- * Populated live from the upstream source when provided.
63206
- * Falls back to auto-formatting when absent. */
63207
- 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")),
63208
63637
  /** Ms epoch when the slice was last updated. */
63209
63638
  lastFetchedAt: number()
63210
63639
  });
@@ -63228,11 +63657,11 @@ var numericSensorCapability = {
63228
63657
  */
63229
63658
  durability: "restored",
63230
63659
  /**
63231
- * 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.
63232
63662
  */
63233
63663
  history: { fields: { value: {
63234
63664
  kind: "gauge",
63235
- unitFrom: "unit",
63236
63665
  minIntervalMs: 3e4
63237
63666
  } } },
63238
63667
  /** Clock fields: written, but excluded from the compare that decides
@@ -63570,23 +63999,26 @@ var PetFeederStatusSchema = object({
63570
63999
  /** Food currently in the bowl (grams). Null when the device has not
63571
64000
  * reported a reading yet. On dual-hopper models this is the combined
63572
64001
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63573
- foodLevel: number().nullable(),
64002
+ foodLevel: number().nullable().meta(readingMeta("none")),
63574
64003
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63575
64004
  * single-hopper models. */
63576
- food1: number().nullable(),
64005
+ food1: number().nullable().meta(readingMeta("none")),
63577
64006
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63578
64007
  * single-hopper models. */
63579
- food2: number().nullable(),
64008
+ food2: number().nullable().meta(readingMeta("none")),
63580
64009
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63581
64010
  * (`device_class: problem`, on = low). True when the bowl is empty /
63582
64011
  * below the feeder's low threshold. */
63583
64012
  lowFood: boolean(),
63584
64013
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63585
64014
  * device has no battery reading. */
63586
- batteryPower: number().min(0).max(100).nullable(),
63587
- /** Days of desiccant life remaining. Null when the model has no
63588
- * desiccant sensor. */
63589
- 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")),
63590
64022
  /** True while a feed is in progress. */
63591
64023
  feeding: boolean(),
63592
64024
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63597,7 +64029,7 @@ var PetFeederStatusSchema = object({
63597
64029
  * with `errorCode` for consumers that want the raw integer. */
63598
64030
  error: string().nullable(),
63599
64031
  /** Raw device error code (0 / null = no error). */
63600
- errorCode: number().nullable(),
64032
+ errorCode: number().nullable().meta(readingMeta("none")),
63601
64033
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63602
64034
  isDualHopper: boolean(),
63603
64035
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63607,7 +64039,7 @@ var PetFeederStatusSchema = object({
63607
64039
  /** Play a chime when dispensing. */
63608
64040
  feedSound: boolean(),
63609
64041
  /** Speaker / prompt volume level (device-scaled integer). */
63610
- volume: number(),
64042
+ volume: number().meta(readingMeta("none")),
63611
64043
  /** Ms epoch when the slice was last refreshed from the cloud. */
63612
64044
  lastFetchedAt: number()
63613
64045
  });
@@ -63726,7 +64158,7 @@ var petFeederCapability = {
63726
64158
  },
63727
64159
  desiccantLeftDays: {
63728
64160
  kind: "gauge",
63729
- unit: "days"
64161
+ unit: "s"
63730
64162
  }
63731
64163
  } }
63732
64164
  };
@@ -64127,36 +64559,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64127
64559
  */
64128
64560
  var PowerMeterStatusSchema = object({
64129
64561
  /** Instantaneous power draw in watts. */
64130
- watts: number().optional().meta({
64131
- unit: "W",
64132
- label: "Power"
64133
- }),
64562
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64134
64563
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64135
- kwhTotal: number().optional().meta({
64136
- unit: "kWh",
64137
- label: "Energy"
64138
- }),
64564
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64139
64565
  /** Voltage in volts. */
64140
- volts: number().optional(),
64566
+ volts: number().optional().meta(readingMeta("voltage")),
64141
64567
  /** Current in amperes. */
64142
- amps: number().optional(),
64143
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64144
- va: number().optional(),
64568
+ amps: number().optional().meta(readingMeta("current")),
64569
+ /** Apparent power in volt-amperes. */
64570
+ va: number().optional().meta(readingMeta("apparent-power")),
64145
64571
  /** Power factor (0..1) if reported. */
64146
- powerFactor: number().min(0).max(1).optional(),
64572
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64147
64573
  /** Ms epoch when the slice was last updated. */
64148
- lastFetchedAt: number(),
64149
- /** Live display unit of the single metric this slice carries (e.g. HA
64150
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64151
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64152
- * unit per slice is unambiguous. The UI prefers this over the role's
64153
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64154
- unit: string().optional(),
64155
- /** Suggested decimal places for numeric display.
64156
- * Populated live from the upstream source when provided (e.g. HA
64157
- * `attributes.suggested_display_precision`). Falls back to
64158
- * auto-formatting when absent. */
64159
- precision: number().int().min(0).max(10).optional()
64574
+ lastFetchedAt: number()
64160
64575
  });
64161
64576
  var powerMeterCapability = {
64162
64577
  name: "power-meter",
@@ -64220,12 +64635,7 @@ var powerMeterCapability = {
64220
64635
  } },
64221
64636
  /** Clock fields: written, but excluded from the compare that decides
64222
64637
  * whether persisting is worth a SQLite commit. */
64223
- volatileStateFields: ["lastFetchedAt"],
64224
- customize: {
64225
- version: 1,
64226
- advanced: "derive",
64227
- targets: []
64228
- }
64638
+ volatileStateFields: ["lastFetchedAt"]
64229
64639
  };
64230
64640
  /**
64231
64641
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64245,11 +64655,11 @@ var powerMeterCapability = {
64245
64655
  */
64246
64656
  var GpsLocationSchema = object({
64247
64657
  /** Latitude in decimal degrees, -90..90. */
64248
- latitude: number().min(-90).max(90),
64658
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64249
64659
  /** Longitude in decimal degrees, -180..180. */
64250
- longitude: number().min(-180).max(180),
64660
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64251
64661
  /** Reported accuracy in meters (lower = better). */
64252
- accuracyMeters: number().nonnegative()
64662
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64253
64663
  });
64254
64664
  var PresenceStatusSchema = object({
64255
64665
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64261,7 +64671,7 @@ var PresenceStatusSchema = object({
64261
64671
  gps: GpsLocationSchema.nullable(),
64262
64672
  /** Optional battery level of the tracking device (0..100). Null
64263
64673
  * when not reported. */
64264
- batteryPercent: number().min(0).max(100).nullable(),
64674
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64265
64675
  /** Ms epoch when the slice was last updated. */
64266
64676
  lastChangedAt: number()
64267
64677
  });
@@ -64311,19 +64721,10 @@ var presenceCapability = {
64311
64721
  * `sourceInfo`.
64312
64722
  */
64313
64723
  var PressureSensorStatusSchema = object({
64314
- /** Current pressure in hPa. */
64315
- hpa: number(),
64724
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64725
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64316
64726
  /** Ms epoch when the slice was last updated. */
64317
- lastFetchedAt: number(),
64318
- /** Live display unit from the upstream source (e.g. HA
64319
- * `attributes.unit_of_measurement`). The UI prefers this over the
64320
- * role's canonical unit. Absent → fall back to the canonical unit. */
64321
- unit: string().optional(),
64322
- /** Suggested decimal places for numeric display.
64323
- * Populated live from the upstream source when provided (e.g. HA
64324
- * `attributes.suggested_display_precision`). Falls back to
64325
- * auto-formatting when absent. */
64326
- precision: number().int().min(0).max(10).optional()
64727
+ lastFetchedAt: number()
64327
64728
  });
64328
64729
  var pressureSensorCapability = {
64329
64730
  name: "pressure-sensor",
@@ -64849,8 +65250,8 @@ var ptzCapability = {
64849
65250
  };
64850
65251
  var PtzAutotrackSettingsSchema = object({
64851
65252
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64852
- stopDelaySeconds: number().int().min(0).max(300),
64853
- 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"))
64854
65255
  });
64855
65256
  /**
64856
65257
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -65004,7 +65405,7 @@ var RunningStateStatusSchema = object({
65004
65405
  /** null = unknown — never coerced to false (D393). */
65005
65406
  running: boolean().nullable(),
65006
65407
  /** Epoch ms the current boolean began; null while `running` is null. */
65007
- since: number().nullable(),
65408
+ since: number().nullable().meta(readingMeta("none")),
65008
65409
  source: RunningStateSourceSchema.meta({ label: "Source" }),
65009
65410
  phase: string().nullable(),
65010
65411
  /** null until a reading has been published (D393). */
@@ -65019,7 +65420,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
65019
65420
  /** Generic Customize target for the running boolean (D936). */
65020
65421
  var runningCustomize = {
65021
65422
  version: 1,
65022
- advanced: "derive",
65023
65423
  targets: [{
65024
65424
  id: "running",
65025
65425
  cardinality: "scalar",
@@ -65039,7 +65439,7 @@ var runningCustomize = {
65039
65439
  DeviceType.Thermostat,
65040
65440
  DeviceType.PetFeeder
65041
65441
  ],
65042
- apply: "add-or-replace",
65442
+ apply: "add",
65043
65443
  path: "running",
65044
65444
  sources: [
65045
65445
  {
@@ -65055,6 +65455,7 @@ var runningCustomize = {
65055
65455
  requiredUnit: "W",
65056
65456
  transforms: [{
65057
65457
  kind: "threshold",
65458
+ comparisons: ["gte", "lte"],
65058
65459
  comparison: "gte",
65059
65460
  defaultThreshold: 5,
65060
65461
  defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
@@ -66306,9 +66707,9 @@ var RecordTriggerSchema = _enum([
66306
66707
  */
66307
66708
  var RecordWindowSchema = object({
66308
66709
  trigger: RecordTriggerSchema,
66309
- day: number().int().min(0).max(6),
66310
- startMinute: number().int().min(0).max(1439),
66311
- 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"))
66312
66713
  });
66313
66714
  /** Status of one physical volume, as the camera itself describes it. */
66314
66715
  var OnboardStorageVolumeSchema = object({
@@ -66384,10 +66785,10 @@ var RecordingOnboardStatusSchema = object({
66384
66785
  /** Master "record to the card at all" switch. */
66385
66786
  enabled: boolean().nullable(),
66386
66787
  overwriteWhenFull: boolean().nullable(),
66387
- preRecordSec: number().nullable(),
66388
- postRecordSec: number().nullable(),
66788
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66789
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66389
66790
  /** Length of one recorded file, in minutes. */
66390
- segmentMinutes: number().nullable(),
66791
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66391
66792
  /** The primary track's weekly windows, flattened. */
66392
66793
  windows: array(RecordWindowSchema),
66393
66794
  /**
@@ -66403,7 +66804,7 @@ var RecordingOnboardStatusSchema = object({
66403
66804
  * provider reporting a non-zero count also reports the schedule as not
66404
66805
  * writable.
66405
66806
  */
66406
- unreadableWindows: number(),
66807
+ unreadableWindows: number().meta(readingMeta("count")),
66407
66808
  /**
66408
66809
  * The camera is scheduled to record and has NO usable storage.
66409
66810
  *
@@ -67350,7 +67751,7 @@ var CamStreamDescriptorSchema = object({
67350
67751
  url: string().optional(),
67351
67752
  codec: string().optional(),
67352
67753
  resolution: CamStreamResolutionSchema.optional(),
67353
- fps: number().positive().optional(),
67754
+ fps: number().positive().optional().meta(readingMeta("rate")),
67354
67755
  label: string().optional(),
67355
67756
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
67356
67757
  deviceFeatures: array(string()).optional(),
@@ -67437,18 +67838,18 @@ var StreamProfileSchema = _enum([
67437
67838
  ]);
67438
67839
  /** The current encoding config of one stream profile. */
67439
67840
  var StreamProfileConfigSchema = object({
67440
- width: number(),
67441
- height: number(),
67841
+ width: number().meta(readingMeta("none")),
67842
+ height: number().meta(readingMeta("none")),
67442
67843
  codec: _enum(["h264", "h265"]),
67443
- framerate: number(),
67444
- bitrate: number(),
67844
+ framerate: number().meta(readingMeta("rate")),
67845
+ bitrate: number().meta(readingMeta("bitrate")),
67445
67846
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
67446
67847
  encoderProfile: _enum([
67447
67848
  "high",
67448
67849
  "main",
67449
67850
  "baseline"
67450
67851
  ]).optional(),
67451
- gop: number().optional(),
67852
+ gop: number().optional().meta(readingMeta("none")),
67452
67853
  /**
67453
67854
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
67454
67855
  *
@@ -67867,12 +68268,7 @@ var switchCapability = {
67867
68268
  label: "On",
67868
68269
  kind: "boolean"
67869
68270
  }
67870
- }] },
67871
- customize: {
67872
- version: 1,
67873
- advanced: "derive",
67874
- targets: []
67875
- }
68271
+ }] }
67876
68272
  };
67877
68273
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67878
68274
  function hasControlCharacter(value) {
@@ -68455,35 +68851,21 @@ var tamperCapability = {
68455
68851
  * entries with `device_class: temperature` and any future native
68456
68852
  * thermometer.
68457
68853
  *
68458
- * Unit normalisation: providers convert to Celsius before storing.
68459
- * The slice value is always Celsius so cross-cap aggregators
68460
- * (climate-control's `currentTemp`, energy analytics) can compose
68461
- * without per-source unit fixups. The canonical display unit (`°C`) is
68462
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68463
- * `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.
68464
68856
  *
68465
68857
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
68466
68858
  * frozen feed (provider hung) distinct from a "temperature hasn't
68467
68859
  * changed" steady state.
68468
68860
  */
68469
68861
  var TemperatureSensorStatusSchema = object({
68470
- /** Current temperature in Celsius. */
68471
- celsius: number().meta({
68472
- unit: "°C",
68862
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68863
+ celsius: number().nullable().meta(readingMeta("temperature", {
68864
+ precision: 1,
68473
68865
  label: "Temperature"
68474
- }),
68866
+ })),
68475
68867
  /** Ms epoch when the slice was last updated (push or poll). */
68476
- lastFetchedAt: number(),
68477
- /** Live display unit from the upstream source (e.g. HA
68478
- * `attributes.unit_of_measurement`). The UI prefers this over the
68479
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
68480
- * Absent → the UI falls back to the role's canonical unit. */
68481
- unit: string().optional(),
68482
- /** Suggested decimal places for numeric display.
68483
- * Populated live from the upstream source when provided (e.g. HA
68484
- * `attributes.suggested_display_precision`). Falls back to
68485
- * auto-formatting when absent. */
68486
- precision: number().int().min(0).max(10).optional()
68868
+ lastFetchedAt: number()
68487
68869
  });
68488
68870
  var temperatureSensorCapability = {
68489
68871
  name: "temperature-sensor",
@@ -68514,12 +68896,7 @@ var temperatureSensorCapability = {
68514
68896
  } } },
68515
68897
  /** Clock fields: written, but excluded from the compare that decides
68516
68898
  * whether persisting is worth a SQLite commit. */
68517
- volatileStateFields: ["lastFetchedAt"],
68518
- customize: {
68519
- version: 1,
68520
- advanced: "derive",
68521
- targets: []
68522
- }
68899
+ volatileStateFields: ["lastFetchedAt"]
68523
68900
  };
68524
68901
  /**
68525
68902
  * toast — system-scoped singleton capability that streams toast
@@ -68802,16 +69179,16 @@ var VacuumControlStatusSchema = object({
68802
69179
  ]),
68803
69180
  /** 0..100 battery percentage. Null when the device has no battery
68804
69181
  * reading. */
68805
- batteryLevel: number().min(0).max(100).nullable(),
69182
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68806
69183
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68807
69184
  * the vacuum has no speed control. */
68808
69185
  fanSpeed: string().nullable(),
68809
69186
  /** Speed tokens the hardware accepts — drives the UI selector. */
68810
69187
  availableFanSpeeds: array(string()),
68811
69188
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68812
- progressPercent: number().min(0).max(100).nullable(),
69189
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68813
69190
  /** Current error code (0 / null = no error). */
68814
- errorCode: number().nullable(),
69191
+ errorCode: number().nullable().meta(readingMeta("none")),
68815
69192
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68816
69193
  errorLabel: string().nullable(),
68817
69194
  /** Ms epoch when the slice was last updated. */
@@ -68889,7 +69266,7 @@ var ValveStatusSchema = object({
68889
69266
  ]),
68890
69267
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68891
69268
  * intermediate position surface. */
68892
- position: number().min(0).max(100).nullable(),
69269
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68893
69270
  /** Ms epoch when the slice was last updated. */
68894
69271
  lastChangedAt: number()
68895
69272
  });
@@ -68997,9 +69374,9 @@ var vibrationCapability = {
68997
69374
  */
68998
69375
  var WaterHeaterStatusSchema = object({
68999
69376
  /** Current measured temperature. Null when not reported. */
69000
- currentTemp: number().nullable(),
69377
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
69001
69378
  /** Target temperature setpoint. Null when no setpoint surface. */
69002
- targetTemp: number().nullable(),
69379
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
69003
69380
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
69004
69381
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
69005
69382
  * device reports an unknown state. */
@@ -69010,9 +69387,9 @@ var WaterHeaterStatusSchema = object({
69010
69387
  * has no away surface. */
69011
69388
  away: boolean().nullable(),
69012
69389
  /** HA `min_temp` attribute. Null when not reported. */
69013
- minTemp: number().nullable(),
69390
+ minTemp: number().nullable().meta(readingMeta("temperature")),
69014
69391
  /** HA `max_temp` attribute. Null when not reported. */
69015
- maxTemp: number().nullable(),
69392
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
69016
69393
  /** Ms epoch when the slice was last updated. */
69017
69394
  lastChangedAt: number()
69018
69395
  });
@@ -69097,25 +69474,18 @@ var waterHeaterCapability = {
69097
69474
  * Forecast deferred — current conditions only for v1.
69098
69475
  */
69099
69476
  var WeatherStatusSchema = object({
69100
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69101
- * when no condition has been reported yet. */
69477
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69102
69478
  condition: string().nullable(),
69103
- /** Current temperature in the reported unit. Null when not provided. */
69104
- temperature: number().nullable(),
69105
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69106
- temperatureUnit: string().nullable(),
69479
+ /** Current temperature in Celsius. Null when not provided or refused. */
69480
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69107
69481
  /** Relative humidity (0..100). Null when not provided. */
69108
- humidity: number().min(0).max(100).nullable(),
69109
- /** Barometric pressure in the reported unit. Null when not provided. */
69110
- pressure: number().nullable(),
69111
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69112
- pressureUnit: string().nullable(),
69113
- /** Wind speed in the reported unit. Null when not provided. */
69114
- windSpeed: number().nullable(),
69115
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69116
- 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")),
69117
69487
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69118
- windBearing: number().nullable(),
69488
+ windBearing: number().nullable().meta(readingMeta("angle")),
69119
69489
  /** Ms epoch when the slice was last updated. */
69120
69490
  lastFetchedAt: number()
69121
69491
  });
@@ -69143,12 +69513,12 @@ var weatherCapability = {
69143
69513
  */
69144
69514
  durability: "session",
69145
69515
  /**
69146
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69516
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69147
69517
  */
69148
69518
  history: { fields: {
69149
69519
  temperature: {
69150
69520
  kind: "gauge",
69151
- unitFrom: "temperatureUnit",
69521
+ unit: "°C",
69152
69522
  deadband: .2
69153
69523
  },
69154
69524
  humidity: {
@@ -69158,12 +69528,12 @@ var weatherCapability = {
69158
69528
  },
69159
69529
  pressure: {
69160
69530
  kind: "gauge",
69161
- unitFrom: "pressureUnit",
69531
+ unit: "hPa",
69162
69532
  deadband: .5
69163
69533
  },
69164
69534
  windSpeed: {
69165
69535
  kind: "gauge",
69166
- unitFrom: "windSpeedUnit",
69536
+ unit: "m/s",
69167
69537
  deadband: .5
69168
69538
  }
69169
69539
  } }
@@ -70527,6 +70897,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
70527
70897
  featureProbe: featureProbeCapability,
70528
70898
  flood: floodCapability,
70529
70899
  gas: gasCapability,
70900
+ groupCommands: groupCommandsCapability,
70901
+ groupStatus: groupStatusCapability,
70530
70902
  heatingValve: heatingValveCapability,
70531
70903
  humidifier: humidifierCapability,
70532
70904
  humiditySensor: humiditySensorCapability,
@@ -71907,6 +72279,13 @@ Object.freeze({
71907
72279
  access: "view",
71908
72280
  role: "admin"
71909
72281
  },
72282
+ "addons.getPackageDocs": {
72283
+ capName: "addons",
72284
+ capScope: "system",
72285
+ addonId: null,
72286
+ access: "view",
72287
+ role: "admin"
72288
+ },
71910
72289
  "addons.getVersions": {
71911
72290
  capName: "addons",
71912
72291
  capScope: "system",
@@ -75015,6 +75394,20 @@ Object.freeze({
75015
75394
  access: "view",
75016
75395
  role: "viewer"
75017
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
+ },
75018
75411
  "heatingValve.getStatus": {
75019
75412
  capName: "heating-valve",
75020
75413
  capScope: "device",
@@ -84837,6 +85230,37 @@ var SENSOR_CAP_VOCABULARY = {
84837
85230
  icon: "flame"
84838
85231
  }]
84839
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
+ },
84840
85264
  "humidity-sensor": {
84841
85265
  scope: "device",
84842
85266
  noun: t("Humidity", "Umidità"),
@@ -84870,7 +85294,6 @@ var SENSOR_CAP_VOCABULARY = {
84870
85294
  field: "value",
84871
85295
  quantity: t("the reading", "il valore"),
84872
85296
  label: t("Reading", "Valore"),
84873
- unitField: "unit",
84874
85297
  icon: "gauge"
84875
85298
  }]
84876
85299
  },
@@ -85006,7 +85429,7 @@ var SENSOR_CAP_VOCABULARY = {
85006
85429
  field: "temperature",
85007
85430
  quantity: t("the outdoor temperature", "la temperatura esterna"),
85008
85431
  label: t("Temperature", "Temperatura"),
85009
- unitField: "temperatureUnit",
85432
+ unit: "°C",
85010
85433
  icon: "thermometer"
85011
85434
  },
85012
85435
  {
@@ -85022,7 +85445,7 @@ var SENSOR_CAP_VOCABULARY = {
85022
85445
  field: "windSpeed",
85023
85446
  quantity: t("the wind speed", "la velocità del vento"),
85024
85447
  label: t("Wind", "Vento"),
85025
- unitField: "windSpeedUnit",
85448
+ unit: "m/s",
85026
85449
  icon: "wind"
85027
85450
  },
85028
85451
  {
@@ -85030,7 +85453,7 @@ var SENSOR_CAP_VOCABULARY = {
85030
85453
  field: "pressure",
85031
85454
  quantity: t("the air pressure", "la pressione atmosferica"),
85032
85455
  label: t("Pressure", "Pressione"),
85033
- unitField: "pressureUnit",
85456
+ unit: "hPa",
85034
85457
  icon: "gauge"
85035
85458
  }
85036
85459
  ]
@@ -86509,6 +86932,16 @@ Object.freeze({
86509
86932
  form: "single",
86510
86933
  optional: false
86511
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
+ }],
86512
86945
  "heatingValve.getStatus": [{
86513
86946
  name: "deviceId",
86514
86947
  form: "single",
@@ -88373,6 +88806,11 @@ var PulseOverrideDraftSchema = object({
88373
88806
  targetId: string().min(1),
88374
88807
  enabled: boolean(),
88375
88808
  debounceMs: number().nonnegative(),
88809
+ edges: array(_enum([
88810
+ "rising",
88811
+ "button",
88812
+ "native-event"
88813
+ ])).min(1).optional(),
88376
88814
  sources: array(OverrideSourceSchema).min(1)
88377
88815
  });
88378
88816
  var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
@@ -88488,6 +88926,8 @@ new Map(Object.entries({
88488
88926
  "detection-blind": "device-events",
88489
88927
  "motion-noisy": "device-events",
88490
88928
  "motion-calm": "device-events",
88929
+ "package-llm-silent": "device-events",
88930
+ "package-llm-answered": "device-events",
88491
88931
  "recording-stalled": "device-events",
88492
88932
  "recording-healthy": "device-events",
88493
88933
  "recording-degraded": "device-events",
@@ -88571,6 +89011,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
88571
89011
  label: "Motion without a subject",
88572
89012
  kinds: ["motion-noisy", "motion-calm"]
88573
89013
  },
89014
+ {
89015
+ id: "package-llm",
89016
+ family: "device-events",
89017
+ label: "Package confirm",
89018
+ kinds: ["package-llm-silent", "package-llm-answered"]
89019
+ },
88574
89020
  {
88575
89021
  id: "recording",
88576
89022
  family: "device-events",
@@ -88752,6 +89198,8 @@ var DEVICE_EVENTS = [
88752
89198
  "detection-blind",
88753
89199
  "motion-noisy",
88754
89200
  "motion-calm",
89201
+ "package-llm-silent",
89202
+ "package-llm-answered",
88755
89203
  "recording-stalled",
88756
89204
  "recording-healthy",
88757
89205
  "recording-degraded",
@@ -89020,6 +89468,15 @@ new Map([
89020
89468
  snapshotLabel: "Cameras with constant motion now",
89021
89469
  snapshotSection: null
89022
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
+ },
89023
89480
  {
89024
89481
  id: "recording-health",
89025
89482
  family: "device-events",