@camstack/addon-provider-amcrest 0.2.160 → 0.2.161

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.mjs CHANGED
@@ -14,7 +14,7 @@ import { networkInterfaces } from "node:os";
14
14
  var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod), cb = null), mod.exports);
15
15
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
16
16
  //#endregion
17
- //#region ../types/dist/event-category-DHXxB8t9.mjs
17
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
18
18
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
19
19
  EventCategory["SystemBoot"] = "system.boot";
20
20
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -340,6 +340,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
340
340
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
341
341
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
342
342
  /**
343
+ * A camera's package LLM confirm has failed closed three times in a row
344
+ * (D931, 2026-10-26). One edge per camera until
345
+ * {@link PipelinePackageLlmAnswered}.
346
+ */
347
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
348
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
349
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
350
+ /**
343
351
  * Per-camera pipeline config was mutated by the orchestrator
344
352
  * (a settings change via `setAgentInferenceDevices` /
345
353
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5449,7 +5457,7 @@ var ZodIssueCode = {
5449
5457
  var ZodFirstPartyTypeKind;
5450
5458
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5451
5459
  //#endregion
5452
- //#region ../types/dist/sleep-BErAxhul.mjs
5460
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5453
5461
  /**
5454
5462
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5455
5463
  * window to float samples (D455).
@@ -8003,6 +8011,99 @@ var RecordKindDeclarationSchema = object({
8003
8011
  }).strict();
8004
8012
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8005
8013
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8014
+ var READING_QUANTITIES = [
8015
+ "temperature",
8016
+ "pressure",
8017
+ "speed",
8018
+ "precipitation",
8019
+ "precipitation-rate",
8020
+ "length",
8021
+ "area",
8022
+ "illuminance",
8023
+ "irradiance",
8024
+ "power",
8025
+ "energy",
8026
+ "voltage",
8027
+ "current",
8028
+ "apparent-power",
8029
+ "humidity",
8030
+ "percentage",
8031
+ "concentration-volume",
8032
+ "concentration-mass",
8033
+ "concentration-ppb",
8034
+ "volume",
8035
+ "duration",
8036
+ "duration-ms",
8037
+ "information",
8038
+ "sound-level",
8039
+ "signal-level",
8040
+ "colour-temperature",
8041
+ "angle",
8042
+ "bitrate",
8043
+ "rate",
8044
+ "mass",
8045
+ "index",
8046
+ "ratio",
8047
+ "count",
8048
+ "none",
8049
+ "device"
8050
+ ];
8051
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8052
+ new Set(READING_QUANTITIES);
8053
+ /**
8054
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8055
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8056
+ */
8057
+ var QUANTITY_BASE = {
8058
+ temperature: "°C",
8059
+ pressure: "hPa",
8060
+ speed: "m/s",
8061
+ precipitation: "mm",
8062
+ "precipitation-rate": "mm/h",
8063
+ length: "m",
8064
+ area: "m²",
8065
+ illuminance: "lx",
8066
+ irradiance: "W/m²",
8067
+ power: "W",
8068
+ energy: "kWh",
8069
+ voltage: "V",
8070
+ current: "A",
8071
+ "apparent-power": "VA",
8072
+ humidity: "%",
8073
+ percentage: "%",
8074
+ "concentration-volume": "ppm",
8075
+ "concentration-mass": "µg/m³",
8076
+ "concentration-ppb": "ppb",
8077
+ volume: "L",
8078
+ duration: "s",
8079
+ "duration-ms": "ms",
8080
+ information: "B",
8081
+ "sound-level": "dBFS",
8082
+ "signal-level": "dBm",
8083
+ "colour-temperature": "mired",
8084
+ angle: "°",
8085
+ bitrate: "kbps",
8086
+ rate: "fps",
8087
+ mass: "kg",
8088
+ index: "none",
8089
+ ratio: "none",
8090
+ count: "none",
8091
+ none: "none",
8092
+ device: "device"
8093
+ };
8094
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8095
+ function baseUnitOf(quantity) {
8096
+ const symbol = QUANTITY_BASE[quantity];
8097
+ if (symbol === "none" || symbol === "device") return void 0;
8098
+ return symbol;
8099
+ }
8100
+ function readingMeta(quantity, extras) {
8101
+ return {
8102
+ quantity,
8103
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8104
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8105
+ };
8106
+ }
8006
8107
  /** Shared Zod schemas used across streaming capabilities. */
8007
8108
  var CamProfileSchema = _enum([
8008
8109
  "high",
@@ -8030,8 +8131,8 @@ var CamStreamKindSchema = _enum([
8030
8131
  "derived"
8031
8132
  ]);
8032
8133
  var CamStreamResolutionSchema = object({
8033
- width: number().int().positive(),
8034
- height: number().int().positive()
8134
+ width: number().int().positive().meta(readingMeta("none")),
8135
+ height: number().int().positive().meta(readingMeta("none"))
8035
8136
  });
8036
8137
  var CameraStreamSchema = object({
8037
8138
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8549,6 +8650,8 @@ var CAP_NAMES = Object.freeze([
8549
8650
  "filesystem-browse",
8550
8651
  "flood",
8551
8652
  "gas",
8653
+ "group-commands",
8654
+ "group-status",
8552
8655
  "heating-valve",
8553
8656
  "humidifier",
8554
8657
  "humidity-sensor",
@@ -8678,14 +8781,6 @@ Object.freeze({
8678
8781
  {
8679
8782
  "path": "aqi",
8680
8783
  "valueType": "number"
8681
- },
8682
- {
8683
- "path": "unit",
8684
- "valueType": "string"
8685
- },
8686
- {
8687
- "path": "precision",
8688
- "valueType": "number"
8689
8784
  }
8690
8785
  ],
8691
8786
  "alarm-panel": [
@@ -8721,20 +8816,10 @@ Object.freeze({
8721
8816
  "valueType": "boolean"
8722
8817
  }
8723
8818
  ],
8724
- "ambient-light-sensor": [
8725
- {
8726
- "path": "lux",
8727
- "valueType": "number"
8728
- },
8729
- {
8730
- "path": "unit",
8731
- "valueType": "string"
8732
- },
8733
- {
8734
- "path": "precision",
8735
- "valueType": "number"
8736
- }
8737
- ],
8819
+ "ambient-light-sensor": [{
8820
+ "path": "lux",
8821
+ "valueType": "number"
8822
+ }],
8738
8823
  "audio-metrics": [
8739
8824
  {
8740
8825
  "path": "ts",
@@ -8814,6 +8899,10 @@ Object.freeze({
8814
8899
  "path": "lastUpdated",
8815
8900
  "valueType": "number"
8816
8901
  },
8902
+ {
8903
+ "path": "volts",
8904
+ "valueType": "number"
8905
+ },
8817
8906
  {
8818
8907
  "path": "lastContactAt",
8819
8908
  "valueType": "number"
@@ -9056,10 +9145,6 @@ Object.freeze({
9056
9145
  "datetime"
9057
9146
  ]
9058
9147
  },
9059
- {
9060
- "path": "unit",
9061
- "valueType": "string"
9062
- },
9063
9148
  {
9064
9149
  "path": "min",
9065
9150
  "valueType": "number"
@@ -9071,19 +9156,6 @@ Object.freeze({
9071
9156
  {
9072
9157
  "path": "step",
9073
9158
  "valueType": "number"
9074
- },
9075
- {
9076
- "path": "precision",
9077
- "valueType": "number"
9078
- },
9079
- {
9080
- "path": "format",
9081
- "valueType": "enum",
9082
- "enumValues": [
9083
- "date",
9084
- "time",
9085
- "datetime"
9086
- ]
9087
9159
  }
9088
9160
  ],
9089
9161
  "cover": [
@@ -9324,13 +9396,8 @@ Object.freeze({
9324
9396
  "path": "value",
9325
9397
  "valueType": "string"
9326
9398
  }, {
9327
- "path": "format",
9328
- "valueType": "enum",
9329
- "enumValues": [
9330
- "date",
9331
- "time",
9332
- "datetime"
9333
- ]
9399
+ "path": "timestamp",
9400
+ "valueType": "boolean"
9334
9401
  }],
9335
9402
  "event-emitter": [
9336
9403
  {
@@ -9403,6 +9470,20 @@ Object.freeze({
9403
9470
  "path": "detected",
9404
9471
  "valueType": "boolean"
9405
9472
  }],
9473
+ "group-status": [
9474
+ {
9475
+ "path": "included",
9476
+ "valueType": "number"
9477
+ },
9478
+ {
9479
+ "path": "excluded",
9480
+ "valueType": "number"
9481
+ },
9482
+ {
9483
+ "path": "total",
9484
+ "valueType": "number"
9485
+ }
9486
+ ],
9406
9487
  "heating-valve": [{
9407
9488
  "path": "position",
9408
9489
  "valueType": "number"
@@ -9437,20 +9518,10 @@ Object.freeze({
9437
9518
  "valueType": "number"
9438
9519
  }
9439
9520
  ],
9440
- "humidity-sensor": [
9441
- {
9442
- "path": "percent",
9443
- "valueType": "number"
9444
- },
9445
- {
9446
- "path": "unit",
9447
- "valueType": "string"
9448
- },
9449
- {
9450
- "path": "precision",
9451
- "valueType": "number"
9452
- }
9453
- ],
9521
+ "humidity-sensor": [{
9522
+ "path": "percent",
9523
+ "valueType": "number"
9524
+ }],
9454
9525
  "image": [{
9455
9526
  "path": "url",
9456
9527
  "valueType": "string"
@@ -9789,20 +9860,10 @@ Object.freeze({
9789
9860
  "valueType": "boolean"
9790
9861
  }
9791
9862
  ],
9792
- "numeric-sensor": [
9793
- {
9794
- "path": "value",
9795
- "valueType": "number"
9796
- },
9797
- {
9798
- "path": "unit",
9799
- "valueType": "string"
9800
- },
9801
- {
9802
- "path": "precision",
9803
- "valueType": "number"
9804
- }
9805
- ],
9863
+ "numeric-sensor": [{
9864
+ "path": "value",
9865
+ "valueType": "number"
9866
+ }],
9806
9867
  "pet-feeder": [
9807
9868
  {
9808
9869
  "path": "foodLevel",
@@ -9894,14 +9955,6 @@ Object.freeze({
9894
9955
  {
9895
9956
  "path": "powerFactor",
9896
9957
  "valueType": "number"
9897
- },
9898
- {
9899
- "path": "unit",
9900
- "valueType": "string"
9901
- },
9902
- {
9903
- "path": "precision",
9904
- "valueType": "number"
9905
9958
  }
9906
9959
  ],
9907
9960
  "presence": [
@@ -9930,20 +9983,10 @@ Object.freeze({
9930
9983
  "valueType": "number"
9931
9984
  }
9932
9985
  ],
9933
- "pressure-sensor": [
9934
- {
9935
- "path": "hpa",
9936
- "valueType": "number"
9937
- },
9938
- {
9939
- "path": "unit",
9940
- "valueType": "string"
9941
- },
9942
- {
9943
- "path": "precision",
9944
- "valueType": "number"
9945
- }
9946
- ],
9986
+ "pressure-sensor": [{
9987
+ "path": "hpa",
9988
+ "valueType": "number"
9989
+ }],
9947
9990
  "privacy-mask": [{
9948
9991
  "path": "enabled",
9949
9992
  "valueType": "boolean"
@@ -10254,20 +10297,10 @@ Object.freeze({
10254
10297
  "path": "tampered",
10255
10298
  "valueType": "boolean"
10256
10299
  }],
10257
- "temperature-sensor": [
10258
- {
10259
- "path": "celsius",
10260
- "valueType": "number"
10261
- },
10262
- {
10263
- "path": "unit",
10264
- "valueType": "string"
10265
- },
10266
- {
10267
- "path": "precision",
10268
- "valueType": "number"
10269
- }
10270
- ],
10300
+ "temperature-sensor": [{
10301
+ "path": "celsius",
10302
+ "valueType": "number"
10303
+ }],
10271
10304
  "update": [
10272
10305
  {
10273
10306
  "path": "currentVersion",
@@ -10378,10 +10411,6 @@ Object.freeze({
10378
10411
  "path": "temperature",
10379
10412
  "valueType": "number"
10380
10413
  },
10381
- {
10382
- "path": "temperatureUnit",
10383
- "valueType": "string"
10384
- },
10385
10414
  {
10386
10415
  "path": "humidity",
10387
10416
  "valueType": "number"
@@ -10390,18 +10419,10 @@ Object.freeze({
10390
10419
  "path": "pressure",
10391
10420
  "valueType": "number"
10392
10421
  },
10393
- {
10394
- "path": "pressureUnit",
10395
- "valueType": "string"
10396
- },
10397
10422
  {
10398
10423
  "path": "windSpeed",
10399
10424
  "valueType": "number"
10400
10425
  },
10401
- {
10402
- "path": "windSpeedUnit",
10403
- "valueType": "string"
10404
- },
10405
10426
  {
10406
10427
  "path": "windBearing",
10407
10428
  "valueType": "number"
@@ -10808,7 +10829,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
10808
10829
  return DeviceRole;
10809
10830
  }({});
10810
10831
  //#endregion
10811
- //#region ../types/dist/composition-kEl9mevu.mjs
10832
+ //#region ../types/dist/composition-OhpuespA.mjs
10812
10833
  /**
10813
10834
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10814
10835
  *
@@ -11563,15 +11584,74 @@ function validateExpressionSource(src) {
11563
11584
  }
11564
11585
  return null;
11565
11586
  }
11566
- /** A composed device's stableId is this prefix plus the block id. */
11567
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11568
- /** Longest snippet a `code` source may carry (slice 4). */
11569
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11587
+ /**
11588
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11589
+ * id. Split out so helper schemas can name it without a cycle through
11590
+ * `composition.ts` (D659).
11591
+ */
11570
11592
  var CompositionSourceRefSchema = object({
11571
11593
  /** The owning addon; `stableId` is unique only within it. */
11572
11594
  addonId: string().min(1),
11573
11595
  stableId: string().min(1)
11574
11596
  });
11597
+ var GROUP_TEMPLATE_IDS = [
11598
+ "temperature",
11599
+ "humidity",
11600
+ "pressure",
11601
+ "illuminance",
11602
+ "power",
11603
+ "energy",
11604
+ "battery",
11605
+ "numeric",
11606
+ "motion",
11607
+ "contact",
11608
+ "leak",
11609
+ "smoke",
11610
+ "occupancy",
11611
+ "binary",
11612
+ "switch",
11613
+ "light"
11614
+ ];
11615
+ var NUMERIC_AGGREGATES = [
11616
+ "mean",
11617
+ "median",
11618
+ "min",
11619
+ "max",
11620
+ "sum"
11621
+ ];
11622
+ var BINARY_AGGREGATES = ["any", "all"];
11623
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11624
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11625
+ var AGGREGATE_REDUCERS = [
11626
+ "mean",
11627
+ "median",
11628
+ "min",
11629
+ "max",
11630
+ "sum",
11631
+ "any",
11632
+ "all"
11633
+ ];
11634
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11635
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11636
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11637
+ /** One member. The field is stored so occupancy can name motion or binary. */
11638
+ var HelperMemberSchema = object({
11639
+ source: CompositionSourceRefSchema,
11640
+ cap: string().min(1),
11641
+ fieldPath: string().min(1)
11642
+ });
11643
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11644
+ kind: literal("group"),
11645
+ templateId: GroupTemplateIdSchema,
11646
+ aggregate: AggregateReducerSchema,
11647
+ members: array(HelperMemberSchema).min(1).max(64),
11648
+ /** Actuator groups only. Readings leave this unset. */
11649
+ active: ActiveCriterionSchema.optional()
11650
+ })]);
11651
+ /** A composed device's stableId is this prefix plus the block id. */
11652
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11653
+ /** Longest snippet a `code` source may carry (slice 4). */
11654
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11575
11655
  /** One field of one capability of one source device. */
11576
11656
  var CompositionFieldReadSchema = object({
11577
11657
  source: CompositionSourceRefSchema,
@@ -11615,7 +11695,33 @@ var CompositionCodeSourceSchema = object({
11615
11695
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11616
11696
  CompositionFromSourceSchema,
11617
11697
  CompositionExpressionSourceSchema,
11618
- CompositionCodeSourceSchema
11698
+ CompositionCodeSourceSchema,
11699
+ object({
11700
+ kind: literal("aggregate"),
11701
+ reducer: _enum([
11702
+ "mean",
11703
+ "median",
11704
+ "min",
11705
+ "max",
11706
+ "sum",
11707
+ "any",
11708
+ "all"
11709
+ ]),
11710
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11711
+ /** `value` is the reading; the others are the member counts. */
11712
+ publish: _enum([
11713
+ "value",
11714
+ "included",
11715
+ "excluded",
11716
+ "total"
11717
+ ]),
11718
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11719
+ empty: _enum([
11720
+ "null",
11721
+ "omit",
11722
+ "offline"
11723
+ ])
11724
+ })
11619
11725
  ]);
11620
11726
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11621
11727
  kind: literal("forward"),
@@ -11661,7 +11767,13 @@ var CompositionSchema = object({
11661
11767
  kind: literal("existing"),
11662
11768
  device: CompositionSourceRefSchema
11663
11769
  })]),
11664
- features: array(CompositionFeatureSchema).min(1).max(16)
11770
+ features: array(CompositionFeatureSchema).min(1).max(16),
11771
+ /**
11772
+ * Present when the block is a helper template. Absent on a free
11773
+ * composition (the room example, device 4603). The editor reads this,
11774
+ * never the compiled features.
11775
+ */
11776
+ helper: HelperSpecSchema.optional()
11665
11777
  }).superRefine((composition, ctx) => {
11666
11778
  const seen = /* @__PURE__ */ new Set();
11667
11779
  composition.features.forEach((feature, index) => {
@@ -17907,7 +18019,11 @@ var CompositionProblemSchema = object({
17907
18019
  "item-field-reserved",
17908
18020
  "target-missing",
17909
18021
  "target-unreadable",
17910
- "not-replaceable"
18022
+ "not-replaceable",
18023
+ "native-capability-replace",
18024
+ "mixed-quantity",
18025
+ "helper-mismatch",
18026
+ "member-constraint"
17911
18027
  ]),
17912
18028
  path: string(),
17913
18029
  message: string()
@@ -17928,7 +18044,8 @@ var CompositionUnavailableModeSchema = _enum([
17928
18044
  "null",
17929
18045
  "offline",
17930
18046
  "unknown",
17931
- "release"
18047
+ "release",
18048
+ "omit"
17932
18049
  ]);
17933
18050
  /**
17934
18051
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17965,9 +18082,11 @@ var CompositionFieldPlanSchema = object({
17965
18082
  reads: array(CompositionFieldReadSchema)
17966
18083
  });
17967
18084
  /**
17968
- * What a feature does to its target (D663): `new` builds a capability on a new
17969
- * device; on an existing device it `add`s a capability the device lacks, or
17970
- * `replace`s named fields of one it has natively.
18085
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18086
+ * new device; on an existing device it `add`s a capability the device lacks.
18087
+ * Replacing a capability the device already serves is refused
18088
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18089
+ * wire for a claim the hub already holds.
17971
18090
  */
17972
18091
  var CompositionFeatureModeSchema = _enum([
17973
18092
  "new",
@@ -18140,6 +18259,275 @@ Object.freeze({
18140
18259
  result: ["number", "null"]
18141
18260
  }
18142
18261
  });
18262
+ /**
18263
+ * One catalog row per base reading a group helper can build (D944 §3).
18264
+ * Data, not a form default written into the block.
18265
+ */
18266
+ var NOW = {
18267
+ fieldPath: "lastFetchedAt",
18268
+ expr: "now"
18269
+ };
18270
+ var CHANGED = {
18271
+ fieldPath: "lastChangedAt",
18272
+ expr: "now"
18273
+ };
18274
+ function numeric(input) {
18275
+ return {
18276
+ ...input,
18277
+ family: "numeric",
18278
+ deviceType: DeviceType.Sensor,
18279
+ memberDeviceType: DeviceType.Sensor,
18280
+ commandCaps: [],
18281
+ baseUnit: baseUnitOf(input.quantity),
18282
+ aggregates: NUMERIC_AGGREGATES,
18283
+ accept: [{
18284
+ cap: input.outputCap,
18285
+ fieldPath: input.outputField
18286
+ }]
18287
+ };
18288
+ }
18289
+ function binary(input) {
18290
+ return {
18291
+ ...input,
18292
+ family: "binary",
18293
+ deviceType: DeviceType.Sensor,
18294
+ memberDeviceType: DeviceType.Sensor,
18295
+ commandCaps: [],
18296
+ quantity: "none",
18297
+ baseUnit: void 0,
18298
+ aggregates: BINARY_AGGREGATES,
18299
+ empty: "offline"
18300
+ };
18301
+ }
18302
+ var GROUP_TEMPLATES = [
18303
+ numeric({
18304
+ id: "temperature",
18305
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18306
+ role: DeviceRole.TemperatureSensor,
18307
+ outputCap: "temperature-sensor",
18308
+ outputField: "celsius",
18309
+ quantity: "temperature",
18310
+ defaultAggregate: "mean",
18311
+ empty: "null",
18312
+ companions: [NOW]
18313
+ }),
18314
+ numeric({
18315
+ id: "humidity",
18316
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18317
+ role: DeviceRole.HumiditySensor,
18318
+ outputCap: "humidity-sensor",
18319
+ outputField: "percent",
18320
+ quantity: "humidity",
18321
+ defaultAggregate: "mean",
18322
+ empty: "null",
18323
+ companions: [NOW]
18324
+ }),
18325
+ numeric({
18326
+ id: "pressure",
18327
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18328
+ role: DeviceRole.PressureSensor,
18329
+ outputCap: "pressure-sensor",
18330
+ outputField: "hpa",
18331
+ quantity: "pressure",
18332
+ defaultAggregate: "mean",
18333
+ empty: "null",
18334
+ companions: [NOW]
18335
+ }),
18336
+ numeric({
18337
+ id: "illuminance",
18338
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18339
+ role: DeviceRole.AmbientLightSensor,
18340
+ outputCap: "ambient-light-sensor",
18341
+ outputField: "lux",
18342
+ quantity: "illuminance",
18343
+ defaultAggregate: "mean",
18344
+ empty: "null",
18345
+ companions: [NOW]
18346
+ }),
18347
+ numeric({
18348
+ id: "power",
18349
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18350
+ role: DeviceRole.PowerSensor,
18351
+ outputCap: "power-meter",
18352
+ outputField: "watts",
18353
+ quantity: "power",
18354
+ defaultAggregate: "sum",
18355
+ empty: "omit",
18356
+ companions: [NOW]
18357
+ }),
18358
+ numeric({
18359
+ id: "energy",
18360
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18361
+ role: DeviceRole.EnergySensor,
18362
+ outputCap: "power-meter",
18363
+ outputField: "kwhTotal",
18364
+ quantity: "energy",
18365
+ defaultAggregate: "sum",
18366
+ empty: "omit",
18367
+ companions: [NOW]
18368
+ }),
18369
+ numeric({
18370
+ id: "battery",
18371
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18372
+ role: DeviceRole.BatterySensor,
18373
+ outputCap: "battery",
18374
+ outputField: "percentage",
18375
+ quantity: "percentage",
18376
+ defaultAggregate: "mean",
18377
+ empty: "null",
18378
+ companions: [
18379
+ {
18380
+ fieldPath: "charging",
18381
+ expr: "\"unknown\""
18382
+ },
18383
+ {
18384
+ fieldPath: "sleeping",
18385
+ expr: "false"
18386
+ },
18387
+ {
18388
+ fieldPath: "lastUpdated",
18389
+ expr: "now"
18390
+ }
18391
+ ]
18392
+ }),
18393
+ numeric({
18394
+ id: "numeric",
18395
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18396
+ role: DeviceRole.NumericSensor,
18397
+ outputCap: "numeric-sensor",
18398
+ outputField: "value",
18399
+ quantity: "device",
18400
+ defaultAggregate: "mean",
18401
+ empty: "null",
18402
+ companions: [NOW]
18403
+ }),
18404
+ binary({
18405
+ id: "motion",
18406
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18407
+ role: DeviceRole.MotionSensor,
18408
+ outputCap: "motion",
18409
+ outputField: "detected",
18410
+ defaultAggregate: "any",
18411
+ accept: [{
18412
+ cap: "motion",
18413
+ fieldPath: "detected"
18414
+ }],
18415
+ companions: [{
18416
+ fieldPath: "lastDetectedAt",
18417
+ expr: "null"
18418
+ }, {
18419
+ fieldPath: "autoClearAfterMs",
18420
+ expr: "null"
18421
+ }]
18422
+ }),
18423
+ binary({
18424
+ id: "contact",
18425
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18426
+ role: DeviceRole.ContactSensor,
18427
+ outputCap: "contact",
18428
+ outputField: "entryOpen",
18429
+ defaultAggregate: "any",
18430
+ accept: [{
18431
+ cap: "contact",
18432
+ fieldPath: "entryOpen"
18433
+ }],
18434
+ companions: [CHANGED]
18435
+ }),
18436
+ binary({
18437
+ id: "leak",
18438
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18439
+ role: DeviceRole.LeakSensor,
18440
+ outputCap: "flood",
18441
+ outputField: "flooded",
18442
+ defaultAggregate: "any",
18443
+ accept: [{
18444
+ cap: "flood",
18445
+ fieldPath: "flooded"
18446
+ }],
18447
+ companions: [CHANGED]
18448
+ }),
18449
+ binary({
18450
+ id: "smoke",
18451
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18452
+ role: DeviceRole.SmokeSensor,
18453
+ outputCap: "smoke",
18454
+ outputField: "detected",
18455
+ defaultAggregate: "any",
18456
+ accept: [{
18457
+ cap: "smoke",
18458
+ fieldPath: "detected"
18459
+ }],
18460
+ companions: [CHANGED]
18461
+ }),
18462
+ binary({
18463
+ id: "occupancy",
18464
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18465
+ role: DeviceRole.BinarySensor,
18466
+ outputCap: "binary",
18467
+ outputField: "on",
18468
+ defaultAggregate: "any",
18469
+ accept: [{
18470
+ cap: "motion",
18471
+ fieldPath: "detected"
18472
+ }, {
18473
+ cap: "binary",
18474
+ fieldPath: "on"
18475
+ }],
18476
+ companions: [CHANGED]
18477
+ }),
18478
+ binary({
18479
+ id: "binary",
18480
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18481
+ role: DeviceRole.BinarySensor,
18482
+ outputCap: "binary",
18483
+ outputField: "on",
18484
+ defaultAggregate: "any",
18485
+ accept: [{
18486
+ cap: "binary",
18487
+ fieldPath: "on"
18488
+ }],
18489
+ companions: [CHANGED]
18490
+ }),
18491
+ actuator({
18492
+ id: "switch",
18493
+ 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.",
18494
+ deviceType: DeviceType.Switch,
18495
+ outputCap: "switch",
18496
+ commandCaps: ["switch"]
18497
+ }),
18498
+ actuator({
18499
+ id: "light",
18500
+ 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.",
18501
+ deviceType: DeviceType.Light,
18502
+ outputCap: "switch",
18503
+ commandCaps: [
18504
+ "switch",
18505
+ "brightness",
18506
+ "color"
18507
+ ]
18508
+ })
18509
+ ];
18510
+ function actuator(input) {
18511
+ return {
18512
+ ...input,
18513
+ family: "actuator",
18514
+ role: void 0,
18515
+ memberDeviceType: input.deviceType,
18516
+ outputField: "on",
18517
+ quantity: "none",
18518
+ baseUnit: void 0,
18519
+ aggregates: BINARY_AGGREGATES,
18520
+ defaultAggregate: "any",
18521
+ empty: "offline",
18522
+ accept: [{
18523
+ cap: "switch",
18524
+ fieldPath: "on"
18525
+ }],
18526
+ companions: [CHANGED]
18527
+ };
18528
+ }
18529
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18530
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18143
18531
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18144
18532
  /**
18145
18533
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19966,12 +20354,12 @@ function buildDeviceAdminUrl(parts) {
19966
20354
  * providers detect renames by matching `uniqueId` and patch `id`
19967
20355
  * in-place so dispatch keeps working.
19968
20356
  *
19969
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19970
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19971
- * state push and are the single source of truth. The `device_class` tag is
19972
- * an HA-specific input that is consumed at the discovery boundary
19973
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19974
- * stored after that point.
20357
+ * **Display unit and precision are NOT stored here.** They are a read-time
20358
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20359
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20360
+ * neither is repeated on each sample. The `device_class` tag is an
20361
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20362
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19975
20363
  *
19976
20364
  * The `raw` escape hatch carries provider-specific attributes that don't
19977
20365
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19985,6 +20373,14 @@ var SourceInfoSchema = object({
19985
20373
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19986
20374
  * Used to detect rename when `id` changes. */
19987
20375
  uniqueId: string().optional(),
20376
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20377
+ quantity: ReadingQuantitySchema.optional(),
20378
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20379
+ dateTimeShape: _enum([
20380
+ "date",
20381
+ "time",
20382
+ "datetime"
20383
+ ]).optional(),
19988
20384
  /** Provider-specific extras that don't fit the structured slots.
19989
20385
  * Whitelist what goes here per provider — do NOT dump entire HA
19990
20386
  * attribute blobs (would explode the meta JSON column). */
@@ -41089,7 +41485,8 @@ method(object({
41089
41485
  "enum"
41090
41486
  ]),
41091
41487
  enumValues: array(string()).optional(),
41092
- item: boolean().optional()
41488
+ item: boolean().optional(),
41489
+ quantity: ReadingQuantitySchema.optional()
41093
41490
  })).readonly(),
41094
41491
  itemArray: object({
41095
41492
  path: string(),
@@ -44933,8 +45330,8 @@ var MaskPolygonShapeSchema = object({
44933
45330
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44934
45331
  var MaskGridShapeSchema = object({
44935
45332
  kind: literal("grid"),
44936
- gridWidth: number(),
44937
- gridHeight: number(),
45333
+ gridWidth: number().meta(readingMeta("none")),
45334
+ gridHeight: number().meta(readingMeta("none")),
44938
45335
  cells: array(boolean())
44939
45336
  });
44940
45337
  discriminatedUnion("kind", [
@@ -45046,6 +45443,8 @@ var NcSystemEventKindSchema = _enum([
45046
45443
  "detection-blind",
45047
45444
  "motion-noisy",
45048
45445
  "motion-calm",
45446
+ "package-llm-silent",
45447
+ "package-llm-answered",
45049
45448
  "recording-stalled",
45050
45449
  "recording-healthy",
45051
45450
  "recording-degraded",
@@ -56650,6 +57049,28 @@ var PackageVersionInfoSchema = object({
56650
57049
  deprecated: string().optional(),
56651
57050
  distTags: array(string()).readonly()
56652
57051
  });
57052
+ /**
57053
+ * One published document. `ok` is markdown from the registry. `missing` means
57054
+ * the registry answered and the file was not there. `error` is a registry or
57055
+ * tarball failure — never an empty string standing in for either.
57056
+ */
57057
+ var PackageDocBodySchema = discriminatedUnion("status", [
57058
+ object({
57059
+ status: literal("ok"),
57060
+ markdown: string()
57061
+ }),
57062
+ object({ status: literal("missing") }),
57063
+ object({
57064
+ status: literal("error"),
57065
+ message: string()
57066
+ })
57067
+ ]);
57068
+ var PackageDocsSchema = object({
57069
+ name: string(),
57070
+ version: string(),
57071
+ readme: PackageDocBodySchema,
57072
+ changelog: PackageDocBodySchema
57073
+ });
56653
57074
  var SearchResultSchema = object({
56654
57075
  name: string(),
56655
57076
  version: string(),
@@ -56868,7 +57289,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56868
57289
  }), object({ success: literal(true) }), {
56869
57290
  kind: "mutation",
56870
57291
  auth: "admin"
56871
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
57292
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
57293
+ name: string().min(1),
57294
+ version: string().min(1).optional()
57295
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56872
57296
  kind: "mutation",
56873
57297
  auth: "admin"
56874
57298
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56927,27 +57351,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56927
57351
  */
56928
57352
  var AirQualitySensorStatusSchema = object({
56929
57353
  /** Carbon dioxide concentration in ppm. */
56930
- co2Ppm: number().min(0).optional(),
57354
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56931
57355
  /** Total volatile organic compounds in ppb. */
56932
- vocPpb: number().min(0).optional(),
57356
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56933
57357
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56934
- pm25: number().min(0).optional(),
57358
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56935
57359
  /** Particulate matter ≤ 10 μm in µg/m³. */
56936
- pm10: number().min(0).optional(),
57360
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56937
57361
  /** Composite AQI value (typically 0..500). */
56938
- aqi: number().optional(),
57362
+ aqi: number().optional().meta(readingMeta("index")),
56939
57363
  /** Ms epoch when the slice was last updated. */
56940
- lastFetchedAt: number(),
56941
- /** Live display unit of the single metric this slice carries (e.g. HA
56942
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56943
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56944
- * per slice is unambiguous. */
56945
- unit: string().optional(),
56946
- /** Suggested decimal places for numeric display.
56947
- * Populated live from the upstream source when provided (e.g. HA
56948
- * `attributes.suggested_display_precision`). Falls back to
56949
- * auto-formatting when absent. */
56950
- precision: number().int().min(0).max(10).optional()
57364
+ lastFetchedAt: number()
56951
57365
  });
56952
57366
  var airQualitySensorCapability = {
56953
57367
  name: "air-quality-sensor",
@@ -56984,12 +57398,12 @@ var airQualitySensorCapability = {
56984
57398
  },
56985
57399
  pm25: {
56986
57400
  kind: "gauge",
56987
- unit: "ug/m3",
57401
+ unit: "µg/m³",
56988
57402
  deadband: 1
56989
57403
  },
56990
57404
  pm10: {
56991
57405
  kind: "gauge",
56992
- unit: "ug/m3",
57406
+ unit: "µg/m³",
56993
57407
  deadband: 1
56994
57408
  },
56995
57409
  aqi: {
@@ -57007,22 +57421,13 @@ var airQualitySensorCapability = {
57007
57421
  * entries with `device_class: illuminance`.
57008
57422
  */
57009
57423
  var AmbientLightSensorStatusSchema = object({
57010
- /** Current illuminance in lux (lx). */
57011
- lux: number().min(0).meta({
57012
- unit: "lx",
57424
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
57425
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
57426
+ precision: 0,
57013
57427
  label: "Illuminance"
57014
- }),
57428
+ })),
57015
57429
  /** Ms epoch when the slice was last updated. */
57016
- lastFetchedAt: number(),
57017
- /** Live display unit from the upstream source (e.g. HA
57018
- * `attributes.unit_of_measurement`). The UI prefers this over the
57019
- * role's canonical unit. Absent → fall back to the canonical unit. */
57020
- unit: string().optional(),
57021
- /** Suggested decimal places for numeric display.
57022
- * Populated live from the upstream source when provided (e.g. HA
57023
- * `attributes.suggested_display_precision`). Falls back to
57024
- * auto-formatting when absent. */
57025
- precision: number().int().min(0).max(10).optional()
57430
+ lastFetchedAt: number()
57026
57431
  });
57027
57432
  var ambientLightSensorCapability = {
57028
57433
  name: "ambient-light-sensor",
@@ -57054,12 +57459,7 @@ var ambientLightSensorCapability = {
57054
57459
  } } },
57055
57460
  /** Clock fields: written, but excluded from the compare that decides
57056
57461
  * whether persisting is worth a SQLite commit. */
57057
- volatileStateFields: ["lastFetchedAt"],
57058
- customize: {
57059
- version: 1,
57060
- advanced: "derive",
57061
- targets: []
57062
- }
57462
+ volatileStateFields: ["lastFetchedAt"]
57063
57463
  };
57064
57464
  /**
57065
57465
  * Per-class audio metrics aggregated over a sliding window.
@@ -57286,10 +57686,7 @@ var BatteryStatusSchema = object({
57286
57686
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
57287
57687
  * declines to band a non-finite reading, which is the correct shape.
57288
57688
  */
57289
- percentage: number().min(0).max(100).nullable().meta({
57290
- unit: "%",
57291
- label: "Charge"
57292
- }),
57689
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
57293
57690
  /**
57294
57691
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
57295
57692
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -57316,7 +57713,14 @@ var BatteryStatusSchema = object({
57316
57713
  */
57317
57714
  sleeping: boolean().meta({ label: "Sleeping" }),
57318
57715
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
57319
- lastUpdated: number(),
57716
+ lastUpdated: number().meta(readingMeta("none")),
57717
+ /**
57718
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57719
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57720
+ * that only report charge. A second reading on this cap, not a second
57721
+ * `numeric-sensor` on the same device.
57722
+ */
57723
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
57320
57724
  /**
57321
57725
  * Ms epoch of the last time the device PROVED it was reachable — a
57322
57726
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -57368,7 +57772,6 @@ var BATTERY_DEVICE_TYPES = [
57368
57772
  /** Generic Customize target for the percentage reading (D936). */
57369
57773
  var batteryCustomize = {
57370
57774
  version: 1,
57371
- advanced: "derive",
57372
57775
  targets: [{
57373
57776
  id: "percentage",
57374
57777
  cardinality: "scalar",
@@ -57377,7 +57780,7 @@ var batteryCustomize = {
57377
57780
  stateField: "percentage"
57378
57781
  },
57379
57782
  deviceTypes: BATTERY_DEVICE_TYPES,
57380
- apply: "add-or-replace",
57783
+ apply: "add",
57381
57784
  path: "percentage",
57382
57785
  sources: [{
57383
57786
  kind: "field",
@@ -57498,10 +57901,16 @@ onStatusChanged: { data: object({
57498
57901
  /**
57499
57902
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57500
57903
  */
57501
- history: { fields: { percentage: {
57502
- kind: "gauge",
57503
- unit: "%"
57504
- } } },
57904
+ history: { fields: {
57905
+ percentage: {
57906
+ kind: "gauge",
57907
+ unit: "%"
57908
+ },
57909
+ volts: {
57910
+ kind: "gauge",
57911
+ unit: "V"
57912
+ }
57913
+ } },
57505
57914
  customize: batteryCustomize,
57506
57915
  /** Clock fields: written, but excluded from the compare that decides
57507
57916
  * whether persisting is worth a SQLite commit. */
@@ -57548,12 +57957,7 @@ var binaryCapability = {
57548
57957
  history: { fields: { on: { kind: "boolean" } } },
57549
57958
  /** Clock fields: written, but excluded from the compare that decides
57550
57959
  * whether persisting is worth a SQLite commit. */
57551
- volatileStateFields: ["lastChangedAt"],
57552
- customize: {
57553
- version: 1,
57554
- advanced: "derive",
57555
- targets: []
57556
- }
57960
+ volatileStateFields: ["lastChangedAt"]
57557
57961
  };
57558
57962
  /**
57559
57963
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57569,7 +57973,7 @@ var binaryCapability = {
57569
57973
  */
57570
57974
  var BrightnessStatusSchema = object({
57571
57975
  /** Current level as 0..100 inclusive. Firmware-reported. */
57572
- percentage: number().min(0).max(100),
57976
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57573
57977
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57574
57978
  lastChangedAt: number()
57575
57979
  });
@@ -58023,21 +58427,21 @@ var ClimateControlStatusSchema = object({
58023
58427
  /** Single setpoint in Celsius. Used by single-target modes
58024
58428
  * (heat / cool / dry). Null when the device is in a range mode
58025
58429
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
58026
- target: number().nullable(),
58430
+ target: number().nullable().meta(readingMeta("temperature")),
58027
58431
  /** Upper bound of the dual setpoint in Celsius. Populated only
58028
58432
  * in `heat_cool` mode. */
58029
- targetHigh: number().nullable(),
58433
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
58030
58434
  /** Lower bound of the dual setpoint in Celsius. Populated only
58031
58435
  * in `heat_cool` mode. */
58032
- targetLow: number().nullable(),
58436
+ targetLow: number().nullable().meta(readingMeta("temperature")),
58033
58437
  /** Target relative humidity (0..100). Null when the device has no
58034
58438
  * humidity control. */
58035
- targetHumidity: number().min(0).max(100).nullable(),
58439
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58036
58440
  /** Current measured temperature in Celsius. Read-only — pushed by
58037
58441
  * the device's internal sensor. */
58038
- currentTemp: number().nullable(),
58442
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
58039
58443
  /** Current measured relative humidity (0..100). Read-only. */
58040
- currentHumidity: number().min(0).max(100).nullable(),
58444
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58041
58445
  /** Vertical louver swing on/off. Null when the device has no vertical
58042
58446
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
58043
58447
  swingVertical: boolean().nullable(),
@@ -58045,11 +58449,11 @@ var ClimateControlStatusSchema = object({
58045
58449
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
58046
58450
  swingHorizontal: boolean().nullable(),
58047
58451
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
58048
- minTemp: number().nullable().optional(),
58452
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
58049
58453
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
58050
- maxTemp: number().nullable().optional(),
58454
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
58051
58455
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
58052
- tempStep: number().positive().nullable().optional(),
58456
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
58053
58457
  /** Ms epoch when the slice was last updated (push or command). */
58054
58458
  lastFetchedAt: number()
58055
58459
  });
@@ -58191,17 +58595,17 @@ var climateControlCapability = {
58191
58595
  * modes to expose in the picker without a slice fetch.
58192
58596
  */
58193
58597
  var RgbTripletSchema = object({
58194
- r: number().int().min(0).max(255),
58195
- g: number().int().min(0).max(255),
58196
- b: number().int().min(0).max(255)
58598
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58599
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58600
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
58197
58601
  });
58198
58602
  var HsvTripletSchema = object({
58199
58603
  /** Hue in degrees, 0..360. */
58200
- h: number().min(0).max(360),
58604
+ h: number().min(0).max(360).meta(readingMeta("none")),
58201
58605
  /** Saturation as 0..100 inclusive. */
58202
- s: number().min(0).max(100),
58606
+ s: number().min(0).max(100).meta(readingMeta("none")),
58203
58607
  /** Value/brightness as 0..100 inclusive. */
58204
- v: number().min(0).max(100)
58608
+ v: number().min(0).max(100).meta(readingMeta("none"))
58205
58609
  });
58206
58610
  var ColorInputSchema = discriminatedUnion("mode", [
58207
58611
  object({
@@ -58230,7 +58634,7 @@ var ColorStatusSchema = object({
58230
58634
  /** Populated when `mode === 'hsv'`. */
58231
58635
  hsv: HsvTripletSchema.optional(),
58232
58636
  /** Populated when `mode === 'mired'`. */
58233
- mireds: number().int().optional(),
58637
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
58234
58638
  /** Ms epoch of the last operator-driven change. */
58235
58639
  lastChangedAt: number()
58236
58640
  });
@@ -58425,18 +58829,12 @@ _enum([
58425
58829
  "empty",
58426
58830
  "full"
58427
58831
  ]);
58428
- /** Units a remaining-life counter is reported in. Closed: the panel renders
58429
- * each. */
58430
- var ConsumableRemainingUnitSchema = _enum([
58431
- "days",
58432
- "hours",
58433
- "cycles"
58434
- ]);
58435
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
58436
- * days). `value` null = unknown (D393). */
58832
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58833
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58834
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
58437
58835
  var ConsumableRemainingSchema = object({
58438
- value: number().min(0).nullable(),
58439
- unit: ConsumableRemainingUnitSchema
58836
+ value: number().min(0).nullable().meta(readingMeta("device")),
58837
+ kind: ConsumableRemainingKindSchema
58440
58838
  });
58441
58839
  /** A single consumable item. Either a continuous `level` (remaining
58442
58840
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -58449,7 +58847,7 @@ var ConsumableItemSchema = object({
58449
58847
  /** Display name. */
58450
58848
  label: string().min(1),
58451
58849
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
58452
- level: number().min(0).max(100).nullable(),
58850
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58453
58851
  /** The provider's discrete verdict when known (binary mode). */
58454
58852
  status: ConsumableStatusSchema.nullable(),
58455
58853
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58531,8 +58929,8 @@ var consumablesCustomize = {
58531
58929
  transforms: [{ kind: "identity" }]
58532
58930
  }],
58533
58931
  literals: [{
58534
- path: "remaining.unit",
58535
- values: [...ConsumableRemainingUnitSchema.options]
58932
+ path: "remaining.kind",
58933
+ values: [...ConsumableRemainingKindSchema.options]
58536
58934
  }]
58537
58935
  },
58538
58936
  {
@@ -58661,7 +59059,6 @@ reset: method(object({
58661
59059
  },
58662
59060
  "remaining.value": {
58663
59061
  kind: "gauge",
58664
- unitFrom: "remaining.unit",
58665
59062
  deadband: 1,
58666
59063
  displayName: "Remaining"
58667
59064
  }
@@ -58731,10 +59128,9 @@ var contactCapability = {
58731
59128
  * (empty array for the other kinds). It lives in the slice because
58732
59129
  * HA `select.*` entities may update their options at runtime.
58733
59130
  *
58734
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58735
- * `kind === 'numeric'` and are absent (undefined) for all other
58736
- * kinds. They are populated live from HA attributes on each state
58737
- * push — consistent with `options` and the sensor-unit approach.
59131
+ * Numeric `min` / `max` / `step` live in the slice and share the device
59132
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
59133
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58738
59134
  */
58739
59135
  var ControlStatusSchema = object({
58740
59136
  kind: _enum([
@@ -58746,39 +59142,17 @@ var ControlStatusSchema = object({
58746
59142
  /** Current value. Numeric kind → number; select/text/datetime → string.
58747
59143
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58748
59144
  * or time-only — HA accepts all three forms). */
58749
- value: union([number(), string()]),
59145
+ value: union([number().meta(readingMeta("device")), string()]),
58750
59146
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58751
59147
  options: array(string()),
58752
59148
  /** Ms epoch when the slice was last updated. */
58753
59149
  lastChangedAt: number(),
58754
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58755
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58756
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58757
- unit: string().optional(),
58758
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58759
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58760
- min: number().optional(),
58761
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58762
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58763
- max: number().optional(),
58764
- /** Step increment. Populated live from HA `attributes.step`.
58765
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58766
- step: number().optional(),
58767
- /** Suggested decimal places for numeric display. Populated live from HA
58768
- * `attributes.suggested_display_precision`, or derived from `step` when the
58769
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58770
- * other kinds. Falls back to auto-formatting when absent. */
58771
- precision: number().int().min(0).max(10).optional(),
58772
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58773
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58774
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58775
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58776
- * `has_date`/`has_time` attributes on each push. */
58777
- format: _enum([
58778
- "date",
58779
- "time",
58780
- "datetime"
58781
- ]).optional()
59150
+ /** Minimum allowed value, in the device quantity's base unit. */
59151
+ min: number().optional().meta(readingMeta("device")),
59152
+ /** Maximum allowed value, in the device quantity's base unit. */
59153
+ max: number().optional().meta(readingMeta("device")),
59154
+ /** Step increment, in the device quantity's base unit. */
59155
+ step: number().optional().meta(readingMeta("device"))
58782
59156
  });
58783
59157
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58784
59158
  object({
@@ -58844,9 +59218,9 @@ var CoverStatusSchema = object({
58844
59218
  ]),
58845
59219
  /** 0 = fully closed, 100 = fully open. Null when the device has
58846
59220
  * no intermediate position surface. */
58847
- position: number().min(0).max(100).nullable(),
59221
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58848
59222
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58849
- tiltPosition: number().min(0).max(100).nullable(),
59223
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58850
59224
  /** Ms epoch when the slice was last updated. */
58851
59225
  lastChangedAt: number()
58852
59226
  });
@@ -58949,9 +59323,9 @@ var NormalizedRangeSchema$1 = object({
58949
59323
  var DayNightStatusSchema = object({
58950
59324
  mode: DayNightModeSchema,
58951
59325
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58952
- sensitivity: number().optional(),
59326
+ sensitivity: number().optional().meta(readingMeta("none")),
58953
59327
  /** Delay before the IR-cut filter flips, in seconds. */
58954
- switchDelaySec: number().optional(),
59328
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58955
59329
  lastFetchedAt: number()
58956
59330
  });
58957
59331
  /**
@@ -59169,11 +59543,11 @@ var DishwasherStatusSchema = object({
59169
59543
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
59170
59544
  availablePrograms: array(string()),
59171
59545
  /** Seconds remaining on the active cycle, or null when not running / not known. */
59172
- remainingSeconds: number().int().nonnegative().nullable(),
59546
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59173
59547
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
59174
- delayedStartSeconds: number().int().nonnegative().nullable(),
59548
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
59175
59549
  /** Cycle completion 0..100, or null when not applicable. */
59176
- progressPercent: number().min(0).max(100).nullable(),
59550
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59177
59551
  /** True when the door is reported open; null when the door state is not known. */
59178
59552
  doorOpen: boolean().nullable(),
59179
59553
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -59197,7 +59571,7 @@ var DishwasherStatusSchema = object({
59197
59571
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
59198
59572
  machineCareDue: boolean().nullable(),
59199
59573
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
59200
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59574
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
59201
59575
  /** Option states: null when the appliance does not expose the option or has not said. */
59202
59576
  halfLoad: boolean().nullable(),
59203
59577
  hygienePlus: boolean().nullable(),
@@ -59212,7 +59586,7 @@ var DishwasherStatusSchema = object({
59212
59586
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
59213
59587
  activeEvents: array(string()),
59214
59588
  /** Numeric error code when the appliance reports one; null otherwise. */
59215
- errorCode: number().nullable(),
59589
+ errorCode: number().nullable().meta(readingMeta("none")),
59216
59590
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
59217
59591
  errorLabel: string().nullable(),
59218
59592
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -59443,7 +59817,7 @@ var DoorbellStatusSchema = object({
59443
59817
  /** Ms epoch of the last press. null = never observed since this provider started. */
59444
59818
  lastPressedAt: number().nullable(),
59445
59819
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59446
- pressCountSinceStart: number()
59820
+ pressCountSinceStart: number().meta(readingMeta("count"))
59447
59821
  });
59448
59822
  var DoorbellPressEventSchema = object({
59449
59823
  deviceId: number(),
@@ -59508,17 +59882,7 @@ onPressed: {
59508
59882
  history: "none",
59509
59883
  customize: doorbellCustomize
59510
59884
  };
59511
- /**
59512
- * Enum-state sensor — a string value picked from a finite option set.
59513
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59514
- * states, weather conditions, contact-source channels, mode strings).
59515
- * The option set is stable across observations; it is persisted in the
59516
- * device config blob (via the control slice at adoption) and is NOT
59517
- * stored in `sourceInfo`.
59518
- */
59519
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59520
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59521
- var EnumSensorDateTimeFormatSchema = _enum([
59885
+ _enum([
59522
59886
  "date",
59523
59887
  "time",
59524
59888
  "datetime"
@@ -59526,12 +59890,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59526
59890
  var EnumSensorStatusSchema = object({
59527
59891
  value: string(),
59528
59892
  /**
59529
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59530
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59531
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59532
- * a time-only sensor → `'time'`.
59893
+ * Set when `value` is a timestamp rather than an enum state, so history
59894
+ * does not learn ISO strings as states. The display shape (date / time /
59895
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59533
59896
  */
59534
- format: EnumSensorDateTimeFormatSchema.optional(),
59897
+ timestamp: boolean().optional(),
59535
59898
  /** Ms epoch when the slice was last updated. */
59536
59899
  lastFetchedAt: number()
59537
59900
  });
@@ -59558,11 +59921,11 @@ var enumSensorCapability = {
59558
59921
  * History (D850): the value as an `enum` series (an appliance's program or
59559
59922
  * operation state, an HVAC action). The option set is per device, so the
59560
59923
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59561
- * `format` carries a timestamp, not a state, and is not recorded.
59924
+ * `timestamp` carries a clock, not a state, and is not recorded.
59562
59925
  */
59563
59926
  history: { fields: { value: {
59564
59927
  kind: "enum",
59565
- skipWhenSet: "format"
59928
+ skipWhenSet: "timestamp"
59566
59929
  } } },
59567
59930
  /** Clock fields: written, but excluded from the compare that decides
59568
59931
  * whether persisting is worth a SQLite commit. */
@@ -59582,13 +59945,13 @@ var EventFireSchema = object({
59582
59945
  deviceId: number(),
59583
59946
  eventType: string(),
59584
59947
  data: record(string(), unknown()).nullable(),
59585
- timestamp: number(),
59586
- seq: number()
59948
+ timestamp: number().meta(readingMeta("none")),
59949
+ seq: number().meta(readingMeta("count"))
59587
59950
  });
59588
59951
  var EventEmitterStatusSchema = object({
59589
59952
  eventTypes: array(string()),
59590
59953
  lastEvent: EventFireSchema.nullable(),
59591
- eventCountSinceStart: number()
59954
+ eventCountSinceStart: number().meta(readingMeta("count"))
59592
59955
  });
59593
59956
  var eventEmitterCapability = {
59594
59957
  name: "event-emitter",
@@ -60001,10 +60364,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60001
60364
  var FanControlStatusSchema = object({
60002
60365
  /** Active speed as 0..100 inclusive. Null when the device has no
60003
60366
  * speed surface (single-speed fan). */
60004
- percentage: number().min(0).max(100).nullable(),
60367
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60005
60368
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60006
60369
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60007
- percentageStep: number().positive().optional(),
60370
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60008
60371
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60009
60372
  * — empty string when no preset is active or supported. */
60010
60373
  preset: string(),
@@ -60125,7 +60488,7 @@ var FeatureProbeStatusSchema = object({
60125
60488
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60126
60489
  model: string().nullable(),
60127
60490
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60128
- channelCount: number().nullable(),
60491
+ channelCount: number().nullable().meta(readingMeta("count")),
60129
60492
  /**
60130
60493
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60131
60494
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60248,9 +60611,91 @@ var gasCapability = {
60248
60611
  * whether persisting is worth a SQLite commit. */
60249
60612
  volatileStateFields: ["lastChangedAt"]
60250
60613
  };
60614
+ var GroupCommandsSchema = object({
60615
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60616
+ results: array(object({
60617
+ deviceId: number().int().nonnegative(),
60618
+ cap: string().min(1),
60619
+ method: string().min(1),
60620
+ ok: boolean(),
60621
+ error: string().optional(),
60622
+ /** Ms epoch of the attempt. */
60623
+ at: number()
60624
+ })) });
60625
+ var groupCommandsCapability = {
60626
+ name: "group-commands",
60627
+ scope: "device",
60628
+ deviceNative: true,
60629
+ mode: "singleton",
60630
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
60631
+ methods: {},
60632
+ status: {
60633
+ schema: GroupCommandsSchema,
60634
+ kind: "push"
60635
+ },
60636
+ runtimeState: GroupCommandsSchema,
60637
+ /**
60638
+ * Runtime-state durability: **session** — the last attempt. A restored
60639
+ * failure would outlive the command that caused it.
60640
+ *
60641
+ * See `RuntimeStateDurability`. Enforced by
60642
+ * `scripts/check-runtime-state-durability.ts`.
60643
+ */
60644
+ durability: "session",
60645
+ history: "none"
60646
+ };
60647
+ /**
60648
+ * How many members of a helper group answered (D944). Counts are integers,
60649
+ * not measurements: zero included is a real count, never a stand-in reading.
60650
+ * The reading itself stays on the output capability. History is none — the
60651
+ * series that later statistics read is the output cap's own history block.
60652
+ */
60653
+ var GroupStatusSchema = object({
60654
+ /** Members whose current value entered the aggregate. */
60655
+ included: number().int().nonnegative().meta(readingMeta("count", {
60656
+ precision: 0,
60657
+ label: "Reporting"
60658
+ })),
60659
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60660
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60661
+ precision: 0,
60662
+ label: "Excluded"
60663
+ })),
60664
+ /** `included` + `excluded`. */
60665
+ total: number().int().nonnegative().meta(readingMeta("count", {
60666
+ precision: 0,
60667
+ label: "Members"
60668
+ }))
60669
+ });
60670
+ var groupStatusCapability = {
60671
+ name: "group-status",
60672
+ scope: "device",
60673
+ deviceNative: true,
60674
+ mode: "singleton",
60675
+ deviceTypes: [
60676
+ DeviceType.Sensor,
60677
+ DeviceType.Switch,
60678
+ DeviceType.Light
60679
+ ],
60680
+ methods: {},
60681
+ status: {
60682
+ schema: GroupStatusSchema,
60683
+ kind: "push"
60684
+ },
60685
+ runtimeState: GroupStatusSchema,
60686
+ /**
60687
+ * Runtime-state durability: **session** — recomputed from members on the
60688
+ * next source event; a restored count would outlive the readings it named.
60689
+ *
60690
+ * See `RuntimeStateDurability`. Enforced by
60691
+ * `scripts/check-runtime-state-durability.ts`.
60692
+ */
60693
+ durability: "session",
60694
+ history: "none"
60695
+ };
60251
60696
  var HeatingValveStatusSchema = object({
60252
60697
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60253
- position: number().min(0).max(100).nullable(),
60698
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60254
60699
  /** Ms epoch when the slice was last updated (push or poll). */
60255
60700
  lastFetchedAt: number()
60256
60701
  });
@@ -60293,9 +60738,9 @@ var HumidifierStatusSchema = object({
60293
60738
  /** Whether the humidifier is currently on. */
60294
60739
  on: boolean(),
60295
60740
  /** Current measured relative humidity (0..100). Null when not reported. */
60296
- currentHumidity: number().min(0).max(100).nullable(),
60741
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60297
60742
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60298
- targetHumidity: number().min(0).max(100).nullable(),
60743
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60299
60744
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60300
60745
  * device has no mode surface. */
60301
60746
  mode: string().nullable(),
@@ -60305,9 +60750,9 @@ var HumidifierStatusSchema = object({
60305
60750
  * `idle` / `off`). Null when the device doesn't report it. */
60306
60751
  action: string().nullable(),
60307
60752
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60308
- minHumidity: number().nullable(),
60753
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60309
60754
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60310
- maxHumidity: number().nullable(),
60755
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60311
60756
  /** Ms epoch when the slice was last updated. */
60312
60757
  lastChangedAt: number()
60313
60758
  });
@@ -60377,22 +60822,13 @@ var humidifierCapability = {
60377
60822
  * `sourceInfo`.
60378
60823
  */
60379
60824
  var HumiditySensorStatusSchema = object({
60380
- /** Current relative humidity, 0..100. */
60381
- percent: number().min(0).max(100).meta({
60382
- unit: "%",
60825
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60826
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60827
+ precision: 0,
60383
60828
  label: "Humidity"
60384
- }),
60829
+ })),
60385
60830
  /** Ms epoch when the slice was last updated. */
60386
- lastFetchedAt: number(),
60387
- /** Live display unit from the upstream source (e.g. HA
60388
- * `attributes.unit_of_measurement`). The UI prefers this over the
60389
- * role's canonical unit. Absent → fall back to the canonical unit. */
60390
- unit: string().optional(),
60391
- /** Suggested decimal places for numeric display.
60392
- * Populated live from the upstream source when provided (e.g. HA
60393
- * `attributes.suggested_display_precision`). Falls back to
60394
- * auto-formatting when absent. */
60395
- precision: number().int().min(0).max(10).optional()
60831
+ lastFetchedAt: number()
60396
60832
  });
60397
60833
  var humiditySensorCapability = {
60398
60834
  name: "humidity-sensor",
@@ -60423,12 +60859,7 @@ var humiditySensorCapability = {
60423
60859
  } } },
60424
60860
  /** Clock fields: written, but excluded from the compare that decides
60425
60861
  * whether persisting is worth a SQLite commit. */
60426
- volatileStateFields: ["lastFetchedAt"],
60427
- customize: {
60428
- version: 1,
60429
- advanced: "derive",
60430
- targets: []
60431
- }
60862
+ volatileStateFields: ["lastFetchedAt"]
60432
60863
  };
60433
60864
  /**
60434
60865
  * Image display cap. Models a single still image exposed by an integration —
@@ -60449,7 +60880,7 @@ var ImageStatusSchema = object({
60449
60880
  * entity exposes no `entity_picture` (yet). */
60450
60881
  url: string().nullable(),
60451
60882
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60452
- lastUpdated: number().nullable()
60883
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60453
60884
  });
60454
60885
  var imageCapability = {
60455
60886
  name: "image",
@@ -60537,19 +60968,19 @@ var NormalizedRangeSchema = object({
60537
60968
  */
60538
60969
  var ImageSettingsStatusSchema = object({
60539
60970
  /** Normalized 0–100. */
60540
- brightness: number().optional(),
60971
+ brightness: number().optional().meta(readingMeta("none")),
60541
60972
  /** Normalized 0–100. */
60542
- contrast: number().optional(),
60973
+ contrast: number().optional().meta(readingMeta("none")),
60543
60974
  /** Normalized 0–100. */
60544
- saturation: number().optional(),
60975
+ saturation: number().optional().meta(readingMeta("none")),
60545
60976
  /** Normalized 0–100. */
60546
- sharpness: number().optional(),
60977
+ sharpness: number().optional().meta(readingMeta("none")),
60547
60978
  mirror: boolean().optional(),
60548
60979
  flip: boolean().optional(),
60549
60980
  rotate: ImageRotateSchema.optional(),
60550
60981
  whiteBalance: WhiteBalanceModeSchema.optional(),
60551
60982
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60552
- warmth: number().optional(),
60983
+ warmth: number().optional().meta(readingMeta("none")),
60553
60984
  exposureMode: ExposureModeSchema.optional(),
60554
60985
  backlightMode: BacklightModeSchema.optional(),
60555
60986
  lastFetchedAt: number()
@@ -60828,10 +61259,10 @@ var IntercomAbilitySchema = object({
60828
61259
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60829
61260
  codecs: array(string()),
60830
61261
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60831
- sampleRate: number(),
61262
+ sampleRate: number().meta(readingMeta("none")),
60832
61263
  duplex: _enum(["half", "full"]),
60833
61264
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60834
- maxBacklogMs: number()
61265
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60835
61266
  });
60836
61267
  var IntercomStatusSchema = object({
60837
61268
  /** True while a talk session is open. */
@@ -61058,13 +61489,13 @@ var LawnMowerControlStatusSchema = object({
61058
61489
  activity: LawnMowerActivitySchema,
61059
61490
  /** 0..100 battery percentage. Null when the device has no battery
61060
61491
  * reading. */
61061
- batteryLevel: number().min(0).max(100).nullable(),
61492
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61062
61493
  /** 0..100 mowing-completion percentage of the current task, or null when no
61063
61494
  * task is active / progress is unavailable. */
61064
- progressPercent: number().min(0).max(100).nullable(),
61495
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61065
61496
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61066
61497
  * or null when unknown. */
61067
- currentCode: number().nullable(),
61498
+ currentCode: number().nullable().meta(readingMeta("none")),
61068
61499
  /** Human label for {@link currentCode}, or null when undecodable. */
61069
61500
  currentCodeLabel: string().nullable(),
61070
61501
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61597,7 +62028,7 @@ var MediaPlayerStatusSchema = object({
61597
62028
  state: MediaPlayerStateSchema,
61598
62029
  /** Volume as 0..100 inclusive. Null when the device has no volume
61599
62030
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61600
- volumeLevel: number().min(0).max(100).nullable(),
62031
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61601
62032
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61602
62033
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61603
62034
  isMuted: boolean().nullable(),
@@ -61610,10 +62041,10 @@ var MediaPlayerStatusSchema = object({
61610
62041
  currentMedia: MediaInfoSchema.nullable(),
61611
62042
  /** Current playback position in ms. Null when not seekable or
61612
62043
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61613
- positionMs: number().int().nonnegative().nullable(),
62044
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61614
62045
  /** Total duration of the current media in ms. Null when unknown
61615
62046
  * (live stream) or nothing is playing. */
61616
- durationMs: number().int().nonnegative().nullable(),
62047
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61617
62048
  /** Shuffle toggle. Null when no shuffle surface.
61618
62049
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61619
62050
  shuffle: boolean().nullable(),
@@ -61922,7 +62353,7 @@ var motionCustomize = {
61922
62353
  eventName: "onMotionChanged"
61923
62354
  },
61924
62355
  deviceTypes: [DeviceType.Camera],
61925
- apply: "replace",
62356
+ apply: "add",
61926
62357
  applyAuthority: {
61927
62358
  kind: "camera-sensors",
61928
62359
  timeoutField: "motionCooldownMs"
@@ -61994,7 +62425,7 @@ var MotionStatusSchema = object({
61994
62425
  * it, every provider seeded it at boot, and every session left it
61995
62426
  * `detected: true` after its detach.
61996
62427
  */
61997
- autoClearAfterMs: number().nullable()
62428
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61998
62429
  });
61999
62430
  /**
62000
62431
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62031,12 +62462,18 @@ var motionCapability = {
62031
62462
  methods: {
62032
62463
  /**
62033
62464
  * Pull the current motion state synchronously. Convenience shortcut
62034
- * for consumers that don't need the full status object; equivalent
62035
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62036
- * `getStatus` once the auto-injected status surface lands in every
62037
- * consumer.
62465
+ * for consumers that don't need the full status object. A composed
62466
+ * group answers from its slice (D944): unknown throws, it is not off.
62038
62467
  */
62039
62468
  isDetected: method(object({ deviceId: number() }), boolean()) },
62469
+ /**
62470
+ * A composed motion group serves `isDetected` from `detected` (D944).
62471
+ * Without this the cap stays uncomposable (`has-commands`).
62472
+ */
62473
+ composedMethods: { isDetected: {
62474
+ kind: "slice",
62475
+ field: "detected"
62476
+ } },
62040
62477
  events: {
62041
62478
  /**
62042
62479
  * Fires every time the runner transitions a camera between
@@ -62176,7 +62613,7 @@ var MotionZoneRegionSchema = object({
62176
62613
  * the grid region(s). */
62177
62614
  var MotionZoneStatusSchema = object({
62178
62615
  enabled: boolean(),
62179
- sensitivity: number(),
62616
+ sensitivity: number().meta(readingMeta("none")),
62180
62617
  /** Grid region(s). Today exactly one `grid` shape. */
62181
62618
  regions: array(MotionZoneRegionSchema),
62182
62619
  lastFetchedAt: number()
@@ -62546,7 +62983,7 @@ var NavigationStatusSchema = object({
62546
62983
  following: boolean(),
62547
62984
  flash: boolean(),
62548
62985
  lightMode: NavigationLightModeSchema,
62549
- lightLevel: number().min(40).max(100),
62986
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62550
62987
  /** Ms epoch when the slice was last updated. */
62551
62988
  lastChangedAt: number()
62552
62989
  });
@@ -62680,13 +63117,13 @@ var NetworkLinkStatusSchema = object({
62680
63117
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62681
63118
  * SKIP a null rather than coerce it.
62682
63119
  */
62683
- signalPercent: number().min(0).max(100).nullable(),
63120
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62684
63121
  /** Raw received signal strength in dBm, when the firmware reports one. */
62685
- rssiDbm: number().optional(),
63122
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62686
63123
  /** Network name of a wireless link, when the firmware reports it. */
62687
63124
  ssid: string().optional(),
62688
63125
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62689
- lastUpdated: number()
63126
+ lastUpdated: number().meta(readingMeta("none"))
62690
63127
  });
62691
63128
  var networkLinkCapability = {
62692
63129
  name: "network-link",
@@ -63071,7 +63508,7 @@ var NotifierStatusSchema = object({
63071
63508
  lastError: string().nullable(),
63072
63509
  /** Number of deliveries currently buffered server-side (rate-limited
63073
63510
  * notifiers may queue). 0 when the provider sends synchronously. */
63074
- queueDepth: number().int().nonnegative(),
63511
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63075
63512
  /** Per-feature capability matrix — authoritative source for the
63076
63513
  * compose-form field gating. */
63077
63514
  supports: NotifierSupportsSchema
@@ -63163,11 +63600,9 @@ onSent: { data: object({
63163
63600
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63164
63601
  * for: water flow, distance, weight, frequency, signal strength, …).
63165
63602
  *
63166
- * The `unit` and `precision` fields carry the live display metadata that
63167
- * the upstream source provides with each state push (HA
63168
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63169
- * The UI reads them directly from the slice — they are the single source
63170
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63603
+ * The quantity is not on this slice. It is chosen when the device is
63604
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63605
+ * that quantity's base unit. `null` is a refused sample (D393).
63171
63606
  *
63172
63607
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63173
63608
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63176,15 +63611,8 @@ onSent: { data: object({
63176
63611
  * numeric values land here.
63177
63612
  */
63178
63613
  var NumericSensorStatusSchema = object({
63179
- value: number(),
63180
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63181
- * Populated live from the upstream source on each state push.
63182
- * Absent for unitless sensors. */
63183
- unit: string().optional(),
63184
- /** Suggested decimal places for numeric display.
63185
- * Populated live from the upstream source when provided.
63186
- * Falls back to auto-formatting when absent. */
63187
- precision: number().int().min(0).max(10).optional(),
63614
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63615
+ value: number().nullable().meta(readingMeta("device")),
63188
63616
  /** Ms epoch when the slice was last updated. */
63189
63617
  lastFetchedAt: number()
63190
63618
  });
@@ -63208,11 +63636,11 @@ var numericSensorCapability = {
63208
63636
  */
63209
63637
  durability: "restored",
63210
63638
  /**
63211
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63639
+ * History (D850): recorded series. The unit is the device quantity's base,
63640
+ * resolved when the series is read — the cap has no single unit.
63212
63641
  */
63213
63642
  history: { fields: { value: {
63214
63643
  kind: "gauge",
63215
- unitFrom: "unit",
63216
63644
  minIntervalMs: 3e4
63217
63645
  } } },
63218
63646
  /** Clock fields: written, but excluded from the compare that decides
@@ -63550,23 +63978,26 @@ var PetFeederStatusSchema = object({
63550
63978
  /** Food currently in the bowl (grams). Null when the device has not
63551
63979
  * reported a reading yet. On dual-hopper models this is the combined
63552
63980
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63553
- foodLevel: number().nullable(),
63981
+ foodLevel: number().nullable().meta(readingMeta("none")),
63554
63982
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63555
63983
  * single-hopper models. */
63556
- food1: number().nullable(),
63984
+ food1: number().nullable().meta(readingMeta("none")),
63557
63985
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63558
63986
  * single-hopper models. */
63559
- food2: number().nullable(),
63987
+ food2: number().nullable().meta(readingMeta("none")),
63560
63988
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63561
63989
  * (`device_class: problem`, on = low). True when the bowl is empty /
63562
63990
  * below the feeder's low threshold. */
63563
63991
  lowFood: boolean(),
63564
63992
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63565
63993
  * device has no battery reading. */
63566
- batteryPower: number().min(0).max(100).nullable(),
63567
- /** Days of desiccant life remaining. Null when the model has no
63568
- * desiccant sensor. */
63569
- desiccantLeftDays: number().nullable(),
63994
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63995
+ /**
63996
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63997
+ * Providers convert from the days the feeder reports. Null when the
63998
+ * model has no desiccant sensor.
63999
+ */
64000
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63570
64001
  /** True while a feed is in progress. */
63571
64002
  feeding: boolean(),
63572
64003
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63577,7 +64008,7 @@ var PetFeederStatusSchema = object({
63577
64008
  * with `errorCode` for consumers that want the raw integer. */
63578
64009
  error: string().nullable(),
63579
64010
  /** Raw device error code (0 / null = no error). */
63580
- errorCode: number().nullable(),
64011
+ errorCode: number().nullable().meta(readingMeta("none")),
63581
64012
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63582
64013
  isDualHopper: boolean(),
63583
64014
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63587,7 +64018,7 @@ var PetFeederStatusSchema = object({
63587
64018
  /** Play a chime when dispensing. */
63588
64019
  feedSound: boolean(),
63589
64020
  /** Speaker / prompt volume level (device-scaled integer). */
63590
- volume: number(),
64021
+ volume: number().meta(readingMeta("none")),
63591
64022
  /** Ms epoch when the slice was last refreshed from the cloud. */
63592
64023
  lastFetchedAt: number()
63593
64024
  });
@@ -63706,7 +64137,7 @@ var petFeederCapability = {
63706
64137
  },
63707
64138
  desiccantLeftDays: {
63708
64139
  kind: "gauge",
63709
- unit: "days"
64140
+ unit: "s"
63710
64141
  }
63711
64142
  } }
63712
64143
  };
@@ -64107,36 +64538,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64107
64538
  */
64108
64539
  var PowerMeterStatusSchema = object({
64109
64540
  /** Instantaneous power draw in watts. */
64110
- watts: number().optional().meta({
64111
- unit: "W",
64112
- label: "Power"
64113
- }),
64541
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64114
64542
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64115
- kwhTotal: number().optional().meta({
64116
- unit: "kWh",
64117
- label: "Energy"
64118
- }),
64543
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64119
64544
  /** Voltage in volts. */
64120
- volts: number().optional(),
64545
+ volts: number().optional().meta(readingMeta("voltage")),
64121
64546
  /** Current in amperes. */
64122
- amps: number().optional(),
64123
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64124
- va: number().optional(),
64547
+ amps: number().optional().meta(readingMeta("current")),
64548
+ /** Apparent power in volt-amperes. */
64549
+ va: number().optional().meta(readingMeta("apparent-power")),
64125
64550
  /** Power factor (0..1) if reported. */
64126
- powerFactor: number().min(0).max(1).optional(),
64551
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64127
64552
  /** Ms epoch when the slice was last updated. */
64128
- lastFetchedAt: number(),
64129
- /** Live display unit of the single metric this slice carries (e.g. HA
64130
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64131
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64132
- * unit per slice is unambiguous. The UI prefers this over the role's
64133
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64134
- unit: string().optional(),
64135
- /** Suggested decimal places for numeric display.
64136
- * Populated live from the upstream source when provided (e.g. HA
64137
- * `attributes.suggested_display_precision`). Falls back to
64138
- * auto-formatting when absent. */
64139
- precision: number().int().min(0).max(10).optional()
64553
+ lastFetchedAt: number()
64140
64554
  });
64141
64555
  var powerMeterCapability = {
64142
64556
  name: "power-meter",
@@ -64200,12 +64614,7 @@ var powerMeterCapability = {
64200
64614
  } },
64201
64615
  /** Clock fields: written, but excluded from the compare that decides
64202
64616
  * whether persisting is worth a SQLite commit. */
64203
- volatileStateFields: ["lastFetchedAt"],
64204
- customize: {
64205
- version: 1,
64206
- advanced: "derive",
64207
- targets: []
64208
- }
64617
+ volatileStateFields: ["lastFetchedAt"]
64209
64618
  };
64210
64619
  /**
64211
64620
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64225,11 +64634,11 @@ var powerMeterCapability = {
64225
64634
  */
64226
64635
  var GpsLocationSchema = object({
64227
64636
  /** Latitude in decimal degrees, -90..90. */
64228
- latitude: number().min(-90).max(90),
64637
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64229
64638
  /** Longitude in decimal degrees, -180..180. */
64230
- longitude: number().min(-180).max(180),
64639
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64231
64640
  /** Reported accuracy in meters (lower = better). */
64232
- accuracyMeters: number().nonnegative()
64641
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64233
64642
  });
64234
64643
  var PresenceStatusSchema = object({
64235
64644
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64241,7 +64650,7 @@ var PresenceStatusSchema = object({
64241
64650
  gps: GpsLocationSchema.nullable(),
64242
64651
  /** Optional battery level of the tracking device (0..100). Null
64243
64652
  * when not reported. */
64244
- batteryPercent: number().min(0).max(100).nullable(),
64653
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64245
64654
  /** Ms epoch when the slice was last updated. */
64246
64655
  lastChangedAt: number()
64247
64656
  });
@@ -64291,19 +64700,10 @@ var presenceCapability = {
64291
64700
  * `sourceInfo`.
64292
64701
  */
64293
64702
  var PressureSensorStatusSchema = object({
64294
- /** Current pressure in hPa. */
64295
- hpa: number(),
64703
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64704
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64296
64705
  /** Ms epoch when the slice was last updated. */
64297
- lastFetchedAt: number(),
64298
- /** Live display unit from the upstream source (e.g. HA
64299
- * `attributes.unit_of_measurement`). The UI prefers this over the
64300
- * role's canonical unit. Absent → fall back to the canonical unit. */
64301
- unit: string().optional(),
64302
- /** Suggested decimal places for numeric display.
64303
- * Populated live from the upstream source when provided (e.g. HA
64304
- * `attributes.suggested_display_precision`). Falls back to
64305
- * auto-formatting when absent. */
64306
- precision: number().int().min(0).max(10).optional()
64706
+ lastFetchedAt: number()
64307
64707
  });
64308
64708
  var pressureSensorCapability = {
64309
64709
  name: "pressure-sensor",
@@ -64829,8 +65229,8 @@ var ptzCapability = {
64829
65229
  };
64830
65230
  var PtzAutotrackSettingsSchema = object({
64831
65231
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64832
- stopDelaySeconds: number().int().min(0).max(300),
64833
- disappearDelaySeconds: number().int().min(0).max(300)
65232
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
65233
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64834
65234
  });
64835
65235
  /**
64836
65236
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64984,7 +65384,7 @@ var RunningStateStatusSchema = object({
64984
65384
  /** null = unknown — never coerced to false (D393). */
64985
65385
  running: boolean().nullable(),
64986
65386
  /** Epoch ms the current boolean began; null while `running` is null. */
64987
- since: number().nullable(),
65387
+ since: number().nullable().meta(readingMeta("none")),
64988
65388
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64989
65389
  phase: string().nullable(),
64990
65390
  /** null until a reading has been published (D393). */
@@ -64999,7 +65399,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64999
65399
  /** Generic Customize target for the running boolean (D936). */
65000
65400
  var runningCustomize = {
65001
65401
  version: 1,
65002
- advanced: "derive",
65003
65402
  targets: [{
65004
65403
  id: "running",
65005
65404
  cardinality: "scalar",
@@ -65019,7 +65418,7 @@ var runningCustomize = {
65019
65418
  DeviceType.Thermostat,
65020
65419
  DeviceType.PetFeeder
65021
65420
  ],
65022
- apply: "add-or-replace",
65421
+ apply: "add",
65023
65422
  path: "running",
65024
65423
  sources: [
65025
65424
  {
@@ -66287,9 +66686,9 @@ var RecordTriggerSchema = _enum([
66287
66686
  */
66288
66687
  var RecordWindowSchema = object({
66289
66688
  trigger: RecordTriggerSchema,
66290
- day: number().int().min(0).max(6),
66291
- startMinute: number().int().min(0).max(1439),
66292
- endMinute: number().int().min(1).max(1440)
66689
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66690
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66691
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
66293
66692
  });
66294
66693
  /** Status of one physical volume, as the camera itself describes it. */
66295
66694
  var OnboardStorageVolumeSchema = object({
@@ -66365,10 +66764,10 @@ var RecordingOnboardStatusSchema = object({
66365
66764
  /** Master "record to the card at all" switch. */
66366
66765
  enabled: boolean().nullable(),
66367
66766
  overwriteWhenFull: boolean().nullable(),
66368
- preRecordSec: number().nullable(),
66369
- postRecordSec: number().nullable(),
66767
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66768
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66370
66769
  /** Length of one recorded file, in minutes. */
66371
- segmentMinutes: number().nullable(),
66770
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66372
66771
  /** The primary track's weekly windows, flattened. */
66373
66772
  windows: array(RecordWindowSchema),
66374
66773
  /**
@@ -66384,7 +66783,7 @@ var RecordingOnboardStatusSchema = object({
66384
66783
  * provider reporting a non-zero count also reports the schedule as not
66385
66784
  * writable.
66386
66785
  */
66387
- unreadableWindows: number(),
66786
+ unreadableWindows: number().meta(readingMeta("count")),
66388
66787
  /**
66389
66788
  * The camera is scheduled to record and has NO usable storage.
66390
66789
  *
@@ -67331,7 +67730,7 @@ var CamStreamDescriptorSchema = object({
67331
67730
  url: string().optional(),
67332
67731
  codec: string().optional(),
67333
67732
  resolution: CamStreamResolutionSchema.optional(),
67334
- fps: number().positive().optional(),
67733
+ fps: number().positive().optional().meta(readingMeta("rate")),
67335
67734
  label: string().optional(),
67336
67735
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
67337
67736
  deviceFeatures: array(string()).optional(),
@@ -67418,18 +67817,18 @@ var StreamProfileSchema = _enum([
67418
67817
  ]);
67419
67818
  /** The current encoding config of one stream profile. */
67420
67819
  var StreamProfileConfigSchema = object({
67421
- width: number(),
67422
- height: number(),
67820
+ width: number().meta(readingMeta("none")),
67821
+ height: number().meta(readingMeta("none")),
67423
67822
  codec: _enum(["h264", "h265"]),
67424
- framerate: number(),
67425
- bitrate: number(),
67823
+ framerate: number().meta(readingMeta("rate")),
67824
+ bitrate: number().meta(readingMeta("bitrate")),
67426
67825
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
67427
67826
  encoderProfile: _enum([
67428
67827
  "high",
67429
67828
  "main",
67430
67829
  "baseline"
67431
67830
  ]).optional(),
67432
- gop: number().optional(),
67831
+ gop: number().optional().meta(readingMeta("none")),
67433
67832
  /**
67434
67833
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
67435
67834
  *
@@ -67848,12 +68247,7 @@ var switchCapability = {
67848
68247
  label: "On",
67849
68248
  kind: "boolean"
67850
68249
  }
67851
- }] },
67852
- customize: {
67853
- version: 1,
67854
- advanced: "derive",
67855
- targets: []
67856
- }
68250
+ }] }
67857
68251
  };
67858
68252
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67859
68253
  function hasControlCharacter(value) {
@@ -68436,35 +68830,21 @@ var tamperCapability = {
68436
68830
  * entries with `device_class: temperature` and any future native
68437
68831
  * thermometer.
68438
68832
  *
68439
- * Unit normalisation: providers convert to Celsius before storing.
68440
- * The slice value is always Celsius so cross-cap aggregators
68441
- * (climate-control's `currentTemp`, energy analytics) can compose
68442
- * without per-source unit fixups. The canonical display unit (`°C`) is
68443
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68444
- * `sourceInfo`.
68833
+ * The number is Celsius (D944). Providers convert on ingest. A failed
68834
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
68445
68835
  *
68446
68836
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
68447
68837
  * frozen feed (provider hung) distinct from a "temperature hasn't
68448
68838
  * changed" steady state.
68449
68839
  */
68450
68840
  var TemperatureSensorStatusSchema = object({
68451
- /** Current temperature in Celsius. */
68452
- celsius: number().meta({
68453
- unit: "°C",
68841
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68842
+ celsius: number().nullable().meta(readingMeta("temperature", {
68843
+ precision: 1,
68454
68844
  label: "Temperature"
68455
- }),
68845
+ })),
68456
68846
  /** Ms epoch when the slice was last updated (push or poll). */
68457
- lastFetchedAt: number(),
68458
- /** Live display unit from the upstream source (e.g. HA
68459
- * `attributes.unit_of_measurement`). The UI prefers this over the
68460
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
68461
- * Absent → the UI falls back to the role's canonical unit. */
68462
- unit: string().optional(),
68463
- /** Suggested decimal places for numeric display.
68464
- * Populated live from the upstream source when provided (e.g. HA
68465
- * `attributes.suggested_display_precision`). Falls back to
68466
- * auto-formatting when absent. */
68467
- precision: number().int().min(0).max(10).optional()
68847
+ lastFetchedAt: number()
68468
68848
  });
68469
68849
  var temperatureSensorCapability = {
68470
68850
  name: "temperature-sensor",
@@ -68495,12 +68875,7 @@ var temperatureSensorCapability = {
68495
68875
  } } },
68496
68876
  /** Clock fields: written, but excluded from the compare that decides
68497
68877
  * whether persisting is worth a SQLite commit. */
68498
- volatileStateFields: ["lastFetchedAt"],
68499
- customize: {
68500
- version: 1,
68501
- advanced: "derive",
68502
- targets: []
68503
- }
68878
+ volatileStateFields: ["lastFetchedAt"]
68504
68879
  };
68505
68880
  /**
68506
68881
  * toast — system-scoped singleton capability that streams toast
@@ -68783,16 +69158,16 @@ var VacuumControlStatusSchema = object({
68783
69158
  ]),
68784
69159
  /** 0..100 battery percentage. Null when the device has no battery
68785
69160
  * reading. */
68786
- batteryLevel: number().min(0).max(100).nullable(),
69161
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68787
69162
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68788
69163
  * the vacuum has no speed control. */
68789
69164
  fanSpeed: string().nullable(),
68790
69165
  /** Speed tokens the hardware accepts — drives the UI selector. */
68791
69166
  availableFanSpeeds: array(string()),
68792
69167
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68793
- progressPercent: number().min(0).max(100).nullable(),
69168
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68794
69169
  /** Current error code (0 / null = no error). */
68795
- errorCode: number().nullable(),
69170
+ errorCode: number().nullable().meta(readingMeta("none")),
68796
69171
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68797
69172
  errorLabel: string().nullable(),
68798
69173
  /** Ms epoch when the slice was last updated. */
@@ -68870,7 +69245,7 @@ var ValveStatusSchema = object({
68870
69245
  ]),
68871
69246
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68872
69247
  * intermediate position surface. */
68873
- position: number().min(0).max(100).nullable(),
69248
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68874
69249
  /** Ms epoch when the slice was last updated. */
68875
69250
  lastChangedAt: number()
68876
69251
  });
@@ -68978,9 +69353,9 @@ var vibrationCapability = {
68978
69353
  */
68979
69354
  var WaterHeaterStatusSchema = object({
68980
69355
  /** Current measured temperature. Null when not reported. */
68981
- currentTemp: number().nullable(),
69356
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68982
69357
  /** Target temperature setpoint. Null when no setpoint surface. */
68983
- targetTemp: number().nullable(),
69358
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68984
69359
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68985
69360
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68986
69361
  * device reports an unknown state. */
@@ -68991,9 +69366,9 @@ var WaterHeaterStatusSchema = object({
68991
69366
  * has no away surface. */
68992
69367
  away: boolean().nullable(),
68993
69368
  /** HA `min_temp` attribute. Null when not reported. */
68994
- minTemp: number().nullable(),
69369
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68995
69370
  /** HA `max_temp` attribute. Null when not reported. */
68996
- maxTemp: number().nullable(),
69371
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68997
69372
  /** Ms epoch when the slice was last updated. */
68998
69373
  lastChangedAt: number()
68999
69374
  });
@@ -69078,25 +69453,18 @@ var waterHeaterCapability = {
69078
69453
  * Forecast deferred — current conditions only for v1.
69079
69454
  */
69080
69455
  var WeatherStatusSchema = object({
69081
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69082
- * when no condition has been reported yet. */
69456
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69083
69457
  condition: string().nullable(),
69084
- /** Current temperature in the reported unit. Null when not provided. */
69085
- temperature: number().nullable(),
69086
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69087
- temperatureUnit: string().nullable(),
69458
+ /** Current temperature in Celsius. Null when not provided or refused. */
69459
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69088
69460
  /** Relative humidity (0..100). Null when not provided. */
69089
- humidity: number().min(0).max(100).nullable(),
69090
- /** Barometric pressure in the reported unit. Null when not provided. */
69091
- pressure: number().nullable(),
69092
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69093
- pressureUnit: string().nullable(),
69094
- /** Wind speed in the reported unit. Null when not provided. */
69095
- windSpeed: number().nullable(),
69096
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69097
- windSpeedUnit: string().nullable(),
69461
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
69462
+ /** Barometric pressure in hPa. Null when not provided or refused. */
69463
+ pressure: number().nullable().meta(readingMeta("pressure")),
69464
+ /** Wind speed in metres per second. Null when not provided or refused. */
69465
+ windSpeed: number().nullable().meta(readingMeta("speed")),
69098
69466
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69099
- windBearing: number().nullable(),
69467
+ windBearing: number().nullable().meta(readingMeta("angle")),
69100
69468
  /** Ms epoch when the slice was last updated. */
69101
69469
  lastFetchedAt: number()
69102
69470
  });
@@ -69124,12 +69492,12 @@ var weatherCapability = {
69124
69492
  */
69125
69493
  durability: "session",
69126
69494
  /**
69127
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69495
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69128
69496
  */
69129
69497
  history: { fields: {
69130
69498
  temperature: {
69131
69499
  kind: "gauge",
69132
- unitFrom: "temperatureUnit",
69500
+ unit: "°C",
69133
69501
  deadband: .2
69134
69502
  },
69135
69503
  humidity: {
@@ -69139,12 +69507,12 @@ var weatherCapability = {
69139
69507
  },
69140
69508
  pressure: {
69141
69509
  kind: "gauge",
69142
- unitFrom: "pressureUnit",
69510
+ unit: "hPa",
69143
69511
  deadband: .5
69144
69512
  },
69145
69513
  windSpeed: {
69146
69514
  kind: "gauge",
69147
- unitFrom: "windSpeedUnit",
69515
+ unit: "m/s",
69148
69516
  deadband: .5
69149
69517
  }
69150
69518
  } }
@@ -70508,6 +70876,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
70508
70876
  featureProbe: featureProbeCapability,
70509
70877
  flood: floodCapability,
70510
70878
  gas: gasCapability,
70879
+ groupCommands: groupCommandsCapability,
70880
+ groupStatus: groupStatusCapability,
70511
70881
  heatingValve: heatingValveCapability,
70512
70882
  humidifier: humidifierCapability,
70513
70883
  humiditySensor: humiditySensorCapability,
@@ -71888,6 +72258,13 @@ Object.freeze({
71888
72258
  access: "view",
71889
72259
  role: "admin"
71890
72260
  },
72261
+ "addons.getPackageDocs": {
72262
+ capName: "addons",
72263
+ capScope: "system",
72264
+ addonId: null,
72265
+ access: "view",
72266
+ role: "admin"
72267
+ },
71891
72268
  "addons.getVersions": {
71892
72269
  capName: "addons",
71893
72270
  capScope: "system",
@@ -74996,6 +75373,20 @@ Object.freeze({
74996
75373
  access: "view",
74997
75374
  role: "viewer"
74998
75375
  },
75376
+ "groupCommands.getStatus": {
75377
+ capName: "group-commands",
75378
+ capScope: "device",
75379
+ addonId: null,
75380
+ access: "view",
75381
+ role: "viewer"
75382
+ },
75383
+ "groupStatus.getStatus": {
75384
+ capName: "group-status",
75385
+ capScope: "device",
75386
+ addonId: null,
75387
+ access: "view",
75388
+ role: "viewer"
75389
+ },
74999
75390
  "heatingValve.getStatus": {
75000
75391
  capName: "heating-valve",
75001
75392
  capScope: "device",
@@ -84818,6 +85209,37 @@ var SENSOR_CAP_VOCABULARY = {
84818
85209
  icon: "flame"
84819
85210
  }]
84820
85211
  },
85212
+ "group-commands": {
85213
+ scope: "device",
85214
+ noun: t("Group command", "Comando di gruppo")
85215
+ },
85216
+ "group-status": {
85217
+ scope: "device",
85218
+ noun: t("Group", "Gruppo"),
85219
+ state: [
85220
+ {
85221
+ kind: "numeric",
85222
+ field: "included",
85223
+ quantity: t("the members reporting", "i membri che rispondono"),
85224
+ label: t("Reporting", "Rispondono"),
85225
+ icon: "users"
85226
+ },
85227
+ {
85228
+ kind: "numeric",
85229
+ field: "excluded",
85230
+ quantity: t("the members left out", "i membri esclusi"),
85231
+ label: t("Excluded", "Esclusi"),
85232
+ icon: "users"
85233
+ },
85234
+ {
85235
+ kind: "numeric",
85236
+ field: "total",
85237
+ quantity: t("the member count", "il numero di membri"),
85238
+ label: t("Members", "Membri"),
85239
+ icon: "sigma"
85240
+ }
85241
+ ]
85242
+ },
84821
85243
  "humidity-sensor": {
84822
85244
  scope: "device",
84823
85245
  noun: t("Humidity", "Umidità"),
@@ -84851,7 +85273,6 @@ var SENSOR_CAP_VOCABULARY = {
84851
85273
  field: "value",
84852
85274
  quantity: t("the reading", "il valore"),
84853
85275
  label: t("Reading", "Valore"),
84854
- unitField: "unit",
84855
85276
  icon: "gauge"
84856
85277
  }]
84857
85278
  },
@@ -84987,7 +85408,7 @@ var SENSOR_CAP_VOCABULARY = {
84987
85408
  field: "temperature",
84988
85409
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84989
85410
  label: t("Temperature", "Temperatura"),
84990
- unitField: "temperatureUnit",
85411
+ unit: "°C",
84991
85412
  icon: "thermometer"
84992
85413
  },
84993
85414
  {
@@ -85003,7 +85424,7 @@ var SENSOR_CAP_VOCABULARY = {
85003
85424
  field: "windSpeed",
85004
85425
  quantity: t("the wind speed", "la velocità del vento"),
85005
85426
  label: t("Wind", "Vento"),
85006
- unitField: "windSpeedUnit",
85427
+ unit: "m/s",
85007
85428
  icon: "wind"
85008
85429
  },
85009
85430
  {
@@ -85011,7 +85432,7 @@ var SENSOR_CAP_VOCABULARY = {
85011
85432
  field: "pressure",
85012
85433
  quantity: t("the air pressure", "la pressione atmosferica"),
85013
85434
  label: t("Pressure", "Pressione"),
85014
- unitField: "pressureUnit",
85435
+ unit: "hPa",
85015
85436
  icon: "gauge"
85016
85437
  }
85017
85438
  ]
@@ -86490,6 +86911,16 @@ Object.freeze({
86490
86911
  form: "single",
86491
86912
  optional: false
86492
86913
  }],
86914
+ "groupCommands.getStatus": [{
86915
+ name: "deviceId",
86916
+ form: "single",
86917
+ optional: false
86918
+ }],
86919
+ "groupStatus.getStatus": [{
86920
+ name: "deviceId",
86921
+ form: "single",
86922
+ optional: false
86923
+ }],
86493
86924
  "heatingValve.getStatus": [{
86494
86925
  name: "deviceId",
86495
86926
  form: "single",
@@ -88474,6 +88905,8 @@ new Map(Object.entries({
88474
88905
  "detection-blind": "device-events",
88475
88906
  "motion-noisy": "device-events",
88476
88907
  "motion-calm": "device-events",
88908
+ "package-llm-silent": "device-events",
88909
+ "package-llm-answered": "device-events",
88477
88910
  "recording-stalled": "device-events",
88478
88911
  "recording-healthy": "device-events",
88479
88912
  "recording-degraded": "device-events",
@@ -88557,6 +88990,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
88557
88990
  label: "Motion without a subject",
88558
88991
  kinds: ["motion-noisy", "motion-calm"]
88559
88992
  },
88993
+ {
88994
+ id: "package-llm",
88995
+ family: "device-events",
88996
+ label: "Package confirm",
88997
+ kinds: ["package-llm-silent", "package-llm-answered"]
88998
+ },
88560
88999
  {
88561
89000
  id: "recording",
88562
89001
  family: "device-events",
@@ -88738,6 +89177,8 @@ var DEVICE_EVENTS = [
88738
89177
  "detection-blind",
88739
89178
  "motion-noisy",
88740
89179
  "motion-calm",
89180
+ "package-llm-silent",
89181
+ "package-llm-answered",
88741
89182
  "recording-stalled",
88742
89183
  "recording-healthy",
88743
89184
  "recording-degraded",
@@ -89006,6 +89447,15 @@ new Map([
89006
89447
  snapshotLabel: "Cameras with constant motion now",
89007
89448
  snapshotSection: null
89008
89449
  },
89450
+ {
89451
+ id: "package-llm-confirm",
89452
+ family: "device-events",
89453
+ label: "Package confirm (not answering / answering again)",
89454
+ bad: ["package-llm-silent"],
89455
+ recovery: ["package-llm-answered"],
89456
+ snapshotLabel: "Cameras whose package confirm is not answering",
89457
+ snapshotSection: null
89458
+ },
89009
89459
  {
89010
89460
  id: "recording-health",
89011
89461
  family: "device-events",