@camstack/addon-provider-amcrest 0.2.159 → 0.2.161

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.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. */
@@ -59424,6 +59798,7 @@ var doorbellCustomize = {
59424
59798
  applyAuthority: { kind: "composition-claim" },
59425
59799
  eventName: "onPressed",
59426
59800
  edges: ["rising"],
59801
+ defaultEdge: "rising",
59427
59802
  timestampPath: "lastPressedAt",
59428
59803
  counterPath: "pressCountSinceStart",
59429
59804
  highWater: "composer-advance",
@@ -59442,7 +59817,7 @@ var DoorbellStatusSchema = object({
59442
59817
  /** Ms epoch of the last press. null = never observed since this provider started. */
59443
59818
  lastPressedAt: number().nullable(),
59444
59819
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
59445
- pressCountSinceStart: number()
59820
+ pressCountSinceStart: number().meta(readingMeta("count"))
59446
59821
  });
59447
59822
  var DoorbellPressEventSchema = object({
59448
59823
  deviceId: number(),
@@ -59507,17 +59882,7 @@ onPressed: {
59507
59882
  history: "none",
59508
59883
  customize: doorbellCustomize
59509
59884
  };
59510
- /**
59511
- * Enum-state sensor — a string value picked from a finite option set.
59512
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59513
- * states, weather conditions, contact-source channels, mode strings).
59514
- * The option set is stable across observations; it is persisted in the
59515
- * device config blob (via the control slice at adoption) and is NOT
59516
- * stored in `sourceInfo`.
59517
- */
59518
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59519
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59520
- var EnumSensorDateTimeFormatSchema = _enum([
59885
+ _enum([
59521
59886
  "date",
59522
59887
  "time",
59523
59888
  "datetime"
@@ -59525,12 +59890,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59525
59890
  var EnumSensorStatusSchema = object({
59526
59891
  value: string(),
59527
59892
  /**
59528
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59529
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59530
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59531
- * 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.
59532
59896
  */
59533
- format: EnumSensorDateTimeFormatSchema.optional(),
59897
+ timestamp: boolean().optional(),
59534
59898
  /** Ms epoch when the slice was last updated. */
59535
59899
  lastFetchedAt: number()
59536
59900
  });
@@ -59557,11 +59921,11 @@ var enumSensorCapability = {
59557
59921
  * History (D850): the value as an `enum` series (an appliance's program or
59558
59922
  * operation state, an HVAC action). The option set is per device, so the
59559
59923
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59560
- * `format` carries a timestamp, not a state, and is not recorded.
59924
+ * `timestamp` carries a clock, not a state, and is not recorded.
59561
59925
  */
59562
59926
  history: { fields: { value: {
59563
59927
  kind: "enum",
59564
- skipWhenSet: "format"
59928
+ skipWhenSet: "timestamp"
59565
59929
  } } },
59566
59930
  /** Clock fields: written, but excluded from the compare that decides
59567
59931
  * whether persisting is worth a SQLite commit. */
@@ -59581,13 +59945,13 @@ var EventFireSchema = object({
59581
59945
  deviceId: number(),
59582
59946
  eventType: string(),
59583
59947
  data: record(string(), unknown()).nullable(),
59584
- timestamp: number(),
59585
- seq: number()
59948
+ timestamp: number().meta(readingMeta("none")),
59949
+ seq: number().meta(readingMeta("count"))
59586
59950
  });
59587
59951
  var EventEmitterStatusSchema = object({
59588
59952
  eventTypes: array(string()),
59589
59953
  lastEvent: EventFireSchema.nullable(),
59590
- eventCountSinceStart: number()
59954
+ eventCountSinceStart: number().meta(readingMeta("count"))
59591
59955
  });
59592
59956
  var eventEmitterCapability = {
59593
59957
  name: "event-emitter",
@@ -60000,10 +60364,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
60000
60364
  var FanControlStatusSchema = object({
60001
60365
  /** Active speed as 0..100 inclusive. Null when the device has no
60002
60366
  * speed surface (single-speed fan). */
60003
- percentage: number().min(0).max(100).nullable(),
60367
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60004
60368
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
60005
60369
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
60006
- percentageStep: number().positive().optional(),
60370
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
60007
60371
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
60008
60372
  * — empty string when no preset is active or supported. */
60009
60373
  preset: string(),
@@ -60124,7 +60488,7 @@ var FeatureProbeStatusSchema = object({
60124
60488
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
60125
60489
  model: string().nullable(),
60126
60490
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
60127
- channelCount: number().nullable(),
60491
+ channelCount: number().nullable().meta(readingMeta("count")),
60128
60492
  /**
60129
60493
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
60130
60494
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -60247,9 +60611,91 @@ var gasCapability = {
60247
60611
  * whether persisting is worth a SQLite commit. */
60248
60612
  volatileStateFields: ["lastChangedAt"]
60249
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
+ };
60250
60696
  var HeatingValveStatusSchema = object({
60251
60697
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
60252
- position: number().min(0).max(100).nullable(),
60698
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60253
60699
  /** Ms epoch when the slice was last updated (push or poll). */
60254
60700
  lastFetchedAt: number()
60255
60701
  });
@@ -60292,9 +60738,9 @@ var HumidifierStatusSchema = object({
60292
60738
  /** Whether the humidifier is currently on. */
60293
60739
  on: boolean(),
60294
60740
  /** Current measured relative humidity (0..100). Null when not reported. */
60295
- currentHumidity: number().min(0).max(100).nullable(),
60741
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60296
60742
  /** Target relative humidity (0..100). Null when no setpoint surface. */
60297
- targetHumidity: number().min(0).max(100).nullable(),
60743
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
60298
60744
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
60299
60745
  * device has no mode surface. */
60300
60746
  mode: string().nullable(),
@@ -60304,9 +60750,9 @@ var HumidifierStatusSchema = object({
60304
60750
  * `idle` / `off`). Null when the device doesn't report it. */
60305
60751
  action: string().nullable(),
60306
60752
  /** HA `min_humidity` attribute. Null → UI uses 0. */
60307
- minHumidity: number().nullable(),
60753
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
60308
60754
  /** HA `max_humidity` attribute. Null → UI uses 100. */
60309
- maxHumidity: number().nullable(),
60755
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
60310
60756
  /** Ms epoch when the slice was last updated. */
60311
60757
  lastChangedAt: number()
60312
60758
  });
@@ -60376,22 +60822,13 @@ var humidifierCapability = {
60376
60822
  * `sourceInfo`.
60377
60823
  */
60378
60824
  var HumiditySensorStatusSchema = object({
60379
- /** Current relative humidity, 0..100. */
60380
- percent: number().min(0).max(100).meta({
60381
- 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,
60382
60828
  label: "Humidity"
60383
- }),
60829
+ })),
60384
60830
  /** Ms epoch when the slice was last updated. */
60385
- lastFetchedAt: number(),
60386
- /** Live display unit from the upstream source (e.g. HA
60387
- * `attributes.unit_of_measurement`). The UI prefers this over the
60388
- * role's canonical unit. Absent → fall back to the canonical unit. */
60389
- unit: string().optional(),
60390
- /** Suggested decimal places for numeric display.
60391
- * Populated live from the upstream source when provided (e.g. HA
60392
- * `attributes.suggested_display_precision`). Falls back to
60393
- * auto-formatting when absent. */
60394
- precision: number().int().min(0).max(10).optional()
60831
+ lastFetchedAt: number()
60395
60832
  });
60396
60833
  var humiditySensorCapability = {
60397
60834
  name: "humidity-sensor",
@@ -60422,12 +60859,7 @@ var humiditySensorCapability = {
60422
60859
  } } },
60423
60860
  /** Clock fields: written, but excluded from the compare that decides
60424
60861
  * whether persisting is worth a SQLite commit. */
60425
- volatileStateFields: ["lastFetchedAt"],
60426
- customize: {
60427
- version: 1,
60428
- advanced: "derive",
60429
- targets: []
60430
- }
60862
+ volatileStateFields: ["lastFetchedAt"]
60431
60863
  };
60432
60864
  /**
60433
60865
  * Image display cap. Models a single still image exposed by an integration —
@@ -60448,7 +60880,7 @@ var ImageStatusSchema = object({
60448
60880
  * entity exposes no `entity_picture` (yet). */
60449
60881
  url: string().nullable(),
60450
60882
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
60451
- lastUpdated: number().nullable()
60883
+ lastUpdated: number().nullable().meta(readingMeta("none"))
60452
60884
  });
60453
60885
  var imageCapability = {
60454
60886
  name: "image",
@@ -60536,19 +60968,19 @@ var NormalizedRangeSchema = object({
60536
60968
  */
60537
60969
  var ImageSettingsStatusSchema = object({
60538
60970
  /** Normalized 0–100. */
60539
- brightness: number().optional(),
60971
+ brightness: number().optional().meta(readingMeta("none")),
60540
60972
  /** Normalized 0–100. */
60541
- contrast: number().optional(),
60973
+ contrast: number().optional().meta(readingMeta("none")),
60542
60974
  /** Normalized 0–100. */
60543
- saturation: number().optional(),
60975
+ saturation: number().optional().meta(readingMeta("none")),
60544
60976
  /** Normalized 0–100. */
60545
- sharpness: number().optional(),
60977
+ sharpness: number().optional().meta(readingMeta("none")),
60546
60978
  mirror: boolean().optional(),
60547
60979
  flip: boolean().optional(),
60548
60980
  rotate: ImageRotateSchema.optional(),
60549
60981
  whiteBalance: WhiteBalanceModeSchema.optional(),
60550
60982
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60551
- warmth: number().optional(),
60983
+ warmth: number().optional().meta(readingMeta("none")),
60552
60984
  exposureMode: ExposureModeSchema.optional(),
60553
60985
  backlightMode: BacklightModeSchema.optional(),
60554
60986
  lastFetchedAt: number()
@@ -60827,10 +61259,10 @@ var IntercomAbilitySchema = object({
60827
61259
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60828
61260
  codecs: array(string()),
60829
61261
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60830
- sampleRate: number(),
61262
+ sampleRate: number().meta(readingMeta("none")),
60831
61263
  duplex: _enum(["half", "full"]),
60832
61264
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60833
- maxBacklogMs: number()
61265
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60834
61266
  });
60835
61267
  var IntercomStatusSchema = object({
60836
61268
  /** True while a talk session is open. */
@@ -61057,13 +61489,13 @@ var LawnMowerControlStatusSchema = object({
61057
61489
  activity: LawnMowerActivitySchema,
61058
61490
  /** 0..100 battery percentage. Null when the device has no battery
61059
61491
  * reading. */
61060
- batteryLevel: number().min(0).max(100).nullable(),
61492
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61061
61493
  /** 0..100 mowing-completion percentage of the current task, or null when no
61062
61494
  * task is active / progress is unavailable. */
61063
- progressPercent: number().min(0).max(100).nullable(),
61495
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61064
61496
  /** Current device/event code (dynamic — mostly status, sometimes an error),
61065
61497
  * or null when unknown. */
61066
- currentCode: number().nullable(),
61498
+ currentCode: number().nullable().meta(readingMeta("none")),
61067
61499
  /** Human label for {@link currentCode}, or null when undecodable. */
61068
61500
  currentCodeLabel: string().nullable(),
61069
61501
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61596,7 +62028,7 @@ var MediaPlayerStatusSchema = object({
61596
62028
  state: MediaPlayerStateSchema,
61597
62029
  /** Volume as 0..100 inclusive. Null when the device has no volume
61598
62030
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61599
- volumeLevel: number().min(0).max(100).nullable(),
62031
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61600
62032
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61601
62033
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61602
62034
  isMuted: boolean().nullable(),
@@ -61609,10 +62041,10 @@ var MediaPlayerStatusSchema = object({
61609
62041
  currentMedia: MediaInfoSchema.nullable(),
61610
62042
  /** Current playback position in ms. Null when not seekable or
61611
62043
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61612
- positionMs: number().int().nonnegative().nullable(),
62044
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61613
62045
  /** Total duration of the current media in ms. Null when unknown
61614
62046
  * (live stream) or nothing is playing. */
61615
- durationMs: number().int().nonnegative().nullable(),
62047
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61616
62048
  /** Shuffle toggle. Null when no shuffle surface.
61617
62049
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61618
62050
  shuffle: boolean().nullable(),
@@ -61921,7 +62353,7 @@ var motionCustomize = {
61921
62353
  eventName: "onMotionChanged"
61922
62354
  },
61923
62355
  deviceTypes: [DeviceType.Camera],
61924
- apply: "replace",
62356
+ apply: "add",
61925
62357
  applyAuthority: {
61926
62358
  kind: "camera-sensors",
61927
62359
  timeoutField: "motionCooldownMs"
@@ -61993,7 +62425,7 @@ var MotionStatusSchema = object({
61993
62425
  * it, every provider seeded it at boot, and every session left it
61994
62426
  * `detected: true` after its detach.
61995
62427
  */
61996
- autoClearAfterMs: number().nullable()
62428
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61997
62429
  });
61998
62430
  /**
61999
62431
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -62030,12 +62462,18 @@ var motionCapability = {
62030
62462
  methods: {
62031
62463
  /**
62032
62464
  * Pull the current motion state synchronously. Convenience shortcut
62033
- * for consumers that don't need the full status object; equivalent
62034
- * to `getStatus()?.detected ?? false`. Will likely be folded into
62035
- * `getStatus` once the auto-injected status surface lands in every
62036
- * 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.
62037
62467
  */
62038
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
+ } },
62039
62477
  events: {
62040
62478
  /**
62041
62479
  * Fires every time the runner transitions a camera between
@@ -62175,7 +62613,7 @@ var MotionZoneRegionSchema = object({
62175
62613
  * the grid region(s). */
62176
62614
  var MotionZoneStatusSchema = object({
62177
62615
  enabled: boolean(),
62178
- sensitivity: number(),
62616
+ sensitivity: number().meta(readingMeta("none")),
62179
62617
  /** Grid region(s). Today exactly one `grid` shape. */
62180
62618
  regions: array(MotionZoneRegionSchema),
62181
62619
  lastFetchedAt: number()
@@ -62545,7 +62983,7 @@ var NavigationStatusSchema = object({
62545
62983
  following: boolean(),
62546
62984
  flash: boolean(),
62547
62985
  lightMode: NavigationLightModeSchema,
62548
- lightLevel: number().min(40).max(100),
62986
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62549
62987
  /** Ms epoch when the slice was last updated. */
62550
62988
  lastChangedAt: number()
62551
62989
  });
@@ -62679,13 +63117,13 @@ var NetworkLinkStatusSchema = object({
62679
63117
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62680
63118
  * SKIP a null rather than coerce it.
62681
63119
  */
62682
- signalPercent: number().min(0).max(100).nullable(),
63120
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62683
63121
  /** Raw received signal strength in dBm, when the firmware reports one. */
62684
- rssiDbm: number().optional(),
63122
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62685
63123
  /** Network name of a wireless link, when the firmware reports it. */
62686
63124
  ssid: string().optional(),
62687
63125
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62688
- lastUpdated: number()
63126
+ lastUpdated: number().meta(readingMeta("none"))
62689
63127
  });
62690
63128
  var networkLinkCapability = {
62691
63129
  name: "network-link",
@@ -63070,7 +63508,7 @@ var NotifierStatusSchema = object({
63070
63508
  lastError: string().nullable(),
63071
63509
  /** Number of deliveries currently buffered server-side (rate-limited
63072
63510
  * notifiers may queue). 0 when the provider sends synchronously. */
63073
- queueDepth: number().int().nonnegative(),
63511
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
63074
63512
  /** Per-feature capability matrix — authoritative source for the
63075
63513
  * compose-form field gating. */
63076
63514
  supports: NotifierSupportsSchema
@@ -63162,11 +63600,9 @@ onSent: { data: object({
63162
63600
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
63163
63601
  * for: water flow, distance, weight, frequency, signal strength, …).
63164
63602
  *
63165
- * The `unit` and `precision` fields carry the live display metadata that
63166
- * the upstream source provides with each state push (HA
63167
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
63168
- * The UI reads them directly from the slice — they are the single source
63169
- * 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).
63170
63606
  *
63171
63607
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
63172
63608
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -63175,15 +63611,8 @@ onSent: { data: object({
63175
63611
  * numeric values land here.
63176
63612
  */
63177
63613
  var NumericSensorStatusSchema = object({
63178
- value: number(),
63179
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
63180
- * Populated live from the upstream source on each state push.
63181
- * Absent for unitless sensors. */
63182
- unit: string().optional(),
63183
- /** Suggested decimal places for numeric display.
63184
- * Populated live from the upstream source when provided.
63185
- * Falls back to auto-formatting when absent. */
63186
- 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")),
63187
63616
  /** Ms epoch when the slice was last updated. */
63188
63617
  lastFetchedAt: number()
63189
63618
  });
@@ -63207,11 +63636,11 @@ var numericSensorCapability = {
63207
63636
  */
63208
63637
  durability: "restored",
63209
63638
  /**
63210
- * 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.
63211
63641
  */
63212
63642
  history: { fields: { value: {
63213
63643
  kind: "gauge",
63214
- unitFrom: "unit",
63215
63644
  minIntervalMs: 3e4
63216
63645
  } } },
63217
63646
  /** Clock fields: written, but excluded from the compare that decides
@@ -63549,23 +63978,26 @@ var PetFeederStatusSchema = object({
63549
63978
  /** Food currently in the bowl (grams). Null when the device has not
63550
63979
  * reported a reading yet. On dual-hopper models this is the combined
63551
63980
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63552
- foodLevel: number().nullable(),
63981
+ foodLevel: number().nullable().meta(readingMeta("none")),
63553
63982
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63554
63983
  * single-hopper models. */
63555
- food1: number().nullable(),
63984
+ food1: number().nullable().meta(readingMeta("none")),
63556
63985
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63557
63986
  * single-hopper models. */
63558
- food2: number().nullable(),
63987
+ food2: number().nullable().meta(readingMeta("none")),
63559
63988
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63560
63989
  * (`device_class: problem`, on = low). True when the bowl is empty /
63561
63990
  * below the feeder's low threshold. */
63562
63991
  lowFood: boolean(),
63563
63992
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63564
63993
  * device has no battery reading. */
63565
- batteryPower: number().min(0).max(100).nullable(),
63566
- /** Days of desiccant life remaining. Null when the model has no
63567
- * desiccant sensor. */
63568
- 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")),
63569
64001
  /** True while a feed is in progress. */
63570
64002
  feeding: boolean(),
63571
64003
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63576,7 +64008,7 @@ var PetFeederStatusSchema = object({
63576
64008
  * with `errorCode` for consumers that want the raw integer. */
63577
64009
  error: string().nullable(),
63578
64010
  /** Raw device error code (0 / null = no error). */
63579
- errorCode: number().nullable(),
64011
+ errorCode: number().nullable().meta(readingMeta("none")),
63580
64012
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63581
64013
  isDualHopper: boolean(),
63582
64014
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63586,7 +64018,7 @@ var PetFeederStatusSchema = object({
63586
64018
  /** Play a chime when dispensing. */
63587
64019
  feedSound: boolean(),
63588
64020
  /** Speaker / prompt volume level (device-scaled integer). */
63589
- volume: number(),
64021
+ volume: number().meta(readingMeta("none")),
63590
64022
  /** Ms epoch when the slice was last refreshed from the cloud. */
63591
64023
  lastFetchedAt: number()
63592
64024
  });
@@ -63705,7 +64137,7 @@ var petFeederCapability = {
63705
64137
  },
63706
64138
  desiccantLeftDays: {
63707
64139
  kind: "gauge",
63708
- unit: "days"
64140
+ unit: "s"
63709
64141
  }
63710
64142
  } }
63711
64143
  };
@@ -64106,36 +64538,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
64106
64538
  */
64107
64539
  var PowerMeterStatusSchema = object({
64108
64540
  /** Instantaneous power draw in watts. */
64109
- watts: number().optional().meta({
64110
- unit: "W",
64111
- label: "Power"
64112
- }),
64541
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
64113
64542
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
64114
- kwhTotal: number().optional().meta({
64115
- unit: "kWh",
64116
- label: "Energy"
64117
- }),
64543
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
64118
64544
  /** Voltage in volts. */
64119
- volts: number().optional(),
64545
+ volts: number().optional().meta(readingMeta("voltage")),
64120
64546
  /** Current in amperes. */
64121
- amps: number().optional(),
64122
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
64123
- va: number().optional(),
64547
+ amps: number().optional().meta(readingMeta("current")),
64548
+ /** Apparent power in volt-amperes. */
64549
+ va: number().optional().meta(readingMeta("apparent-power")),
64124
64550
  /** Power factor (0..1) if reported. */
64125
- powerFactor: number().min(0).max(1).optional(),
64551
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
64126
64552
  /** Ms epoch when the slice was last updated. */
64127
- lastFetchedAt: number(),
64128
- /** Live display unit of the single metric this slice carries (e.g. HA
64129
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
64130
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
64131
- * unit per slice is unambiguous. The UI prefers this over the role's
64132
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
64133
- unit: string().optional(),
64134
- /** Suggested decimal places for numeric display.
64135
- * Populated live from the upstream source when provided (e.g. HA
64136
- * `attributes.suggested_display_precision`). Falls back to
64137
- * auto-formatting when absent. */
64138
- precision: number().int().min(0).max(10).optional()
64553
+ lastFetchedAt: number()
64139
64554
  });
64140
64555
  var powerMeterCapability = {
64141
64556
  name: "power-meter",
@@ -64199,12 +64614,7 @@ var powerMeterCapability = {
64199
64614
  } },
64200
64615
  /** Clock fields: written, but excluded from the compare that decides
64201
64616
  * whether persisting is worth a SQLite commit. */
64202
- volatileStateFields: ["lastFetchedAt"],
64203
- customize: {
64204
- version: 1,
64205
- advanced: "derive",
64206
- targets: []
64207
- }
64617
+ volatileStateFields: ["lastFetchedAt"]
64208
64618
  };
64209
64619
  /**
64210
64620
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -64224,11 +64634,11 @@ var powerMeterCapability = {
64224
64634
  */
64225
64635
  var GpsLocationSchema = object({
64226
64636
  /** Latitude in decimal degrees, -90..90. */
64227
- latitude: number().min(-90).max(90),
64637
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
64228
64638
  /** Longitude in decimal degrees, -180..180. */
64229
- longitude: number().min(-180).max(180),
64639
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
64230
64640
  /** Reported accuracy in meters (lower = better). */
64231
- accuracyMeters: number().nonnegative()
64641
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
64232
64642
  });
64233
64643
  var PresenceStatusSchema = object({
64234
64644
  /** `home` / `not_home` / any user-defined zone name. */
@@ -64240,7 +64650,7 @@ var PresenceStatusSchema = object({
64240
64650
  gps: GpsLocationSchema.nullable(),
64241
64651
  /** Optional battery level of the tracking device (0..100). Null
64242
64652
  * when not reported. */
64243
- batteryPercent: number().min(0).max(100).nullable(),
64653
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64244
64654
  /** Ms epoch when the slice was last updated. */
64245
64655
  lastChangedAt: number()
64246
64656
  });
@@ -64290,19 +64700,10 @@ var presenceCapability = {
64290
64700
  * `sourceInfo`.
64291
64701
  */
64292
64702
  var PressureSensorStatusSchema = object({
64293
- /** Current pressure in hPa. */
64294
- hpa: number(),
64703
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64704
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
64295
64705
  /** Ms epoch when the slice was last updated. */
64296
- lastFetchedAt: number(),
64297
- /** Live display unit from the upstream source (e.g. HA
64298
- * `attributes.unit_of_measurement`). The UI prefers this over the
64299
- * role's canonical unit. Absent → fall back to the canonical unit. */
64300
- unit: string().optional(),
64301
- /** Suggested decimal places for numeric display.
64302
- * Populated live from the upstream source when provided (e.g. HA
64303
- * `attributes.suggested_display_precision`). Falls back to
64304
- * auto-formatting when absent. */
64305
- precision: number().int().min(0).max(10).optional()
64706
+ lastFetchedAt: number()
64306
64707
  });
64307
64708
  var pressureSensorCapability = {
64308
64709
  name: "pressure-sensor",
@@ -64828,8 +65229,8 @@ var ptzCapability = {
64828
65229
  };
64829
65230
  var PtzAutotrackSettingsSchema = object({
64830
65231
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64831
- stopDelaySeconds: number().int().min(0).max(300),
64832
- 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"))
64833
65234
  });
64834
65235
  /**
64835
65236
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64983,7 +65384,7 @@ var RunningStateStatusSchema = object({
64983
65384
  /** null = unknown — never coerced to false (D393). */
64984
65385
  running: boolean().nullable(),
64985
65386
  /** Epoch ms the current boolean began; null while `running` is null. */
64986
- since: number().nullable(),
65387
+ since: number().nullable().meta(readingMeta("none")),
64987
65388
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64988
65389
  phase: string().nullable(),
64989
65390
  /** null until a reading has been published (D393). */
@@ -64998,7 +65399,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64998
65399
  /** Generic Customize target for the running boolean (D936). */
64999
65400
  var runningCustomize = {
65000
65401
  version: 1,
65001
- advanced: "derive",
65002
65402
  targets: [{
65003
65403
  id: "running",
65004
65404
  cardinality: "scalar",
@@ -65018,7 +65418,7 @@ var runningCustomize = {
65018
65418
  DeviceType.Thermostat,
65019
65419
  DeviceType.PetFeeder
65020
65420
  ],
65021
- apply: "add-or-replace",
65421
+ apply: "add",
65022
65422
  path: "running",
65023
65423
  sources: [
65024
65424
  {
@@ -65034,6 +65434,7 @@ var runningCustomize = {
65034
65434
  requiredUnit: "W",
65035
65435
  transforms: [{
65036
65436
  kind: "threshold",
65437
+ comparisons: ["gte", "lte"],
65037
65438
  comparison: "gte",
65038
65439
  defaultThreshold: 5,
65039
65440
  defaultStartDwellMs: RUNNING_POWER_START_DELAY_MS,
@@ -66285,9 +66686,9 @@ var RecordTriggerSchema = _enum([
66285
66686
  */
66286
66687
  var RecordWindowSchema = object({
66287
66688
  trigger: RecordTriggerSchema,
66288
- day: number().int().min(0).max(6),
66289
- startMinute: number().int().min(0).max(1439),
66290
- 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"))
66291
66692
  });
66292
66693
  /** Status of one physical volume, as the camera itself describes it. */
66293
66694
  var OnboardStorageVolumeSchema = object({
@@ -66363,10 +66764,10 @@ var RecordingOnboardStatusSchema = object({
66363
66764
  /** Master "record to the card at all" switch. */
66364
66765
  enabled: boolean().nullable(),
66365
66766
  overwriteWhenFull: boolean().nullable(),
66366
- preRecordSec: number().nullable(),
66367
- postRecordSec: number().nullable(),
66767
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66768
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
66368
66769
  /** Length of one recorded file, in minutes. */
66369
- segmentMinutes: number().nullable(),
66770
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
66370
66771
  /** The primary track's weekly windows, flattened. */
66371
66772
  windows: array(RecordWindowSchema),
66372
66773
  /**
@@ -66382,7 +66783,7 @@ var RecordingOnboardStatusSchema = object({
66382
66783
  * provider reporting a non-zero count also reports the schedule as not
66383
66784
  * writable.
66384
66785
  */
66385
- unreadableWindows: number(),
66786
+ unreadableWindows: number().meta(readingMeta("count")),
66386
66787
  /**
66387
66788
  * The camera is scheduled to record and has NO usable storage.
66388
66789
  *
@@ -67329,7 +67730,7 @@ var CamStreamDescriptorSchema = object({
67329
67730
  url: string().optional(),
67330
67731
  codec: string().optional(),
67331
67732
  resolution: CamStreamResolutionSchema.optional(),
67332
- fps: number().positive().optional(),
67733
+ fps: number().positive().optional().meta(readingMeta("rate")),
67333
67734
  label: string().optional(),
67334
67735
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
67335
67736
  deviceFeatures: array(string()).optional(),
@@ -67416,18 +67817,18 @@ var StreamProfileSchema = _enum([
67416
67817
  ]);
67417
67818
  /** The current encoding config of one stream profile. */
67418
67819
  var StreamProfileConfigSchema = object({
67419
- width: number(),
67420
- height: number(),
67820
+ width: number().meta(readingMeta("none")),
67821
+ height: number().meta(readingMeta("none")),
67421
67822
  codec: _enum(["h264", "h265"]),
67422
- framerate: number(),
67423
- bitrate: number(),
67823
+ framerate: number().meta(readingMeta("rate")),
67824
+ bitrate: number().meta(readingMeta("bitrate")),
67424
67825
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
67425
67826
  encoderProfile: _enum([
67426
67827
  "high",
67427
67828
  "main",
67428
67829
  "baseline"
67429
67830
  ]).optional(),
67430
- gop: number().optional(),
67831
+ gop: number().optional().meta(readingMeta("none")),
67431
67832
  /**
67432
67833
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
67433
67834
  *
@@ -67846,12 +68247,7 @@ var switchCapability = {
67846
68247
  label: "On",
67847
68248
  kind: "boolean"
67848
68249
  }
67849
- }] },
67850
- customize: {
67851
- version: 1,
67852
- advanced: "derive",
67853
- targets: []
67854
- }
68250
+ }] }
67855
68251
  };
67856
68252
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
67857
68253
  function hasControlCharacter(value) {
@@ -68434,35 +68830,21 @@ var tamperCapability = {
68434
68830
  * entries with `device_class: temperature` and any future native
68435
68831
  * thermometer.
68436
68832
  *
68437
- * Unit normalisation: providers convert to Celsius before storing.
68438
- * The slice value is always Celsius so cross-cap aggregators
68439
- * (climate-control's `currentTemp`, energy analytics) can compose
68440
- * without per-source unit fixups. The canonical display unit (`°C`) is
68441
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
68442
- * `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.
68443
68835
  *
68444
68836
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
68445
68837
  * frozen feed (provider hung) distinct from a "temperature hasn't
68446
68838
  * changed" steady state.
68447
68839
  */
68448
68840
  var TemperatureSensorStatusSchema = object({
68449
- /** Current temperature in Celsius. */
68450
- celsius: number().meta({
68451
- unit: "°C",
68841
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
68842
+ celsius: number().nullable().meta(readingMeta("temperature", {
68843
+ precision: 1,
68452
68844
  label: "Temperature"
68453
- }),
68845
+ })),
68454
68846
  /** Ms epoch when the slice was last updated (push or poll). */
68455
- lastFetchedAt: number(),
68456
- /** Live display unit from the upstream source (e.g. HA
68457
- * `attributes.unit_of_measurement`). The UI prefers this over the
68458
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
68459
- * Absent → the UI falls back to the role's canonical unit. */
68460
- unit: string().optional(),
68461
- /** Suggested decimal places for numeric display.
68462
- * Populated live from the upstream source when provided (e.g. HA
68463
- * `attributes.suggested_display_precision`). Falls back to
68464
- * auto-formatting when absent. */
68465
- precision: number().int().min(0).max(10).optional()
68847
+ lastFetchedAt: number()
68466
68848
  });
68467
68849
  var temperatureSensorCapability = {
68468
68850
  name: "temperature-sensor",
@@ -68493,12 +68875,7 @@ var temperatureSensorCapability = {
68493
68875
  } } },
68494
68876
  /** Clock fields: written, but excluded from the compare that decides
68495
68877
  * whether persisting is worth a SQLite commit. */
68496
- volatileStateFields: ["lastFetchedAt"],
68497
- customize: {
68498
- version: 1,
68499
- advanced: "derive",
68500
- targets: []
68501
- }
68878
+ volatileStateFields: ["lastFetchedAt"]
68502
68879
  };
68503
68880
  /**
68504
68881
  * toast — system-scoped singleton capability that streams toast
@@ -68781,16 +69158,16 @@ var VacuumControlStatusSchema = object({
68781
69158
  ]),
68782
69159
  /** 0..100 battery percentage. Null when the device has no battery
68783
69160
  * reading. */
68784
- batteryLevel: number().min(0).max(100).nullable(),
69161
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68785
69162
  /** Current fan-speed token (provider-verbatim). Null when unknown or
68786
69163
  * the vacuum has no speed control. */
68787
69164
  fanSpeed: string().nullable(),
68788
69165
  /** Speed tokens the hardware accepts — drives the UI selector. */
68789
69166
  availableFanSpeeds: array(string()),
68790
69167
  /** 0..100 cleaning-completion percentage of the current task, or null. */
68791
- progressPercent: number().min(0).max(100).nullable(),
69168
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68792
69169
  /** Current error code (0 / null = no error). */
68793
- errorCode: number().nullable(),
69170
+ errorCode: number().nullable().meta(readingMeta("none")),
68794
69171
  /** Human label for {@link errorCode}, or null when none / undecodable. */
68795
69172
  errorLabel: string().nullable(),
68796
69173
  /** Ms epoch when the slice was last updated. */
@@ -68868,7 +69245,7 @@ var ValveStatusSchema = object({
68868
69245
  ]),
68869
69246
  /** 0 = fully closed, 100 = fully open. Null when the device has no
68870
69247
  * intermediate position surface. */
68871
- position: number().min(0).max(100).nullable(),
69248
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
68872
69249
  /** Ms epoch when the slice was last updated. */
68873
69250
  lastChangedAt: number()
68874
69251
  });
@@ -68976,9 +69353,9 @@ var vibrationCapability = {
68976
69353
  */
68977
69354
  var WaterHeaterStatusSchema = object({
68978
69355
  /** Current measured temperature. Null when not reported. */
68979
- currentTemp: number().nullable(),
69356
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68980
69357
  /** Target temperature setpoint. Null when no setpoint surface. */
68981
- targetTemp: number().nullable(),
69358
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68982
69359
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68983
69360
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68984
69361
  * device reports an unknown state. */
@@ -68989,9 +69366,9 @@ var WaterHeaterStatusSchema = object({
68989
69366
  * has no away surface. */
68990
69367
  away: boolean().nullable(),
68991
69368
  /** HA `min_temp` attribute. Null when not reported. */
68992
- minTemp: number().nullable(),
69369
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68993
69370
  /** HA `max_temp` attribute. Null when not reported. */
68994
- maxTemp: number().nullable(),
69371
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68995
69372
  /** Ms epoch when the slice was last updated. */
68996
69373
  lastChangedAt: number()
68997
69374
  });
@@ -69076,25 +69453,18 @@ var waterHeaterCapability = {
69076
69453
  * Forecast deferred — current conditions only for v1.
69077
69454
  */
69078
69455
  var WeatherStatusSchema = object({
69079
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
69080
- * when no condition has been reported yet. */
69456
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
69081
69457
  condition: string().nullable(),
69082
- /** Current temperature in the reported unit. Null when not provided. */
69083
- temperature: number().nullable(),
69084
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
69085
- temperatureUnit: string().nullable(),
69458
+ /** Current temperature in Celsius. Null when not provided or refused. */
69459
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
69086
69460
  /** Relative humidity (0..100). Null when not provided. */
69087
- humidity: number().min(0).max(100).nullable(),
69088
- /** Barometric pressure in the reported unit. Null when not provided. */
69089
- pressure: number().nullable(),
69090
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
69091
- pressureUnit: string().nullable(),
69092
- /** Wind speed in the reported unit. Null when not provided. */
69093
- windSpeed: number().nullable(),
69094
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
69095
- 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")),
69096
69466
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
69097
- windBearing: number().nullable(),
69467
+ windBearing: number().nullable().meta(readingMeta("angle")),
69098
69468
  /** Ms epoch when the slice was last updated. */
69099
69469
  lastFetchedAt: number()
69100
69470
  });
@@ -69122,12 +69492,12 @@ var weatherCapability = {
69122
69492
  */
69123
69493
  durability: "session",
69124
69494
  /**
69125
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
69495
+ * History (D850): recorded series. Numbers are the quantity base (D944).
69126
69496
  */
69127
69497
  history: { fields: {
69128
69498
  temperature: {
69129
69499
  kind: "gauge",
69130
- unitFrom: "temperatureUnit",
69500
+ unit: "°C",
69131
69501
  deadband: .2
69132
69502
  },
69133
69503
  humidity: {
@@ -69137,12 +69507,12 @@ var weatherCapability = {
69137
69507
  },
69138
69508
  pressure: {
69139
69509
  kind: "gauge",
69140
- unitFrom: "pressureUnit",
69510
+ unit: "hPa",
69141
69511
  deadband: .5
69142
69512
  },
69143
69513
  windSpeed: {
69144
69514
  kind: "gauge",
69145
- unitFrom: "windSpeedUnit",
69515
+ unit: "m/s",
69146
69516
  deadband: .5
69147
69517
  }
69148
69518
  } }
@@ -70506,6 +70876,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
70506
70876
  featureProbe: featureProbeCapability,
70507
70877
  flood: floodCapability,
70508
70878
  gas: gasCapability,
70879
+ groupCommands: groupCommandsCapability,
70880
+ groupStatus: groupStatusCapability,
70509
70881
  heatingValve: heatingValveCapability,
70510
70882
  humidifier: humidifierCapability,
70511
70883
  humiditySensor: humiditySensorCapability,
@@ -71886,6 +72258,13 @@ Object.freeze({
71886
72258
  access: "view",
71887
72259
  role: "admin"
71888
72260
  },
72261
+ "addons.getPackageDocs": {
72262
+ capName: "addons",
72263
+ capScope: "system",
72264
+ addonId: null,
72265
+ access: "view",
72266
+ role: "admin"
72267
+ },
71889
72268
  "addons.getVersions": {
71890
72269
  capName: "addons",
71891
72270
  capScope: "system",
@@ -74994,6 +75373,20 @@ Object.freeze({
74994
75373
  access: "view",
74995
75374
  role: "viewer"
74996
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
+ },
74997
75390
  "heatingValve.getStatus": {
74998
75391
  capName: "heating-valve",
74999
75392
  capScope: "device",
@@ -84816,6 +85209,37 @@ var SENSOR_CAP_VOCABULARY = {
84816
85209
  icon: "flame"
84817
85210
  }]
84818
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
+ },
84819
85243
  "humidity-sensor": {
84820
85244
  scope: "device",
84821
85245
  noun: t("Humidity", "Umidità"),
@@ -84849,7 +85273,6 @@ var SENSOR_CAP_VOCABULARY = {
84849
85273
  field: "value",
84850
85274
  quantity: t("the reading", "il valore"),
84851
85275
  label: t("Reading", "Valore"),
84852
- unitField: "unit",
84853
85276
  icon: "gauge"
84854
85277
  }]
84855
85278
  },
@@ -84985,7 +85408,7 @@ var SENSOR_CAP_VOCABULARY = {
84985
85408
  field: "temperature",
84986
85409
  quantity: t("the outdoor temperature", "la temperatura esterna"),
84987
85410
  label: t("Temperature", "Temperatura"),
84988
- unitField: "temperatureUnit",
85411
+ unit: "°C",
84989
85412
  icon: "thermometer"
84990
85413
  },
84991
85414
  {
@@ -85001,7 +85424,7 @@ var SENSOR_CAP_VOCABULARY = {
85001
85424
  field: "windSpeed",
85002
85425
  quantity: t("the wind speed", "la velocità del vento"),
85003
85426
  label: t("Wind", "Vento"),
85004
- unitField: "windSpeedUnit",
85427
+ unit: "m/s",
85005
85428
  icon: "wind"
85006
85429
  },
85007
85430
  {
@@ -85009,7 +85432,7 @@ var SENSOR_CAP_VOCABULARY = {
85009
85432
  field: "pressure",
85010
85433
  quantity: t("the air pressure", "la pressione atmosferica"),
85011
85434
  label: t("Pressure", "Pressione"),
85012
- unitField: "pressureUnit",
85435
+ unit: "hPa",
85013
85436
  icon: "gauge"
85014
85437
  }
85015
85438
  ]
@@ -86488,6 +86911,16 @@ Object.freeze({
86488
86911
  form: "single",
86489
86912
  optional: false
86490
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
+ }],
86491
86924
  "heatingValve.getStatus": [{
86492
86925
  name: "deviceId",
86493
86926
  form: "single",
@@ -88352,6 +88785,11 @@ var PulseOverrideDraftSchema = object({
88352
88785
  targetId: string().min(1),
88353
88786
  enabled: boolean(),
88354
88787
  debounceMs: number().nonnegative(),
88788
+ edges: array(_enum([
88789
+ "rising",
88790
+ "button",
88791
+ "native-event"
88792
+ ])).min(1).optional(),
88355
88793
  sources: array(OverrideSourceSchema).min(1)
88356
88794
  });
88357
88795
  var SpanSourceDraftSchema = OverrideSourceSchema.extend({ mode: _enum([
@@ -88467,6 +88905,8 @@ new Map(Object.entries({
88467
88905
  "detection-blind": "device-events",
88468
88906
  "motion-noisy": "device-events",
88469
88907
  "motion-calm": "device-events",
88908
+ "package-llm-silent": "device-events",
88909
+ "package-llm-answered": "device-events",
88470
88910
  "recording-stalled": "device-events",
88471
88911
  "recording-healthy": "device-events",
88472
88912
  "recording-degraded": "device-events",
@@ -88550,6 +88990,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
88550
88990
  label: "Motion without a subject",
88551
88991
  kinds: ["motion-noisy", "motion-calm"]
88552
88992
  },
88993
+ {
88994
+ id: "package-llm",
88995
+ family: "device-events",
88996
+ label: "Package confirm",
88997
+ kinds: ["package-llm-silent", "package-llm-answered"]
88998
+ },
88553
88999
  {
88554
89000
  id: "recording",
88555
89001
  family: "device-events",
@@ -88731,6 +89177,8 @@ var DEVICE_EVENTS = [
88731
89177
  "detection-blind",
88732
89178
  "motion-noisy",
88733
89179
  "motion-calm",
89180
+ "package-llm-silent",
89181
+ "package-llm-answered",
88734
89182
  "recording-stalled",
88735
89183
  "recording-healthy",
88736
89184
  "recording-degraded",
@@ -88999,6 +89447,15 @@ new Map([
88999
89447
  snapshotLabel: "Cameras with constant motion now",
89000
89448
  snapshotSection: null
89001
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
+ },
89002
89459
  {
89003
89460
  id: "recording-health",
89004
89461
  family: "device-events",