@camstack/addon-terminal 0.1.153 → 0.1.154

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/addon.js CHANGED
@@ -31,7 +31,7 @@ let node_module = require("node:module");
31
31
  let sharp = require("sharp");
32
32
  sharp = __toESM(sharp);
33
33
  let node_http = require("node:http");
34
- //#region ../types/dist/event-category-DHXxB8t9.mjs
34
+ //#region ../types/dist/event-category-CT-hz5Rb.mjs
35
35
  var EventCategory = /* @__PURE__ */ function(EventCategory) {
36
36
  EventCategory["SystemBoot"] = "system.boot";
37
37
  EventCategory["SystemAddonsReady"] = "system.addons-ready";
@@ -357,6 +357,14 @@ var EventCategory = /* @__PURE__ */ function(EventCategory) {
357
357
  /** The recovery edge of {@link PipelineMotionNoisy}: the motion fell back, or a detection arrived. */
358
358
  EventCategory["PipelineMotionCalm"] = "pipeline.motion-calm";
359
359
  /**
360
+ * A camera's package LLM confirm has failed closed three times in a row
361
+ * (D931, 2026-10-26). One edge per camera until
362
+ * {@link PipelinePackageLlmAnswered}.
363
+ */
364
+ EventCategory["PipelinePackageLlmSilent"] = "pipeline.package-llm-silent";
365
+ /** The recovery edge of {@link PipelinePackageLlmSilent}: a confirm returned yes or no. */
366
+ EventCategory["PipelinePackageLlmAnswered"] = "pipeline.package-llm-answered";
367
+ /**
360
368
  * Per-camera pipeline config was mutated by the orchestrator
361
369
  * (a settings change via `setAgentInferenceDevices` /
362
370
  * `setCameraStepToggle` / `setCameraStepSettings`). Orchestrator
@@ -5466,7 +5474,7 @@ var ZodIssueCode = {
5466
5474
  var ZodFirstPartyTypeKind;
5467
5475
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5468
5476
  //#endregion
5469
- //#region ../types/dist/sleep-BErAxhul.mjs
5477
+ //#region ../types/dist/sleep-bykKrTgu.mjs
5470
5478
  /**
5471
5479
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5472
5480
  * window to float samples (D455).
@@ -8020,6 +8028,99 @@ var RecordKindDeclarationSchema = object({
8020
8028
  }).strict();
8021
8029
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8022
8030
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8031
+ var READING_QUANTITIES = [
8032
+ "temperature",
8033
+ "pressure",
8034
+ "speed",
8035
+ "precipitation",
8036
+ "precipitation-rate",
8037
+ "length",
8038
+ "area",
8039
+ "illuminance",
8040
+ "irradiance",
8041
+ "power",
8042
+ "energy",
8043
+ "voltage",
8044
+ "current",
8045
+ "apparent-power",
8046
+ "humidity",
8047
+ "percentage",
8048
+ "concentration-volume",
8049
+ "concentration-mass",
8050
+ "concentration-ppb",
8051
+ "volume",
8052
+ "duration",
8053
+ "duration-ms",
8054
+ "information",
8055
+ "sound-level",
8056
+ "signal-level",
8057
+ "colour-temperature",
8058
+ "angle",
8059
+ "bitrate",
8060
+ "rate",
8061
+ "mass",
8062
+ "index",
8063
+ "ratio",
8064
+ "count",
8065
+ "none",
8066
+ "device"
8067
+ ];
8068
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8069
+ new Set(READING_QUANTITIES);
8070
+ /**
8071
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8072
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8073
+ */
8074
+ var QUANTITY_BASE = {
8075
+ temperature: "°C",
8076
+ pressure: "hPa",
8077
+ speed: "m/s",
8078
+ precipitation: "mm",
8079
+ "precipitation-rate": "mm/h",
8080
+ length: "m",
8081
+ area: "m²",
8082
+ illuminance: "lx",
8083
+ irradiance: "W/m²",
8084
+ power: "W",
8085
+ energy: "kWh",
8086
+ voltage: "V",
8087
+ current: "A",
8088
+ "apparent-power": "VA",
8089
+ humidity: "%",
8090
+ percentage: "%",
8091
+ "concentration-volume": "ppm",
8092
+ "concentration-mass": "µg/m³",
8093
+ "concentration-ppb": "ppb",
8094
+ volume: "L",
8095
+ duration: "s",
8096
+ "duration-ms": "ms",
8097
+ information: "B",
8098
+ "sound-level": "dBFS",
8099
+ "signal-level": "dBm",
8100
+ "colour-temperature": "mired",
8101
+ angle: "°",
8102
+ bitrate: "kbps",
8103
+ rate: "fps",
8104
+ mass: "kg",
8105
+ index: "none",
8106
+ ratio: "none",
8107
+ count: "none",
8108
+ none: "none",
8109
+ device: "device"
8110
+ };
8111
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8112
+ function baseUnitOf(quantity) {
8113
+ const symbol = QUANTITY_BASE[quantity];
8114
+ if (symbol === "none" || symbol === "device") return void 0;
8115
+ return symbol;
8116
+ }
8117
+ function readingMeta(quantity, extras) {
8118
+ return {
8119
+ quantity,
8120
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8121
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8122
+ };
8123
+ }
8023
8124
  /** Shared Zod schemas used across streaming capabilities. */
8024
8125
  var CamProfileSchema = _enum([
8025
8126
  "high",
@@ -8047,8 +8148,8 @@ var CamStreamKindSchema = _enum([
8047
8148
  "derived"
8048
8149
  ]);
8049
8150
  var CamStreamResolutionSchema = object({
8050
- width: number().int().positive(),
8051
- height: number().int().positive()
8151
+ width: number().int().positive().meta(readingMeta("none")),
8152
+ height: number().int().positive().meta(readingMeta("none"))
8052
8153
  });
8053
8154
  var CameraStreamSchema = object({
8054
8155
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8625,6 +8726,8 @@ var CAP_NAMES = Object.freeze([
8625
8726
  "filesystem-browse",
8626
8727
  "flood",
8627
8728
  "gas",
8729
+ "group-commands",
8730
+ "group-status",
8628
8731
  "heating-valve",
8629
8732
  "humidifier",
8630
8733
  "humidity-sensor",
@@ -8754,14 +8857,6 @@ Object.freeze({
8754
8857
  {
8755
8858
  "path": "aqi",
8756
8859
  "valueType": "number"
8757
- },
8758
- {
8759
- "path": "unit",
8760
- "valueType": "string"
8761
- },
8762
- {
8763
- "path": "precision",
8764
- "valueType": "number"
8765
8860
  }
8766
8861
  ],
8767
8862
  "alarm-panel": [
@@ -8797,20 +8892,10 @@ Object.freeze({
8797
8892
  "valueType": "boolean"
8798
8893
  }
8799
8894
  ],
8800
- "ambient-light-sensor": [
8801
- {
8802
- "path": "lux",
8803
- "valueType": "number"
8804
- },
8805
- {
8806
- "path": "unit",
8807
- "valueType": "string"
8808
- },
8809
- {
8810
- "path": "precision",
8811
- "valueType": "number"
8812
- }
8813
- ],
8895
+ "ambient-light-sensor": [{
8896
+ "path": "lux",
8897
+ "valueType": "number"
8898
+ }],
8814
8899
  "audio-metrics": [
8815
8900
  {
8816
8901
  "path": "ts",
@@ -8890,6 +8975,10 @@ Object.freeze({
8890
8975
  "path": "lastUpdated",
8891
8976
  "valueType": "number"
8892
8977
  },
8978
+ {
8979
+ "path": "volts",
8980
+ "valueType": "number"
8981
+ },
8893
8982
  {
8894
8983
  "path": "lastContactAt",
8895
8984
  "valueType": "number"
@@ -9132,10 +9221,6 @@ Object.freeze({
9132
9221
  "datetime"
9133
9222
  ]
9134
9223
  },
9135
- {
9136
- "path": "unit",
9137
- "valueType": "string"
9138
- },
9139
9224
  {
9140
9225
  "path": "min",
9141
9226
  "valueType": "number"
@@ -9147,19 +9232,6 @@ Object.freeze({
9147
9232
  {
9148
9233
  "path": "step",
9149
9234
  "valueType": "number"
9150
- },
9151
- {
9152
- "path": "precision",
9153
- "valueType": "number"
9154
- },
9155
- {
9156
- "path": "format",
9157
- "valueType": "enum",
9158
- "enumValues": [
9159
- "date",
9160
- "time",
9161
- "datetime"
9162
- ]
9163
9235
  }
9164
9236
  ],
9165
9237
  "cover": [
@@ -9400,13 +9472,8 @@ Object.freeze({
9400
9472
  "path": "value",
9401
9473
  "valueType": "string"
9402
9474
  }, {
9403
- "path": "format",
9404
- "valueType": "enum",
9405
- "enumValues": [
9406
- "date",
9407
- "time",
9408
- "datetime"
9409
- ]
9475
+ "path": "timestamp",
9476
+ "valueType": "boolean"
9410
9477
  }],
9411
9478
  "event-emitter": [
9412
9479
  {
@@ -9479,6 +9546,20 @@ Object.freeze({
9479
9546
  "path": "detected",
9480
9547
  "valueType": "boolean"
9481
9548
  }],
9549
+ "group-status": [
9550
+ {
9551
+ "path": "included",
9552
+ "valueType": "number"
9553
+ },
9554
+ {
9555
+ "path": "excluded",
9556
+ "valueType": "number"
9557
+ },
9558
+ {
9559
+ "path": "total",
9560
+ "valueType": "number"
9561
+ }
9562
+ ],
9482
9563
  "heating-valve": [{
9483
9564
  "path": "position",
9484
9565
  "valueType": "number"
@@ -9513,20 +9594,10 @@ Object.freeze({
9513
9594
  "valueType": "number"
9514
9595
  }
9515
9596
  ],
9516
- "humidity-sensor": [
9517
- {
9518
- "path": "percent",
9519
- "valueType": "number"
9520
- },
9521
- {
9522
- "path": "unit",
9523
- "valueType": "string"
9524
- },
9525
- {
9526
- "path": "precision",
9527
- "valueType": "number"
9528
- }
9529
- ],
9597
+ "humidity-sensor": [{
9598
+ "path": "percent",
9599
+ "valueType": "number"
9600
+ }],
9530
9601
  "image": [{
9531
9602
  "path": "url",
9532
9603
  "valueType": "string"
@@ -9865,20 +9936,10 @@ Object.freeze({
9865
9936
  "valueType": "boolean"
9866
9937
  }
9867
9938
  ],
9868
- "numeric-sensor": [
9869
- {
9870
- "path": "value",
9871
- "valueType": "number"
9872
- },
9873
- {
9874
- "path": "unit",
9875
- "valueType": "string"
9876
- },
9877
- {
9878
- "path": "precision",
9879
- "valueType": "number"
9880
- }
9881
- ],
9939
+ "numeric-sensor": [{
9940
+ "path": "value",
9941
+ "valueType": "number"
9942
+ }],
9882
9943
  "pet-feeder": [
9883
9944
  {
9884
9945
  "path": "foodLevel",
@@ -9970,14 +10031,6 @@ Object.freeze({
9970
10031
  {
9971
10032
  "path": "powerFactor",
9972
10033
  "valueType": "number"
9973
- },
9974
- {
9975
- "path": "unit",
9976
- "valueType": "string"
9977
- },
9978
- {
9979
- "path": "precision",
9980
- "valueType": "number"
9981
10034
  }
9982
10035
  ],
9983
10036
  "presence": [
@@ -10006,20 +10059,10 @@ Object.freeze({
10006
10059
  "valueType": "number"
10007
10060
  }
10008
10061
  ],
10009
- "pressure-sensor": [
10010
- {
10011
- "path": "hpa",
10012
- "valueType": "number"
10013
- },
10014
- {
10015
- "path": "unit",
10016
- "valueType": "string"
10017
- },
10018
- {
10019
- "path": "precision",
10020
- "valueType": "number"
10021
- }
10022
- ],
10062
+ "pressure-sensor": [{
10063
+ "path": "hpa",
10064
+ "valueType": "number"
10065
+ }],
10023
10066
  "privacy-mask": [{
10024
10067
  "path": "enabled",
10025
10068
  "valueType": "boolean"
@@ -10330,20 +10373,10 @@ Object.freeze({
10330
10373
  "path": "tampered",
10331
10374
  "valueType": "boolean"
10332
10375
  }],
10333
- "temperature-sensor": [
10334
- {
10335
- "path": "celsius",
10336
- "valueType": "number"
10337
- },
10338
- {
10339
- "path": "unit",
10340
- "valueType": "string"
10341
- },
10342
- {
10343
- "path": "precision",
10344
- "valueType": "number"
10345
- }
10346
- ],
10376
+ "temperature-sensor": [{
10377
+ "path": "celsius",
10378
+ "valueType": "number"
10379
+ }],
10347
10380
  "update": [
10348
10381
  {
10349
10382
  "path": "currentVersion",
@@ -10454,10 +10487,6 @@ Object.freeze({
10454
10487
  "path": "temperature",
10455
10488
  "valueType": "number"
10456
10489
  },
10457
- {
10458
- "path": "temperatureUnit",
10459
- "valueType": "string"
10460
- },
10461
10490
  {
10462
10491
  "path": "humidity",
10463
10492
  "valueType": "number"
@@ -10466,18 +10495,10 @@ Object.freeze({
10466
10495
  "path": "pressure",
10467
10496
  "valueType": "number"
10468
10497
  },
10469
- {
10470
- "path": "pressureUnit",
10471
- "valueType": "string"
10472
- },
10473
10498
  {
10474
10499
  "path": "windSpeed",
10475
10500
  "valueType": "number"
10476
10501
  },
10477
- {
10478
- "path": "windSpeedUnit",
10479
- "valueType": "string"
10480
- },
10481
10502
  {
10482
10503
  "path": "windBearing",
10483
10504
  "valueType": "number"
@@ -10880,7 +10901,7 @@ function errMsg(err) {
10880
10901
  return String(err);
10881
10902
  }
10882
10903
  //#endregion
10883
- //#region ../types/dist/composition-kEl9mevu.mjs
10904
+ //#region ../types/dist/composition-OhpuespA.mjs
10884
10905
  /**
10885
10906
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10886
10907
  *
@@ -11635,15 +11656,74 @@ function validateExpressionSource(src) {
11635
11656
  }
11636
11657
  return null;
11637
11658
  }
11638
- /** A composed device's stableId is this prefix plus the block id. */
11639
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11640
- /** Longest snippet a `code` source may carry (slice 4). */
11641
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11659
+ /**
11660
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11661
+ * id. Split out so helper schemas can name it without a cycle through
11662
+ * `composition.ts` (D659).
11663
+ */
11642
11664
  var CompositionSourceRefSchema = object({
11643
11665
  /** The owning addon; `stableId` is unique only within it. */
11644
11666
  addonId: string().min(1),
11645
11667
  stableId: string().min(1)
11646
11668
  });
11669
+ var GROUP_TEMPLATE_IDS = [
11670
+ "temperature",
11671
+ "humidity",
11672
+ "pressure",
11673
+ "illuminance",
11674
+ "power",
11675
+ "energy",
11676
+ "battery",
11677
+ "numeric",
11678
+ "motion",
11679
+ "contact",
11680
+ "leak",
11681
+ "smoke",
11682
+ "occupancy",
11683
+ "binary",
11684
+ "switch",
11685
+ "light"
11686
+ ];
11687
+ var NUMERIC_AGGREGATES = [
11688
+ "mean",
11689
+ "median",
11690
+ "min",
11691
+ "max",
11692
+ "sum"
11693
+ ];
11694
+ var BINARY_AGGREGATES = ["any", "all"];
11695
+ /** How an actuator group decides it is on. Unknown members never count as off. */
11696
+ var ACTIVE_CRITERIA = ["at-least-one", "all"];
11697
+ var AGGREGATE_REDUCERS = [
11698
+ "mean",
11699
+ "median",
11700
+ "min",
11701
+ "max",
11702
+ "sum",
11703
+ "any",
11704
+ "all"
11705
+ ];
11706
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11707
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11708
+ var ActiveCriterionSchema = _enum(ACTIVE_CRITERIA);
11709
+ /** One member. The field is stored so occupancy can name motion or binary. */
11710
+ var HelperMemberSchema = object({
11711
+ source: CompositionSourceRefSchema,
11712
+ cap: string().min(1),
11713
+ fieldPath: string().min(1)
11714
+ });
11715
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11716
+ kind: literal("group"),
11717
+ templateId: GroupTemplateIdSchema,
11718
+ aggregate: AggregateReducerSchema,
11719
+ members: array(HelperMemberSchema).min(1).max(64),
11720
+ /** Actuator groups only. Readings leave this unset. */
11721
+ active: ActiveCriterionSchema.optional()
11722
+ })]);
11723
+ /** A composed device's stableId is this prefix plus the block id. */
11724
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11725
+ /** Longest snippet a `code` source may carry (slice 4). */
11726
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11647
11727
  /** One field of one capability of one source device. */
11648
11728
  var CompositionFieldReadSchema = object({
11649
11729
  source: CompositionSourceRefSchema,
@@ -11687,7 +11767,33 @@ var CompositionCodeSourceSchema = object({
11687
11767
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11688
11768
  CompositionFromSourceSchema,
11689
11769
  CompositionExpressionSourceSchema,
11690
- CompositionCodeSourceSchema
11770
+ CompositionCodeSourceSchema,
11771
+ object({
11772
+ kind: literal("aggregate"),
11773
+ reducer: _enum([
11774
+ "mean",
11775
+ "median",
11776
+ "min",
11777
+ "max",
11778
+ "sum",
11779
+ "any",
11780
+ "all"
11781
+ ]),
11782
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11783
+ /** `value` is the reading; the others are the member counts. */
11784
+ publish: _enum([
11785
+ "value",
11786
+ "included",
11787
+ "excluded",
11788
+ "total"
11789
+ ]),
11790
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11791
+ empty: _enum([
11792
+ "null",
11793
+ "omit",
11794
+ "offline"
11795
+ ])
11796
+ })
11691
11797
  ]);
11692
11798
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11693
11799
  kind: literal("forward"),
@@ -11733,7 +11839,13 @@ var CompositionSchema = object({
11733
11839
  kind: literal("existing"),
11734
11840
  device: CompositionSourceRefSchema
11735
11841
  })]),
11736
- features: array(CompositionFeatureSchema).min(1).max(16)
11842
+ features: array(CompositionFeatureSchema).min(1).max(16),
11843
+ /**
11844
+ * Present when the block is a helper template. Absent on a free
11845
+ * composition (the room example, device 4603). The editor reads this,
11846
+ * never the compiled features.
11847
+ */
11848
+ helper: HelperSpecSchema.optional()
11737
11849
  }).superRefine((composition, ctx) => {
11738
11850
  const seen = /* @__PURE__ */ new Set();
11739
11851
  composition.features.forEach((feature, index) => {
@@ -17973,7 +18085,11 @@ var CompositionProblemSchema = object({
17973
18085
  "item-field-reserved",
17974
18086
  "target-missing",
17975
18087
  "target-unreadable",
17976
- "not-replaceable"
18088
+ "not-replaceable",
18089
+ "native-capability-replace",
18090
+ "mixed-quantity",
18091
+ "helper-mismatch",
18092
+ "member-constraint"
17977
18093
  ]),
17978
18094
  path: string(),
17979
18095
  message: string()
@@ -17994,7 +18110,8 @@ var CompositionUnavailableModeSchema = _enum([
17994
18110
  "null",
17995
18111
  "offline",
17996
18112
  "unknown",
17997
- "release"
18113
+ "release",
18114
+ "omit"
17998
18115
  ]);
17999
18116
  /**
18000
18117
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18031,9 +18148,11 @@ var CompositionFieldPlanSchema = object({
18031
18148
  reads: array(CompositionFieldReadSchema)
18032
18149
  });
18033
18150
  /**
18034
- * What a feature does to its target (D663): `new` builds a capability on a new
18035
- * device; on an existing device it `add`s a capability the device lacks, or
18036
- * `replace`s named fields of one it has natively.
18151
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18152
+ * new device; on an existing device it `add`s a capability the device lacks.
18153
+ * Replacing a capability the device already serves is refused
18154
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18155
+ * wire for a claim the hub already holds.
18037
18156
  */
18038
18157
  var CompositionFeatureModeSchema = _enum([
18039
18158
  "new",
@@ -18206,6 +18325,275 @@ Object.freeze({
18206
18325
  result: ["number", "null"]
18207
18326
  }
18208
18327
  });
18328
+ /**
18329
+ * One catalog row per base reading a group helper can build (D944 §3).
18330
+ * Data, not a form default written into the block.
18331
+ */
18332
+ var NOW = {
18333
+ fieldPath: "lastFetchedAt",
18334
+ expr: "now"
18335
+ };
18336
+ var CHANGED = {
18337
+ fieldPath: "lastChangedAt",
18338
+ expr: "now"
18339
+ };
18340
+ function numeric(input) {
18341
+ return {
18342
+ ...input,
18343
+ family: "numeric",
18344
+ deviceType: DeviceType.Sensor,
18345
+ memberDeviceType: DeviceType.Sensor,
18346
+ commandCaps: [],
18347
+ baseUnit: baseUnitOf(input.quantity),
18348
+ aggregates: NUMERIC_AGGREGATES,
18349
+ accept: [{
18350
+ cap: input.outputCap,
18351
+ fieldPath: input.outputField
18352
+ }]
18353
+ };
18354
+ }
18355
+ function binary(input) {
18356
+ return {
18357
+ ...input,
18358
+ family: "binary",
18359
+ deviceType: DeviceType.Sensor,
18360
+ memberDeviceType: DeviceType.Sensor,
18361
+ commandCaps: [],
18362
+ quantity: "none",
18363
+ baseUnit: void 0,
18364
+ aggregates: BINARY_AGGREGATES,
18365
+ empty: "offline"
18366
+ };
18367
+ }
18368
+ var GROUP_TEMPLATES = [
18369
+ numeric({
18370
+ id: "temperature",
18371
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18372
+ role: DeviceRole.TemperatureSensor,
18373
+ outputCap: "temperature-sensor",
18374
+ outputField: "celsius",
18375
+ quantity: "temperature",
18376
+ defaultAggregate: "mean",
18377
+ empty: "null",
18378
+ companions: [NOW]
18379
+ }),
18380
+ numeric({
18381
+ id: "humidity",
18382
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18383
+ role: DeviceRole.HumiditySensor,
18384
+ outputCap: "humidity-sensor",
18385
+ outputField: "percent",
18386
+ quantity: "humidity",
18387
+ defaultAggregate: "mean",
18388
+ empty: "null",
18389
+ companions: [NOW]
18390
+ }),
18391
+ numeric({
18392
+ id: "pressure",
18393
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18394
+ role: DeviceRole.PressureSensor,
18395
+ outputCap: "pressure-sensor",
18396
+ outputField: "hpa",
18397
+ quantity: "pressure",
18398
+ defaultAggregate: "mean",
18399
+ empty: "null",
18400
+ companions: [NOW]
18401
+ }),
18402
+ numeric({
18403
+ id: "illuminance",
18404
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18405
+ role: DeviceRole.AmbientLightSensor,
18406
+ outputCap: "ambient-light-sensor",
18407
+ outputField: "lux",
18408
+ quantity: "illuminance",
18409
+ defaultAggregate: "mean",
18410
+ empty: "null",
18411
+ companions: [NOW]
18412
+ }),
18413
+ numeric({
18414
+ id: "power",
18415
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18416
+ role: DeviceRole.PowerSensor,
18417
+ outputCap: "power-meter",
18418
+ outputField: "watts",
18419
+ quantity: "power",
18420
+ defaultAggregate: "sum",
18421
+ empty: "omit",
18422
+ companions: [NOW]
18423
+ }),
18424
+ numeric({
18425
+ id: "energy",
18426
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18427
+ role: DeviceRole.EnergySensor,
18428
+ outputCap: "power-meter",
18429
+ outputField: "kwhTotal",
18430
+ quantity: "energy",
18431
+ defaultAggregate: "sum",
18432
+ empty: "omit",
18433
+ companions: [NOW]
18434
+ }),
18435
+ numeric({
18436
+ id: "battery",
18437
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18438
+ role: DeviceRole.BatterySensor,
18439
+ outputCap: "battery",
18440
+ outputField: "percentage",
18441
+ quantity: "percentage",
18442
+ defaultAggregate: "mean",
18443
+ empty: "null",
18444
+ companions: [
18445
+ {
18446
+ fieldPath: "charging",
18447
+ expr: "\"unknown\""
18448
+ },
18449
+ {
18450
+ fieldPath: "sleeping",
18451
+ expr: "false"
18452
+ },
18453
+ {
18454
+ fieldPath: "lastUpdated",
18455
+ expr: "now"
18456
+ }
18457
+ ]
18458
+ }),
18459
+ numeric({
18460
+ id: "numeric",
18461
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18462
+ role: DeviceRole.NumericSensor,
18463
+ outputCap: "numeric-sensor",
18464
+ outputField: "value",
18465
+ quantity: "device",
18466
+ defaultAggregate: "mean",
18467
+ empty: "null",
18468
+ companions: [NOW]
18469
+ }),
18470
+ binary({
18471
+ id: "motion",
18472
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18473
+ role: DeviceRole.MotionSensor,
18474
+ outputCap: "motion",
18475
+ outputField: "detected",
18476
+ defaultAggregate: "any",
18477
+ accept: [{
18478
+ cap: "motion",
18479
+ fieldPath: "detected"
18480
+ }],
18481
+ companions: [{
18482
+ fieldPath: "lastDetectedAt",
18483
+ expr: "null"
18484
+ }, {
18485
+ fieldPath: "autoClearAfterMs",
18486
+ expr: "null"
18487
+ }]
18488
+ }),
18489
+ binary({
18490
+ id: "contact",
18491
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18492
+ role: DeviceRole.ContactSensor,
18493
+ outputCap: "contact",
18494
+ outputField: "entryOpen",
18495
+ defaultAggregate: "any",
18496
+ accept: [{
18497
+ cap: "contact",
18498
+ fieldPath: "entryOpen"
18499
+ }],
18500
+ companions: [CHANGED]
18501
+ }),
18502
+ binary({
18503
+ id: "leak",
18504
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18505
+ role: DeviceRole.LeakSensor,
18506
+ outputCap: "flood",
18507
+ outputField: "flooded",
18508
+ defaultAggregate: "any",
18509
+ accept: [{
18510
+ cap: "flood",
18511
+ fieldPath: "flooded"
18512
+ }],
18513
+ companions: [CHANGED]
18514
+ }),
18515
+ binary({
18516
+ id: "smoke",
18517
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18518
+ role: DeviceRole.SmokeSensor,
18519
+ outputCap: "smoke",
18520
+ outputField: "detected",
18521
+ defaultAggregate: "any",
18522
+ accept: [{
18523
+ cap: "smoke",
18524
+ fieldPath: "detected"
18525
+ }],
18526
+ companions: [CHANGED]
18527
+ }),
18528
+ binary({
18529
+ id: "occupancy",
18530
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18531
+ role: DeviceRole.BinarySensor,
18532
+ outputCap: "binary",
18533
+ outputField: "on",
18534
+ defaultAggregate: "any",
18535
+ accept: [{
18536
+ cap: "motion",
18537
+ fieldPath: "detected"
18538
+ }, {
18539
+ cap: "binary",
18540
+ fieldPath: "on"
18541
+ }],
18542
+ companions: [CHANGED]
18543
+ }),
18544
+ binary({
18545
+ id: "binary",
18546
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18547
+ role: DeviceRole.BinarySensor,
18548
+ outputCap: "binary",
18549
+ outputField: "on",
18550
+ defaultAggregate: "any",
18551
+ accept: [{
18552
+ cap: "binary",
18553
+ fieldPath: "on"
18554
+ }],
18555
+ companions: [CHANGED]
18556
+ }),
18557
+ actuator({
18558
+ id: "switch",
18559
+ 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.",
18560
+ deviceType: DeviceType.Switch,
18561
+ outputCap: "switch",
18562
+ commandCaps: ["switch"]
18563
+ }),
18564
+ actuator({
18565
+ id: "light",
18566
+ 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.",
18567
+ deviceType: DeviceType.Light,
18568
+ outputCap: "switch",
18569
+ commandCaps: [
18570
+ "switch",
18571
+ "brightness",
18572
+ "color"
18573
+ ]
18574
+ })
18575
+ ];
18576
+ function actuator(input) {
18577
+ return {
18578
+ ...input,
18579
+ family: "actuator",
18580
+ role: void 0,
18581
+ memberDeviceType: input.deviceType,
18582
+ outputField: "on",
18583
+ quantity: "none",
18584
+ baseUnit: void 0,
18585
+ aggregates: BINARY_AGGREGATES,
18586
+ defaultAggregate: "any",
18587
+ empty: "offline",
18588
+ accept: [{
18589
+ cap: "switch",
18590
+ fieldPath: "on"
18591
+ }],
18592
+ companions: [CHANGED]
18593
+ };
18594
+ }
18595
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18596
+ DeviceFeature.LightColorRgb, DeviceFeature.LightColorHsv, DeviceFeature.LightColorMired;
18209
18597
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18210
18598
  /**
18211
18599
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19919,12 +20307,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19919
20307
  * providers detect renames by matching `uniqueId` and patch `id`
19920
20308
  * in-place so dispatch keeps working.
19921
20309
  *
19922
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19923
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19924
- * state push and are the single source of truth. The `device_class` tag is
19925
- * an HA-specific input that is consumed at the discovery boundary
19926
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19927
- * stored after that point.
20310
+ * **Display unit and precision are NOT stored here.** They are a read-time
20311
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20312
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20313
+ * neither is repeated on each sample. The `device_class` tag is an
20314
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20315
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19928
20316
  *
19929
20317
  * The `raw` escape hatch carries provider-specific attributes that don't
19930
20318
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19938,6 +20326,14 @@ var SourceInfoSchema = object({
19938
20326
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19939
20327
  * Used to detect rename when `id` changes. */
19940
20328
  uniqueId: string().optional(),
20329
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20330
+ quantity: ReadingQuantitySchema.optional(),
20331
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20332
+ dateTimeShape: _enum([
20333
+ "date",
20334
+ "time",
20335
+ "datetime"
20336
+ ]).optional(),
19941
20337
  /** Provider-specific extras that don't fit the structured slots.
19942
20338
  * Whitelist what goes here per provider — do NOT dump entire HA
19943
20339
  * attribute blobs (would explode the meta JSON column). */
@@ -40961,7 +41357,8 @@ method(object({
40961
41357
  "enum"
40962
41358
  ]),
40963
41359
  enumValues: array(string()).optional(),
40964
- item: boolean().optional()
41360
+ item: boolean().optional(),
41361
+ quantity: ReadingQuantitySchema.optional()
40965
41362
  })).readonly(),
40966
41363
  itemArray: object({
40967
41364
  path: string(),
@@ -44805,8 +45202,8 @@ var MaskPolygonShapeSchema = object({
44805
45202
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44806
45203
  var MaskGridShapeSchema = object({
44807
45204
  kind: literal("grid"),
44808
- gridWidth: number(),
44809
- gridHeight: number(),
45205
+ gridWidth: number().meta(readingMeta("none")),
45206
+ gridHeight: number().meta(readingMeta("none")),
44810
45207
  cells: array(boolean())
44811
45208
  });
44812
45209
  discriminatedUnion("kind", [
@@ -44918,6 +45315,8 @@ var NcSystemEventKindSchema = _enum([
44918
45315
  "detection-blind",
44919
45316
  "motion-noisy",
44920
45317
  "motion-calm",
45318
+ "package-llm-silent",
45319
+ "package-llm-answered",
44921
45320
  "recording-stalled",
44922
45321
  "recording-healthy",
44923
45322
  "recording-degraded",
@@ -56157,6 +56556,28 @@ var PackageVersionInfoSchema = object({
56157
56556
  deprecated: string().optional(),
56158
56557
  distTags: array(string()).readonly()
56159
56558
  });
56559
+ /**
56560
+ * One published document. `ok` is markdown from the registry. `missing` means
56561
+ * the registry answered and the file was not there. `error` is a registry or
56562
+ * tarball failure — never an empty string standing in for either.
56563
+ */
56564
+ var PackageDocBodySchema = discriminatedUnion("status", [
56565
+ object({
56566
+ status: literal("ok"),
56567
+ markdown: string()
56568
+ }),
56569
+ object({ status: literal("missing") }),
56570
+ object({
56571
+ status: literal("error"),
56572
+ message: string()
56573
+ })
56574
+ ]);
56575
+ var PackageDocsSchema = object({
56576
+ name: string(),
56577
+ version: string(),
56578
+ readme: PackageDocBodySchema,
56579
+ changelog: PackageDocBodySchema
56580
+ });
56160
56581
  var SearchResultSchema = object({
56161
56582
  name: string(),
56162
56583
  version: string(),
@@ -56375,7 +56796,10 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56375
56796
  }), object({ success: literal(true) }), {
56376
56797
  kind: "mutation",
56377
56798
  auth: "admin"
56378
- }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({ addonId: string() }), RestartAddonResultSchema, {
56799
+ }), method(object({ name: string() }), array(PackageVersionInfoSchema).readonly()), method(object({
56800
+ name: string().min(1),
56801
+ version: string().min(1).optional()
56802
+ }), PackageDocsSchema, { auth: "admin" }), method(object({ addonId: string() }), RestartAddonResultSchema, {
56379
56803
  kind: "mutation",
56380
56804
  auth: "admin"
56381
56805
  }), method(object({ packageName: string() }), object({ success: literal(true) }), {
@@ -56434,27 +56858,17 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56434
56858
  */
56435
56859
  var AirQualitySensorStatusSchema = object({
56436
56860
  /** Carbon dioxide concentration in ppm. */
56437
- co2Ppm: number().min(0).optional(),
56861
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56438
56862
  /** Total volatile organic compounds in ppb. */
56439
- vocPpb: number().min(0).optional(),
56863
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56440
56864
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56441
- pm25: number().min(0).optional(),
56865
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56442
56866
  /** Particulate matter ≤ 10 μm in µg/m³. */
56443
- pm10: number().min(0).optional(),
56867
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56444
56868
  /** Composite AQI value (typically 0..500). */
56445
- aqi: number().optional(),
56869
+ aqi: number().optional().meta(readingMeta("index")),
56446
56870
  /** Ms epoch when the slice was last updated. */
56447
- lastFetchedAt: number(),
56448
- /** Live display unit of the single metric this slice carries (e.g. HA
56449
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56450
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56451
- * per slice is unambiguous. */
56452
- unit: string().optional(),
56453
- /** Suggested decimal places for numeric display.
56454
- * Populated live from the upstream source when provided (e.g. HA
56455
- * `attributes.suggested_display_precision`). Falls back to
56456
- * auto-formatting when absent. */
56457
- precision: number().int().min(0).max(10).optional()
56871
+ lastFetchedAt: number()
56458
56872
  });
56459
56873
  var airQualitySensorCapability = {
56460
56874
  name: "air-quality-sensor",
@@ -56491,12 +56905,12 @@ var airQualitySensorCapability = {
56491
56905
  },
56492
56906
  pm25: {
56493
56907
  kind: "gauge",
56494
- unit: "ug/m3",
56908
+ unit: "µg/m³",
56495
56909
  deadband: 1
56496
56910
  },
56497
56911
  pm10: {
56498
56912
  kind: "gauge",
56499
- unit: "ug/m3",
56913
+ unit: "µg/m³",
56500
56914
  deadband: 1
56501
56915
  },
56502
56916
  aqi: {
@@ -56514,22 +56928,13 @@ var airQualitySensorCapability = {
56514
56928
  * entries with `device_class: illuminance`.
56515
56929
  */
56516
56930
  var AmbientLightSensorStatusSchema = object({
56517
- /** Current illuminance in lux (lx). */
56518
- lux: number().min(0).meta({
56519
- unit: "lx",
56931
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56932
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56933
+ precision: 0,
56520
56934
  label: "Illuminance"
56521
- }),
56935
+ })),
56522
56936
  /** Ms epoch when the slice was last updated. */
56523
- lastFetchedAt: number(),
56524
- /** Live display unit from the upstream source (e.g. HA
56525
- * `attributes.unit_of_measurement`). The UI prefers this over the
56526
- * role's canonical unit. Absent → fall back to the canonical unit. */
56527
- unit: string().optional(),
56528
- /** Suggested decimal places for numeric display.
56529
- * Populated live from the upstream source when provided (e.g. HA
56530
- * `attributes.suggested_display_precision`). Falls back to
56531
- * auto-formatting when absent. */
56532
- precision: number().int().min(0).max(10).optional()
56937
+ lastFetchedAt: number()
56533
56938
  });
56534
56939
  var ambientLightSensorCapability = {
56535
56940
  name: "ambient-light-sensor",
@@ -56561,12 +56966,7 @@ var ambientLightSensorCapability = {
56561
56966
  } } },
56562
56967
  /** Clock fields: written, but excluded from the compare that decides
56563
56968
  * whether persisting is worth a SQLite commit. */
56564
- volatileStateFields: ["lastFetchedAt"],
56565
- customize: {
56566
- version: 1,
56567
- advanced: "derive",
56568
- targets: []
56569
- }
56969
+ volatileStateFields: ["lastFetchedAt"]
56570
56970
  };
56571
56971
  /**
56572
56972
  * Per-class audio metrics aggregated over a sliding window.
@@ -56793,10 +57193,7 @@ var BatteryStatusSchema = object({
56793
57193
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56794
57194
  * declines to band a non-finite reading, which is the correct shape.
56795
57195
  */
56796
- percentage: number().min(0).max(100).nullable().meta({
56797
- unit: "%",
56798
- label: "Charge"
56799
- }),
57196
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56800
57197
  /**
56801
57198
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56802
57199
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56823,7 +57220,14 @@ var BatteryStatusSchema = object({
56823
57220
  */
56824
57221
  sleeping: boolean().meta({ label: "Sleeping" }),
56825
57222
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56826
- lastUpdated: number(),
57223
+ lastUpdated: number().meta(readingMeta("none")),
57224
+ /**
57225
+ * Battery voltage in volts, for a cell the firmware reports as voltage
57226
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
57227
+ * that only report charge. A second reading on this cap, not a second
57228
+ * `numeric-sensor` on the same device.
57229
+ */
57230
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56827
57231
  /**
56828
57232
  * Ms epoch of the last time the device PROVED it was reachable — a
56829
57233
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56875,7 +57279,6 @@ var BATTERY_DEVICE_TYPES = [
56875
57279
  /** Generic Customize target for the percentage reading (D936). */
56876
57280
  var batteryCustomize = {
56877
57281
  version: 1,
56878
- advanced: "derive",
56879
57282
  targets: [{
56880
57283
  id: "percentage",
56881
57284
  cardinality: "scalar",
@@ -56884,7 +57287,7 @@ var batteryCustomize = {
56884
57287
  stateField: "percentage"
56885
57288
  },
56886
57289
  deviceTypes: BATTERY_DEVICE_TYPES,
56887
- apply: "add-or-replace",
57290
+ apply: "add",
56888
57291
  path: "percentage",
56889
57292
  sources: [{
56890
57293
  kind: "field",
@@ -57005,10 +57408,16 @@ onStatusChanged: { data: object({
57005
57408
  /**
57006
57409
  * History (D850): recorded series. The generic capture replaces the `battery` series of `pipeline-analytics:stats-5m`.
57007
57410
  */
57008
- history: { fields: { percentage: {
57009
- kind: "gauge",
57010
- unit: "%"
57011
- } } },
57411
+ history: { fields: {
57412
+ percentage: {
57413
+ kind: "gauge",
57414
+ unit: "%"
57415
+ },
57416
+ volts: {
57417
+ kind: "gauge",
57418
+ unit: "V"
57419
+ }
57420
+ } },
57012
57421
  customize: batteryCustomize,
57013
57422
  /** Clock fields: written, but excluded from the compare that decides
57014
57423
  * whether persisting is worth a SQLite commit. */
@@ -57055,12 +57464,7 @@ var binaryCapability = {
57055
57464
  history: { fields: { on: { kind: "boolean" } } },
57056
57465
  /** Clock fields: written, but excluded from the compare that decides
57057
57466
  * whether persisting is worth a SQLite commit. */
57058
- volatileStateFields: ["lastChangedAt"],
57059
- customize: {
57060
- version: 1,
57061
- advanced: "derive",
57062
- targets: []
57063
- }
57467
+ volatileStateFields: ["lastChangedAt"]
57064
57468
  };
57065
57469
  /**
57066
57470
  * Dimmable-light brightness control. Co-exists with `switch` on the
@@ -57076,7 +57480,7 @@ var binaryCapability = {
57076
57480
  */
57077
57481
  var BrightnessStatusSchema = object({
57078
57482
  /** Current level as 0..100 inclusive. Firmware-reported. */
57079
- percentage: number().min(0).max(100),
57483
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
57080
57484
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
57081
57485
  lastChangedAt: number()
57082
57486
  });
@@ -57530,21 +57934,21 @@ var ClimateControlStatusSchema = object({
57530
57934
  /** Single setpoint in Celsius. Used by single-target modes
57531
57935
  * (heat / cool / dry). Null when the device is in a range mode
57532
57936
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57533
- target: number().nullable(),
57937
+ target: number().nullable().meta(readingMeta("temperature")),
57534
57938
  /** Upper bound of the dual setpoint in Celsius. Populated only
57535
57939
  * in `heat_cool` mode. */
57536
- targetHigh: number().nullable(),
57940
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57537
57941
  /** Lower bound of the dual setpoint in Celsius. Populated only
57538
57942
  * in `heat_cool` mode. */
57539
- targetLow: number().nullable(),
57943
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57540
57944
  /** Target relative humidity (0..100). Null when the device has no
57541
57945
  * humidity control. */
57542
- targetHumidity: number().min(0).max(100).nullable(),
57946
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57543
57947
  /** Current measured temperature in Celsius. Read-only — pushed by
57544
57948
  * the device's internal sensor. */
57545
- currentTemp: number().nullable(),
57949
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57546
57950
  /** Current measured relative humidity (0..100). Read-only. */
57547
- currentHumidity: number().min(0).max(100).nullable(),
57951
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57548
57952
  /** Vertical louver swing on/off. Null when the device has no vertical
57549
57953
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57550
57954
  swingVertical: boolean().nullable(),
@@ -57552,11 +57956,11 @@ var ClimateControlStatusSchema = object({
57552
57956
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57553
57957
  swingHorizontal: boolean().nullable(),
57554
57958
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57555
- minTemp: number().nullable().optional(),
57959
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57556
57960
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57557
- maxTemp: number().nullable().optional(),
57961
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57558
57962
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57559
- tempStep: number().positive().nullable().optional(),
57963
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57560
57964
  /** Ms epoch when the slice was last updated (push or command). */
57561
57965
  lastFetchedAt: number()
57562
57966
  });
@@ -57698,17 +58102,17 @@ var climateControlCapability = {
57698
58102
  * modes to expose in the picker without a slice fetch.
57699
58103
  */
57700
58104
  var RgbTripletSchema = object({
57701
- r: number().int().min(0).max(255),
57702
- g: number().int().min(0).max(255),
57703
- b: number().int().min(0).max(255)
58105
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
58106
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
58107
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57704
58108
  });
57705
58109
  var HsvTripletSchema = object({
57706
58110
  /** Hue in degrees, 0..360. */
57707
- h: number().min(0).max(360),
58111
+ h: number().min(0).max(360).meta(readingMeta("none")),
57708
58112
  /** Saturation as 0..100 inclusive. */
57709
- s: number().min(0).max(100),
58113
+ s: number().min(0).max(100).meta(readingMeta("none")),
57710
58114
  /** Value/brightness as 0..100 inclusive. */
57711
- v: number().min(0).max(100)
58115
+ v: number().min(0).max(100).meta(readingMeta("none"))
57712
58116
  });
57713
58117
  var ColorInputSchema = discriminatedUnion("mode", [
57714
58118
  object({
@@ -57737,7 +58141,7 @@ var ColorStatusSchema = object({
57737
58141
  /** Populated when `mode === 'hsv'`. */
57738
58142
  hsv: HsvTripletSchema.optional(),
57739
58143
  /** Populated when `mode === 'mired'`. */
57740
- mireds: number().int().optional(),
58144
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57741
58145
  /** Ms epoch of the last operator-driven change. */
57742
58146
  lastChangedAt: number()
57743
58147
  });
@@ -57932,18 +58336,12 @@ _enum([
57932
58336
  "empty",
57933
58337
  "full"
57934
58338
  ]);
57935
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57936
- * each. */
57937
- var ConsumableRemainingUnitSchema = _enum([
57938
- "days",
57939
- "hours",
57940
- "cycles"
57941
- ]);
57942
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57943
- * days). `value` null = unknown (D393). */
58339
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
58340
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
58341
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57944
58342
  var ConsumableRemainingSchema = object({
57945
- value: number().min(0).nullable(),
57946
- unit: ConsumableRemainingUnitSchema
58343
+ value: number().min(0).nullable().meta(readingMeta("device")),
58344
+ kind: ConsumableRemainingKindSchema
57947
58345
  });
57948
58346
  /** A single consumable item. Either a continuous `level` (remaining
57949
58347
  * life %, or fill % for a `supply` / `waste` item) or a discrete `status`
@@ -57956,7 +58354,7 @@ var ConsumableItemSchema = object({
57956
58354
  /** Display name. */
57957
58355
  label: string().min(1),
57958
58356
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57959
- level: number().min(0).max(100).nullable(),
58357
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57960
58358
  /** The provider's discrete verdict when known (binary mode). */
57961
58359
  status: ConsumableStatusSchema.nullable(),
57962
58360
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -58038,8 +58436,8 @@ var consumablesCustomize = {
58038
58436
  transforms: [{ kind: "identity" }]
58039
58437
  }],
58040
58438
  literals: [{
58041
- path: "remaining.unit",
58042
- values: [...ConsumableRemainingUnitSchema.options]
58439
+ path: "remaining.kind",
58440
+ values: [...ConsumableRemainingKindSchema.options]
58043
58441
  }]
58044
58442
  },
58045
58443
  {
@@ -58168,7 +58566,6 @@ reset: method(object({
58168
58566
  },
58169
58567
  "remaining.value": {
58170
58568
  kind: "gauge",
58171
- unitFrom: "remaining.unit",
58172
58569
  deadband: 1,
58173
58570
  displayName: "Remaining"
58174
58571
  }
@@ -58238,10 +58635,9 @@ var contactCapability = {
58238
58635
  * (empty array for the other kinds). It lives in the slice because
58239
58636
  * HA `select.*` entities may update their options at runtime.
58240
58637
  *
58241
- * The `unit`/`min`/`max`/`step` fields are meaningful for
58242
- * `kind === 'numeric'` and are absent (undefined) for all other
58243
- * kinds. They are populated live from HA attributes on each state
58244
- * push — consistent with `options` and the sensor-unit approach.
58638
+ * Numeric `min` / `max` / `step` live in the slice and share the device
58639
+ * quantity (`SourceInfo.quantity`, D944). Display precision is a read-time
58640
+ * preference. A datetime control's shape is `SourceInfo.dateTimeShape`.
58245
58641
  */
58246
58642
  var ControlStatusSchema = object({
58247
58643
  kind: _enum([
@@ -58253,39 +58649,17 @@ var ControlStatusSchema = object({
58253
58649
  /** Current value. Numeric kind → number; select/text/datetime → string.
58254
58650
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
58255
58651
  * or time-only — HA accepts all three forms). */
58256
- value: union([number(), string()]),
58652
+ value: union([number().meta(readingMeta("device")), string()]),
58257
58653
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
58258
58654
  options: array(string()),
58259
58655
  /** Ms epoch when the slice was last updated. */
58260
58656
  lastChangedAt: number(),
58261
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
58262
- * Populated live from HA `attributes.unit_of_measurement` on each push.
58263
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58264
- unit: string().optional(),
58265
- /** Minimum allowed value. Populated live from HA `attributes.min`.
58266
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58267
- min: number().optional(),
58268
- /** Maximum allowed value. Populated live from HA `attributes.max`.
58269
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58270
- max: number().optional(),
58271
- /** Step increment. Populated live from HA `attributes.step`.
58272
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
58273
- step: number().optional(),
58274
- /** Suggested decimal places for numeric display. Populated live from HA
58275
- * `attributes.suggested_display_precision`, or derived from `step` when the
58276
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
58277
- * other kinds. Falls back to auto-formatting when absent. */
58278
- precision: number().int().min(0).max(10).optional(),
58279
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
58280
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
58281
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
58282
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
58283
- * `has_date`/`has_time` attributes on each push. */
58284
- format: _enum([
58285
- "date",
58286
- "time",
58287
- "datetime"
58288
- ]).optional()
58657
+ /** Minimum allowed value, in the device quantity's base unit. */
58658
+ min: number().optional().meta(readingMeta("device")),
58659
+ /** Maximum allowed value, in the device quantity's base unit. */
58660
+ max: number().optional().meta(readingMeta("device")),
58661
+ /** Step increment, in the device quantity's base unit. */
58662
+ step: number().optional().meta(readingMeta("device"))
58289
58663
  });
58290
58664
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
58291
58665
  object({
@@ -58351,9 +58725,9 @@ var CoverStatusSchema = object({
58351
58725
  ]),
58352
58726
  /** 0 = fully closed, 100 = fully open. Null when the device has
58353
58727
  * no intermediate position surface. */
58354
- position: number().min(0).max(100).nullable(),
58728
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58355
58729
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
58356
- tiltPosition: number().min(0).max(100).nullable(),
58730
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58357
58731
  /** Ms epoch when the slice was last updated. */
58358
58732
  lastChangedAt: number()
58359
58733
  });
@@ -58456,9 +58830,9 @@ var NormalizedRangeSchema$1 = object({
58456
58830
  var DayNightStatusSchema = object({
58457
58831
  mode: DayNightModeSchema,
58458
58832
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
58459
- sensitivity: number().optional(),
58833
+ sensitivity: number().optional().meta(readingMeta("none")),
58460
58834
  /** Delay before the IR-cut filter flips, in seconds. */
58461
- switchDelaySec: number().optional(),
58835
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
58462
58836
  lastFetchedAt: number()
58463
58837
  });
58464
58838
  /**
@@ -58676,11 +59050,11 @@ var DishwasherStatusSchema = object({
58676
59050
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
58677
59051
  availablePrograms: array(string()),
58678
59052
  /** Seconds remaining on the active cycle, or null when not running / not known. */
58679
- remainingSeconds: number().int().nonnegative().nullable(),
59053
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58680
59054
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
58681
- delayedStartSeconds: number().int().nonnegative().nullable(),
59055
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
58682
59056
  /** Cycle completion 0..100, or null when not applicable. */
58683
- progressPercent: number().min(0).max(100).nullable(),
59057
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58684
59058
  /** True when the door is reported open; null when the door state is not known. */
58685
59059
  doorOpen: boolean().nullable(),
58686
59060
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -58704,7 +59078,7 @@ var DishwasherStatusSchema = object({
58704
59078
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
58705
59079
  machineCareDue: boolean().nullable(),
58706
59080
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
58707
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
59081
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
58708
59082
  /** Option states: null when the appliance does not expose the option or has not said. */
58709
59083
  halfLoad: boolean().nullable(),
58710
59084
  hygienePlus: boolean().nullable(),
@@ -58719,7 +59093,7 @@ var DishwasherStatusSchema = object({
58719
59093
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
58720
59094
  activeEvents: array(string()),
58721
59095
  /** Numeric error code when the appliance reports one; null otherwise. */
58722
- errorCode: number().nullable(),
59096
+ errorCode: number().nullable().meta(readingMeta("none")),
58723
59097
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
58724
59098
  errorLabel: string().nullable(),
58725
59099
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -58950,7 +59324,7 @@ var DoorbellStatusSchema = object({
58950
59324
  /** Ms epoch of the last press. null = never observed since this provider started. */
58951
59325
  lastPressedAt: number().nullable(),
58952
59326
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
58953
- pressCountSinceStart: number()
59327
+ pressCountSinceStart: number().meta(readingMeta("count"))
58954
59328
  });
58955
59329
  var DoorbellPressEventSchema = object({
58956
59330
  deviceId: number(),
@@ -59015,17 +59389,7 @@ onPressed: {
59015
59389
  history: "none",
59016
59390
  customize: doorbellCustomize
59017
59391
  };
59018
- /**
59019
- * Enum-state sensor — a string value picked from a finite option set.
59020
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
59021
- * states, weather conditions, contact-source channels, mode strings).
59022
- * The option set is stable across observations; it is persisted in the
59023
- * device config blob (via the control slice at adoption) and is NOT
59024
- * stored in `sourceInfo`.
59025
- */
59026
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
59027
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
59028
- var EnumSensorDateTimeFormatSchema = _enum([
59392
+ _enum([
59029
59393
  "date",
59030
59394
  "time",
59031
59395
  "datetime"
@@ -59033,12 +59397,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
59033
59397
  var EnumSensorStatusSchema = object({
59034
59398
  value: string(),
59035
59399
  /**
59036
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
59037
- * as a locale date/time/datetime; absent for plain enum sensors. HA
59038
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
59039
- * a time-only sensor → `'time'`.
59400
+ * Set when `value` is a timestamp rather than an enum state, so history
59401
+ * does not learn ISO strings as states. The display shape (date / time /
59402
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
59040
59403
  */
59041
- format: EnumSensorDateTimeFormatSchema.optional(),
59404
+ timestamp: boolean().optional(),
59042
59405
  /** Ms epoch when the slice was last updated. */
59043
59406
  lastFetchedAt: number()
59044
59407
  });
@@ -59065,11 +59428,11 @@ var enumSensorCapability = {
59065
59428
  * History (D850): the value as an `enum` series (an appliance's program or
59066
59429
  * operation state, an HVAC action). The option set is per device, so the
59067
59430
  * value list is LEARNED as values are first seen (bounded). A sensor with a
59068
- * `format` carries a timestamp, not a state, and is not recorded.
59431
+ * `timestamp` carries a clock, not a state, and is not recorded.
59069
59432
  */
59070
59433
  history: { fields: { value: {
59071
59434
  kind: "enum",
59072
- skipWhenSet: "format"
59435
+ skipWhenSet: "timestamp"
59073
59436
  } } },
59074
59437
  /** Clock fields: written, but excluded from the compare that decides
59075
59438
  * whether persisting is worth a SQLite commit. */
@@ -59089,13 +59452,13 @@ var EventFireSchema = object({
59089
59452
  deviceId: number(),
59090
59453
  eventType: string(),
59091
59454
  data: record(string(), unknown()).nullable(),
59092
- timestamp: number(),
59093
- seq: number()
59455
+ timestamp: number().meta(readingMeta("none")),
59456
+ seq: number().meta(readingMeta("count"))
59094
59457
  });
59095
59458
  var EventEmitterStatusSchema = object({
59096
59459
  eventTypes: array(string()),
59097
59460
  lastEvent: EventFireSchema.nullable(),
59098
- eventCountSinceStart: number()
59461
+ eventCountSinceStart: number().meta(readingMeta("count"))
59099
59462
  });
59100
59463
  var eventEmitterCapability = {
59101
59464
  name: "event-emitter",
@@ -59508,10 +59871,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
59508
59871
  var FanControlStatusSchema = object({
59509
59872
  /** Active speed as 0..100 inclusive. Null when the device has no
59510
59873
  * speed surface (single-speed fan). */
59511
- percentage: number().min(0).max(100).nullable(),
59874
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59512
59875
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
59513
59876
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
59514
- percentageStep: number().positive().optional(),
59877
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
59515
59878
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
59516
59879
  * — empty string when no preset is active or supported. */
59517
59880
  preset: string(),
@@ -59632,7 +59995,7 @@ var FeatureProbeStatusSchema = object({
59632
59995
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
59633
59996
  model: string().nullable(),
59634
59997
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
59635
- channelCount: number().nullable(),
59998
+ channelCount: number().nullable().meta(readingMeta("count")),
59636
59999
  /**
59637
60000
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
59638
60001
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -59755,9 +60118,91 @@ var gasCapability = {
59755
60118
  * whether persisting is worth a SQLite commit. */
59756
60119
  volatileStateFields: ["lastChangedAt"]
59757
60120
  };
60121
+ var GroupCommandsSchema = object({
60122
+ /** One row per member that has been commanded. A later command replaces that member's row. */
60123
+ results: array(object({
60124
+ deviceId: number().int().nonnegative(),
60125
+ cap: string().min(1),
60126
+ method: string().min(1),
60127
+ ok: boolean(),
60128
+ error: string().optional(),
60129
+ /** Ms epoch of the attempt. */
60130
+ at: number()
60131
+ })) });
60132
+ var groupCommandsCapability = {
60133
+ name: "group-commands",
60134
+ scope: "device",
60135
+ deviceNative: true,
60136
+ mode: "singleton",
60137
+ deviceTypes: [DeviceType.Switch, DeviceType.Light],
60138
+ methods: {},
60139
+ status: {
60140
+ schema: GroupCommandsSchema,
60141
+ kind: "push"
60142
+ },
60143
+ runtimeState: GroupCommandsSchema,
60144
+ /**
60145
+ * Runtime-state durability: **session** — the last attempt. A restored
60146
+ * failure would outlive the command that caused it.
60147
+ *
60148
+ * See `RuntimeStateDurability`. Enforced by
60149
+ * `scripts/check-runtime-state-durability.ts`.
60150
+ */
60151
+ durability: "session",
60152
+ history: "none"
60153
+ };
60154
+ /**
60155
+ * How many members of a helper group answered (D944). Counts are integers,
60156
+ * not measurements: zero included is a real count, never a stand-in reading.
60157
+ * The reading itself stays on the output capability. History is none — the
60158
+ * series that later statistics read is the output cap's own history block.
60159
+ */
60160
+ var GroupStatusSchema = object({
60161
+ /** Members whose current value entered the aggregate. */
60162
+ included: number().int().nonnegative().meta(readingMeta("count", {
60163
+ precision: 0,
60164
+ label: "Reporting"
60165
+ })),
60166
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
60167
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
60168
+ precision: 0,
60169
+ label: "Excluded"
60170
+ })),
60171
+ /** `included` + `excluded`. */
60172
+ total: number().int().nonnegative().meta(readingMeta("count", {
60173
+ precision: 0,
60174
+ label: "Members"
60175
+ }))
60176
+ });
60177
+ var groupStatusCapability = {
60178
+ name: "group-status",
60179
+ scope: "device",
60180
+ deviceNative: true,
60181
+ mode: "singleton",
60182
+ deviceTypes: [
60183
+ DeviceType.Sensor,
60184
+ DeviceType.Switch,
60185
+ DeviceType.Light
60186
+ ],
60187
+ methods: {},
60188
+ status: {
60189
+ schema: GroupStatusSchema,
60190
+ kind: "push"
60191
+ },
60192
+ runtimeState: GroupStatusSchema,
60193
+ /**
60194
+ * Runtime-state durability: **session** — recomputed from members on the
60195
+ * next source event; a restored count would outlive the readings it named.
60196
+ *
60197
+ * See `RuntimeStateDurability`. Enforced by
60198
+ * `scripts/check-runtime-state-durability.ts`.
60199
+ */
60200
+ durability: "session",
60201
+ history: "none"
60202
+ };
59758
60203
  var HeatingValveStatusSchema = object({
59759
60204
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
59760
- position: number().min(0).max(100).nullable(),
60205
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59761
60206
  /** Ms epoch when the slice was last updated (push or poll). */
59762
60207
  lastFetchedAt: number()
59763
60208
  });
@@ -59800,9 +60245,9 @@ var HumidifierStatusSchema = object({
59800
60245
  /** Whether the humidifier is currently on. */
59801
60246
  on: boolean(),
59802
60247
  /** Current measured relative humidity (0..100). Null when not reported. */
59803
- currentHumidity: number().min(0).max(100).nullable(),
60248
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59804
60249
  /** Target relative humidity (0..100). Null when no setpoint surface. */
59805
- targetHumidity: number().min(0).max(100).nullable(),
60250
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
59806
60251
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
59807
60252
  * device has no mode surface. */
59808
60253
  mode: string().nullable(),
@@ -59812,9 +60257,9 @@ var HumidifierStatusSchema = object({
59812
60257
  * `idle` / `off`). Null when the device doesn't report it. */
59813
60258
  action: string().nullable(),
59814
60259
  /** HA `min_humidity` attribute. Null → UI uses 0. */
59815
- minHumidity: number().nullable(),
60260
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
59816
60261
  /** HA `max_humidity` attribute. Null → UI uses 100. */
59817
- maxHumidity: number().nullable(),
60262
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
59818
60263
  /** Ms epoch when the slice was last updated. */
59819
60264
  lastChangedAt: number()
59820
60265
  });
@@ -59884,22 +60329,13 @@ var humidifierCapability = {
59884
60329
  * `sourceInfo`.
59885
60330
  */
59886
60331
  var HumiditySensorStatusSchema = object({
59887
- /** Current relative humidity, 0..100. */
59888
- percent: number().min(0).max(100).meta({
59889
- unit: "%",
60332
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
60333
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
60334
+ precision: 0,
59890
60335
  label: "Humidity"
59891
- }),
60336
+ })),
59892
60337
  /** Ms epoch when the slice was last updated. */
59893
- lastFetchedAt: number(),
59894
- /** Live display unit from the upstream source (e.g. HA
59895
- * `attributes.unit_of_measurement`). The UI prefers this over the
59896
- * role's canonical unit. Absent → fall back to the canonical unit. */
59897
- unit: string().optional(),
59898
- /** Suggested decimal places for numeric display.
59899
- * Populated live from the upstream source when provided (e.g. HA
59900
- * `attributes.suggested_display_precision`). Falls back to
59901
- * auto-formatting when absent. */
59902
- precision: number().int().min(0).max(10).optional()
60338
+ lastFetchedAt: number()
59903
60339
  });
59904
60340
  var humiditySensorCapability = {
59905
60341
  name: "humidity-sensor",
@@ -59930,12 +60366,7 @@ var humiditySensorCapability = {
59930
60366
  } } },
59931
60367
  /** Clock fields: written, but excluded from the compare that decides
59932
60368
  * whether persisting is worth a SQLite commit. */
59933
- volatileStateFields: ["lastFetchedAt"],
59934
- customize: {
59935
- version: 1,
59936
- advanced: "derive",
59937
- targets: []
59938
- }
60369
+ volatileStateFields: ["lastFetchedAt"]
59939
60370
  };
59940
60371
  /**
59941
60372
  * Image display cap. Models a single still image exposed by an integration —
@@ -59956,7 +60387,7 @@ var ImageStatusSchema = object({
59956
60387
  * entity exposes no `entity_picture` (yet). */
59957
60388
  url: string().nullable(),
59958
60389
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
59959
- lastUpdated: number().nullable()
60390
+ lastUpdated: number().nullable().meta(readingMeta("none"))
59960
60391
  });
59961
60392
  var imageCapability = {
59962
60393
  name: "image",
@@ -60044,19 +60475,19 @@ var NormalizedRangeSchema = object({
60044
60475
  */
60045
60476
  var ImageSettingsStatusSchema = object({
60046
60477
  /** Normalized 0–100. */
60047
- brightness: number().optional(),
60478
+ brightness: number().optional().meta(readingMeta("none")),
60048
60479
  /** Normalized 0–100. */
60049
- contrast: number().optional(),
60480
+ contrast: number().optional().meta(readingMeta("none")),
60050
60481
  /** Normalized 0–100. */
60051
- saturation: number().optional(),
60482
+ saturation: number().optional().meta(readingMeta("none")),
60052
60483
  /** Normalized 0–100. */
60053
- sharpness: number().optional(),
60484
+ sharpness: number().optional().meta(readingMeta("none")),
60054
60485
  mirror: boolean().optional(),
60055
60486
  flip: boolean().optional(),
60056
60487
  rotate: ImageRotateSchema.optional(),
60057
60488
  whiteBalance: WhiteBalanceModeSchema.optional(),
60058
60489
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
60059
- warmth: number().optional(),
60490
+ warmth: number().optional().meta(readingMeta("none")),
60060
60491
  exposureMode: ExposureModeSchema.optional(),
60061
60492
  backlightMode: BacklightModeSchema.optional(),
60062
60493
  lastFetchedAt: number()
@@ -60335,10 +60766,10 @@ var IntercomAbilitySchema = object({
60335
60766
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
60336
60767
  codecs: array(string()),
60337
60768
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
60338
- sampleRate: number(),
60769
+ sampleRate: number().meta(readingMeta("none")),
60339
60770
  duplex: _enum(["half", "full"]),
60340
60771
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
60341
- maxBacklogMs: number()
60772
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
60342
60773
  });
60343
60774
  var IntercomStatusSchema = object({
60344
60775
  /** True while a talk session is open. */
@@ -60565,13 +60996,13 @@ var LawnMowerControlStatusSchema = object({
60565
60996
  activity: LawnMowerActivitySchema,
60566
60997
  /** 0..100 battery percentage. Null when the device has no battery
60567
60998
  * reading. */
60568
- batteryLevel: number().min(0).max(100).nullable(),
60999
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60569
61000
  /** 0..100 mowing-completion percentage of the current task, or null when no
60570
61001
  * task is active / progress is unavailable. */
60571
- progressPercent: number().min(0).max(100).nullable(),
61002
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60572
61003
  /** Current device/event code (dynamic — mostly status, sometimes an error),
60573
61004
  * or null when unknown. */
60574
- currentCode: number().nullable(),
61005
+ currentCode: number().nullable().meta(readingMeta("none")),
60575
61006
  /** Human label for {@link currentCode}, or null when undecodable. */
60576
61007
  currentCodeLabel: string().nullable(),
60577
61008
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -61104,7 +61535,7 @@ var MediaPlayerStatusSchema = object({
61104
61535
  state: MediaPlayerStateSchema,
61105
61536
  /** Volume as 0..100 inclusive. Null when the device has no volume
61106
61537
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
61107
- volumeLevel: number().min(0).max(100).nullable(),
61538
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61108
61539
  /** Mute toggle distinct from volume=0. Null when no mute surface.
61109
61540
  * Pair with `DeviceFeature.MediaPlayerMute`. */
61110
61541
  isMuted: boolean().nullable(),
@@ -61117,10 +61548,10 @@ var MediaPlayerStatusSchema = object({
61117
61548
  currentMedia: MediaInfoSchema.nullable(),
61118
61549
  /** Current playback position in ms. Null when not seekable or
61119
61550
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
61120
- positionMs: number().int().nonnegative().nullable(),
61551
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61121
61552
  /** Total duration of the current media in ms. Null when unknown
61122
61553
  * (live stream) or nothing is playing. */
61123
- durationMs: number().int().nonnegative().nullable(),
61554
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
61124
61555
  /** Shuffle toggle. Null when no shuffle surface.
61125
61556
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
61126
61557
  shuffle: boolean().nullable(),
@@ -61429,7 +61860,7 @@ var motionCustomize = {
61429
61860
  eventName: "onMotionChanged"
61430
61861
  },
61431
61862
  deviceTypes: [DeviceType.Camera],
61432
- apply: "replace",
61863
+ apply: "add",
61433
61864
  applyAuthority: {
61434
61865
  kind: "camera-sensors",
61435
61866
  timeoutField: "motionCooldownMs"
@@ -61501,7 +61932,7 @@ var MotionStatusSchema = object({
61501
61932
  * it, every provider seeded it at boot, and every session left it
61502
61933
  * `detected: true` after its detach.
61503
61934
  */
61504
- autoClearAfterMs: number().nullable()
61935
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
61505
61936
  });
61506
61937
  /**
61507
61938
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -61538,12 +61969,18 @@ var motionCapability = {
61538
61969
  methods: {
61539
61970
  /**
61540
61971
  * Pull the current motion state synchronously. Convenience shortcut
61541
- * for consumers that don't need the full status object; equivalent
61542
- * to `getStatus()?.detected ?? false`. Will likely be folded into
61543
- * `getStatus` once the auto-injected status surface lands in every
61544
- * consumer.
61972
+ * for consumers that don't need the full status object. A composed
61973
+ * group answers from its slice (D944): unknown throws, it is not off.
61545
61974
  */
61546
61975
  isDetected: method(object({ deviceId: number() }), boolean()) },
61976
+ /**
61977
+ * A composed motion group serves `isDetected` from `detected` (D944).
61978
+ * Without this the cap stays uncomposable (`has-commands`).
61979
+ */
61980
+ composedMethods: { isDetected: {
61981
+ kind: "slice",
61982
+ field: "detected"
61983
+ } },
61547
61984
  events: {
61548
61985
  /**
61549
61986
  * Fires every time the runner transitions a camera between
@@ -61683,7 +62120,7 @@ var MotionZoneRegionSchema = object({
61683
62120
  * the grid region(s). */
61684
62121
  var MotionZoneStatusSchema = object({
61685
62122
  enabled: boolean(),
61686
- sensitivity: number(),
62123
+ sensitivity: number().meta(readingMeta("none")),
61687
62124
  /** Grid region(s). Today exactly one `grid` shape. */
61688
62125
  regions: array(MotionZoneRegionSchema),
61689
62126
  lastFetchedAt: number()
@@ -62053,7 +62490,7 @@ var NavigationStatusSchema = object({
62053
62490
  following: boolean(),
62054
62491
  flash: boolean(),
62055
62492
  lightMode: NavigationLightModeSchema,
62056
- lightLevel: number().min(40).max(100),
62493
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
62057
62494
  /** Ms epoch when the slice was last updated. */
62058
62495
  lastChangedAt: number()
62059
62496
  });
@@ -62187,13 +62624,13 @@ var NetworkLinkStatusSchema = object({
62187
62624
  * one whose reading has not landed must not be drawn at 0 %. Consumers
62188
62625
  * SKIP a null rather than coerce it.
62189
62626
  */
62190
- signalPercent: number().min(0).max(100).nullable(),
62627
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
62191
62628
  /** Raw received signal strength in dBm, when the firmware reports one. */
62192
- rssiDbm: number().optional(),
62629
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
62193
62630
  /** Network name of a wireless link, when the firmware reports it. */
62194
62631
  ssid: string().optional(),
62195
62632
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
62196
- lastUpdated: number()
62633
+ lastUpdated: number().meta(readingMeta("none"))
62197
62634
  });
62198
62635
  var networkLinkCapability = {
62199
62636
  name: "network-link",
@@ -62578,7 +63015,7 @@ var NotifierStatusSchema = object({
62578
63015
  lastError: string().nullable(),
62579
63016
  /** Number of deliveries currently buffered server-side (rate-limited
62580
63017
  * notifiers may queue). 0 when the provider sends synchronously. */
62581
- queueDepth: number().int().nonnegative(),
63018
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
62582
63019
  /** Per-feature capability matrix — authoritative source for the
62583
63020
  * compose-form field gating. */
62584
63021
  supports: NotifierSupportsSchema
@@ -62670,11 +63107,9 @@ onSent: { data: object({
62670
63107
  * cap fits (HA `sensor` whose `device_class` we don't have a typed cap
62671
63108
  * for: water flow, distance, weight, frequency, signal strength, …).
62672
63109
  *
62673
- * The `unit` and `precision` fields carry the live display metadata that
62674
- * the upstream source provides with each state push (HA
62675
- * `attributes.unit_of_measurement` / `attributes.suggested_display_precision`).
62676
- * The UI reads them directly from the slice — they are the single source
62677
- * of truth for generic numeric rendering and do NOT live on `sourceInfo`.
63110
+ * The quantity is not on this slice. It is chosen when the device is
63111
+ * adopted and stored once on `SourceInfo.quantity` (D944). `value` is in
63112
+ * that quantity's base unit. `null` is a refused sample (D393).
62678
63113
  *
62679
63114
  * Prefer the typed alternatives (`temperature-sensor`, `humidity-sensor`,
62680
63115
  * `ambient-light-sensor`, `pressure-sensor`, `power-meter`,
@@ -62683,15 +63118,8 @@ onSent: { data: object({
62683
63118
  * numeric values land here.
62684
63119
  */
62685
63120
  var NumericSensorStatusSchema = object({
62686
- value: number(),
62687
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
62688
- * Populated live from the upstream source on each state push.
62689
- * Absent for unitless sensors. */
62690
- unit: string().optional(),
62691
- /** Suggested decimal places for numeric display.
62692
- * Populated live from the upstream source when provided.
62693
- * Falls back to auto-formatting when absent. */
62694
- precision: number().int().min(0).max(10).optional(),
63121
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
63122
+ value: number().nullable().meta(readingMeta("device")),
62695
63123
  /** Ms epoch when the slice was last updated. */
62696
63124
  lastFetchedAt: number()
62697
63125
  });
@@ -62715,11 +63143,11 @@ var numericSensorCapability = {
62715
63143
  */
62716
63144
  durability: "restored",
62717
63145
  /**
62718
- * History (D850): recorded series. The unit is per device, read from the sibling `unit` field.
63146
+ * History (D850): recorded series. The unit is the device quantity's base,
63147
+ * resolved when the series is read — the cap has no single unit.
62719
63148
  */
62720
63149
  history: { fields: { value: {
62721
63150
  kind: "gauge",
62722
- unitFrom: "unit",
62723
63151
  minIntervalMs: 3e4
62724
63152
  } } },
62725
63153
  /** Clock fields: written, but excluded from the compare that decides
@@ -63011,23 +63439,26 @@ var PetFeederStatusSchema = object({
63011
63439
  /** Food currently in the bowl (grams). Null when the device has not
63012
63440
  * reported a reading yet. On dual-hopper models this is the combined
63013
63441
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
63014
- foodLevel: number().nullable(),
63442
+ foodLevel: number().nullable().meta(readingMeta("none")),
63015
63443
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
63016
63444
  * single-hopper models. */
63017
- food1: number().nullable(),
63445
+ food1: number().nullable().meta(readingMeta("none")),
63018
63446
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
63019
63447
  * single-hopper models. */
63020
- food2: number().nullable(),
63448
+ food2: number().nullable().meta(readingMeta("none")),
63021
63449
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
63022
63450
  * (`device_class: problem`, on = low). True when the bowl is empty /
63023
63451
  * below the feeder's low threshold. */
63024
63452
  lowFood: boolean(),
63025
63453
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
63026
63454
  * device has no battery reading. */
63027
- batteryPower: number().min(0).max(100).nullable(),
63028
- /** Days of desiccant life remaining. Null when the model has no
63029
- * desiccant sensor. */
63030
- desiccantLeftDays: number().nullable(),
63455
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63456
+ /**
63457
+ * Desiccant life remaining, stored in seconds (duration base, D944).
63458
+ * Providers convert from the days the feeder reports. Null when the
63459
+ * model has no desiccant sensor.
63460
+ */
63461
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
63031
63462
  /** True while a feed is in progress. */
63032
63463
  feeding: boolean(),
63033
63464
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -63038,7 +63469,7 @@ var PetFeederStatusSchema = object({
63038
63469
  * with `errorCode` for consumers that want the raw integer. */
63039
63470
  error: string().nullable(),
63040
63471
  /** Raw device error code (0 / null = no error). */
63041
- errorCode: number().nullable(),
63472
+ errorCode: number().nullable().meta(readingMeta("none")),
63042
63473
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
63043
63474
  isDualHopper: boolean(),
63044
63475
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -63048,7 +63479,7 @@ var PetFeederStatusSchema = object({
63048
63479
  /** Play a chime when dispensing. */
63049
63480
  feedSound: boolean(),
63050
63481
  /** Speaker / prompt volume level (device-scaled integer). */
63051
- volume: number(),
63482
+ volume: number().meta(readingMeta("none")),
63052
63483
  /** Ms epoch when the slice was last refreshed from the cloud. */
63053
63484
  lastFetchedAt: number()
63054
63485
  });
@@ -63167,7 +63598,7 @@ var petFeederCapability = {
63167
63598
  },
63168
63599
  desiccantLeftDays: {
63169
63600
  kind: "gauge",
63170
- unit: "days"
63601
+ unit: "s"
63171
63602
  }
63172
63603
  } }
63173
63604
  };
@@ -63568,36 +63999,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
63568
63999
  */
63569
64000
  var PowerMeterStatusSchema = object({
63570
64001
  /** Instantaneous power draw in watts. */
63571
- watts: number().optional().meta({
63572
- unit: "W",
63573
- label: "Power"
63574
- }),
64002
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
63575
64003
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
63576
- kwhTotal: number().optional().meta({
63577
- unit: "kWh",
63578
- label: "Energy"
63579
- }),
64004
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
63580
64005
  /** Voltage in volts. */
63581
- volts: number().optional(),
64006
+ volts: number().optional().meta(readingMeta("voltage")),
63582
64007
  /** Current in amperes. */
63583
- amps: number().optional(),
63584
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
63585
- va: number().optional(),
64008
+ amps: number().optional().meta(readingMeta("current")),
64009
+ /** Apparent power in volt-amperes. */
64010
+ va: number().optional().meta(readingMeta("apparent-power")),
63586
64011
  /** Power factor (0..1) if reported. */
63587
- powerFactor: number().min(0).max(1).optional(),
64012
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
63588
64013
  /** Ms epoch when the slice was last updated. */
63589
- lastFetchedAt: number(),
63590
- /** Live display unit of the single metric this slice carries (e.g. HA
63591
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
63592
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
63593
- * unit per slice is unambiguous. The UI prefers this over the role's
63594
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
63595
- unit: string().optional(),
63596
- /** Suggested decimal places for numeric display.
63597
- * Populated live from the upstream source when provided (e.g. HA
63598
- * `attributes.suggested_display_precision`). Falls back to
63599
- * auto-formatting when absent. */
63600
- precision: number().int().min(0).max(10).optional()
64014
+ lastFetchedAt: number()
63601
64015
  });
63602
64016
  var powerMeterCapability = {
63603
64017
  name: "power-meter",
@@ -63661,12 +64075,7 @@ var powerMeterCapability = {
63661
64075
  } },
63662
64076
  /** Clock fields: written, but excluded from the compare that decides
63663
64077
  * whether persisting is worth a SQLite commit. */
63664
- volatileStateFields: ["lastFetchedAt"],
63665
- customize: {
63666
- version: 1,
63667
- advanced: "derive",
63668
- targets: []
63669
- }
64078
+ volatileStateFields: ["lastFetchedAt"]
63670
64079
  };
63671
64080
  /**
63672
64081
  * Presence cap. Models HA `person.*` and `device_tracker.*` entities
@@ -63686,11 +64095,11 @@ var powerMeterCapability = {
63686
64095
  */
63687
64096
  var GpsLocationSchema = object({
63688
64097
  /** Latitude in decimal degrees, -90..90. */
63689
- latitude: number().min(-90).max(90),
64098
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
63690
64099
  /** Longitude in decimal degrees, -180..180. */
63691
- longitude: number().min(-180).max(180),
64100
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
63692
64101
  /** Reported accuracy in meters (lower = better). */
63693
- accuracyMeters: number().nonnegative()
64102
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
63694
64103
  });
63695
64104
  var PresenceStatusSchema = object({
63696
64105
  /** `home` / `not_home` / any user-defined zone name. */
@@ -63702,7 +64111,7 @@ var PresenceStatusSchema = object({
63702
64111
  gps: GpsLocationSchema.nullable(),
63703
64112
  /** Optional battery level of the tracking device (0..100). Null
63704
64113
  * when not reported. */
63705
- batteryPercent: number().min(0).max(100).nullable(),
64114
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63706
64115
  /** Ms epoch when the slice was last updated. */
63707
64116
  lastChangedAt: number()
63708
64117
  });
@@ -63752,19 +64161,10 @@ var presenceCapability = {
63752
64161
  * `sourceInfo`.
63753
64162
  */
63754
64163
  var PressureSensorStatusSchema = object({
63755
- /** Current pressure in hPa. */
63756
- hpa: number(),
64164
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
64165
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
63757
64166
  /** Ms epoch when the slice was last updated. */
63758
- lastFetchedAt: number(),
63759
- /** Live display unit from the upstream source (e.g. HA
63760
- * `attributes.unit_of_measurement`). The UI prefers this over the
63761
- * role's canonical unit. Absent → fall back to the canonical unit. */
63762
- unit: string().optional(),
63763
- /** Suggested decimal places for numeric display.
63764
- * Populated live from the upstream source when provided (e.g. HA
63765
- * `attributes.suggested_display_precision`). Falls back to
63766
- * auto-formatting when absent. */
63767
- precision: number().int().min(0).max(10).optional()
64167
+ lastFetchedAt: number()
63768
64168
  });
63769
64169
  var pressureSensorCapability = {
63770
64170
  name: "pressure-sensor",
@@ -64178,8 +64578,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
64178
64578
  }), _void(), { kind: "mutation" });
64179
64579
  var PtzAutotrackSettingsSchema = object({
64180
64580
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
64181
- stopDelaySeconds: number().int().min(0).max(300),
64182
- disappearDelaySeconds: number().int().min(0).max(300)
64581
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
64582
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
64183
64583
  });
64184
64584
  /**
64185
64585
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -64333,7 +64733,7 @@ var RunningStateStatusSchema = object({
64333
64733
  /** null = unknown — never coerced to false (D393). */
64334
64734
  running: boolean().nullable(),
64335
64735
  /** Epoch ms the current boolean began; null while `running` is null. */
64336
- since: number().nullable(),
64736
+ since: number().nullable().meta(readingMeta("none")),
64337
64737
  source: RunningStateSourceSchema.meta({ label: "Source" }),
64338
64738
  phase: string().nullable(),
64339
64739
  /** null until a reading has been published (D393). */
@@ -64348,7 +64748,6 @@ var RUNNING_DEVICE_TYPES = Object.values(DeviceType);
64348
64748
  /** Generic Customize target for the running boolean (D936). */
64349
64749
  var runningCustomize = {
64350
64750
  version: 1,
64351
- advanced: "derive",
64352
64751
  targets: [{
64353
64752
  id: "running",
64354
64753
  cardinality: "scalar",
@@ -64368,7 +64767,7 @@ var runningCustomize = {
64368
64767
  DeviceType.Thermostat,
64369
64768
  DeviceType.PetFeeder
64370
64769
  ],
64371
- apply: "add-or-replace",
64770
+ apply: "add",
64372
64771
  path: "running",
64373
64772
  sources: [
64374
64773
  {
@@ -65632,9 +66031,9 @@ var RecordTriggerSchema = _enum([
65632
66031
  */
65633
66032
  var RecordWindowSchema = object({
65634
66033
  trigger: RecordTriggerSchema,
65635
- day: number().int().min(0).max(6),
65636
- startMinute: number().int().min(0).max(1439),
65637
- endMinute: number().int().min(1).max(1440)
66034
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
66035
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
66036
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
65638
66037
  });
65639
66038
  /** Status of one physical volume, as the camera itself describes it. */
65640
66039
  var OnboardStorageVolumeSchema = object({
@@ -65710,10 +66109,10 @@ var RecordingOnboardStatusSchema = object({
65710
66109
  /** Master "record to the card at all" switch. */
65711
66110
  enabled: boolean().nullable(),
65712
66111
  overwriteWhenFull: boolean().nullable(),
65713
- preRecordSec: number().nullable(),
65714
- postRecordSec: number().nullable(),
66112
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
66113
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
65715
66114
  /** Length of one recorded file, in minutes. */
65716
- segmentMinutes: number().nullable(),
66115
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
65717
66116
  /** The primary track's weekly windows, flattened. */
65718
66117
  windows: array(RecordWindowSchema),
65719
66118
  /**
@@ -65729,7 +66128,7 @@ var RecordingOnboardStatusSchema = object({
65729
66128
  * provider reporting a non-zero count also reports the schedule as not
65730
66129
  * writable.
65731
66130
  */
65732
- unreadableWindows: number(),
66131
+ unreadableWindows: number().meta(readingMeta("count")),
65733
66132
  /**
65734
66133
  * The camera is scheduled to record and has NO usable storage.
65735
66134
  *
@@ -66574,7 +66973,7 @@ var CamStreamDescriptorSchema = object({
66574
66973
  url: string().optional(),
66575
66974
  codec: string().optional(),
66576
66975
  resolution: CamStreamResolutionSchema.optional(),
66577
- fps: number().positive().optional(),
66976
+ fps: number().positive().optional().meta(readingMeta("rate")),
66578
66977
  label: string().optional(),
66579
66978
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
66580
66979
  deviceFeatures: array(string()).optional(),
@@ -66661,18 +67060,18 @@ var StreamProfileSchema = _enum([
66661
67060
  ]);
66662
67061
  /** The current encoding config of one stream profile. */
66663
67062
  var StreamProfileConfigSchema = object({
66664
- width: number(),
66665
- height: number(),
67063
+ width: number().meta(readingMeta("none")),
67064
+ height: number().meta(readingMeta("none")),
66666
67065
  codec: _enum(["h264", "h265"]),
66667
- framerate: number(),
66668
- bitrate: number(),
67066
+ framerate: number().meta(readingMeta("rate")),
67067
+ bitrate: number().meta(readingMeta("bitrate")),
66669
67068
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
66670
67069
  encoderProfile: _enum([
66671
67070
  "high",
66672
67071
  "main",
66673
67072
  "baseline"
66674
67073
  ]).optional(),
66675
- gop: number().optional(),
67074
+ gop: number().optional().meta(readingMeta("none")),
66676
67075
  /**
66677
67076
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
66678
67077
  *
@@ -66893,12 +67292,7 @@ var switchCapability = {
66893
67292
  label: "On",
66894
67293
  kind: "boolean"
66895
67294
  }
66896
- }] },
66897
- customize: {
66898
- version: 1,
66899
- advanced: "derive",
66900
- targets: []
66901
- }
67295
+ }] }
66902
67296
  };
66903
67297
  /** C0 controls, DEL and the C1 range: none of them belongs in a name. */
66904
67298
  function hasControlCharacter(value) {
@@ -67481,35 +67875,21 @@ var tamperCapability = {
67481
67875
  * entries with `device_class: temperature` and any future native
67482
67876
  * thermometer.
67483
67877
  *
67484
- * Unit normalisation: providers convert to Celsius before storing.
67485
- * The slice value is always Celsius so cross-cap aggregators
67486
- * (climate-control's `currentTemp`, energy analytics) can compose
67487
- * without per-source unit fixups. The canonical display unit (`°C`) is
67488
- * a descriptor constant in the UI (ROLE_DESCRIPTOR), not stored in
67489
- * `sourceInfo`.
67878
+ * The number is Celsius (D944). Providers convert on ingest. A failed
67879
+ * conversion is `null`, never a guess. Display unit is a read-time preference.
67490
67880
  *
67491
67881
  * Status `lastFetchedAt` lets staleness-aware consumers detect a
67492
67882
  * frozen feed (provider hung) distinct from a "temperature hasn't
67493
67883
  * changed" steady state.
67494
67884
  */
67495
67885
  var TemperatureSensorStatusSchema = object({
67496
- /** Current temperature in Celsius. */
67497
- celsius: number().meta({
67498
- unit: "°C",
67886
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
67887
+ celsius: number().nullable().meta(readingMeta("temperature", {
67888
+ precision: 1,
67499
67889
  label: "Temperature"
67500
- }),
67890
+ })),
67501
67891
  /** Ms epoch when the slice was last updated (push or poll). */
67502
- lastFetchedAt: number(),
67503
- /** Live display unit from the upstream source (e.g. HA
67504
- * `attributes.unit_of_measurement`). The UI prefers this over the
67505
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
67506
- * Absent → the UI falls back to the role's canonical unit. */
67507
- unit: string().optional(),
67508
- /** Suggested decimal places for numeric display.
67509
- * Populated live from the upstream source when provided (e.g. HA
67510
- * `attributes.suggested_display_precision`). Falls back to
67511
- * auto-formatting when absent. */
67512
- precision: number().int().min(0).max(10).optional()
67892
+ lastFetchedAt: number()
67513
67893
  });
67514
67894
  var temperatureSensorCapability = {
67515
67895
  name: "temperature-sensor",
@@ -67540,12 +67920,7 @@ var temperatureSensorCapability = {
67540
67920
  } } },
67541
67921
  /** Clock fields: written, but excluded from the compare that decides
67542
67922
  * whether persisting is worth a SQLite commit. */
67543
- volatileStateFields: ["lastFetchedAt"],
67544
- customize: {
67545
- version: 1,
67546
- advanced: "derive",
67547
- targets: []
67548
- }
67923
+ volatileStateFields: ["lastFetchedAt"]
67549
67924
  };
67550
67925
  /**
67551
67926
  * toast — system-scoped singleton capability that streams toast
@@ -67828,16 +68203,16 @@ var VacuumControlStatusSchema = object({
67828
68203
  ]),
67829
68204
  /** 0..100 battery percentage. Null when the device has no battery
67830
68205
  * reading. */
67831
- batteryLevel: number().min(0).max(100).nullable(),
68206
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67832
68207
  /** Current fan-speed token (provider-verbatim). Null when unknown or
67833
68208
  * the vacuum has no speed control. */
67834
68209
  fanSpeed: string().nullable(),
67835
68210
  /** Speed tokens the hardware accepts — drives the UI selector. */
67836
68211
  availableFanSpeeds: array(string()),
67837
68212
  /** 0..100 cleaning-completion percentage of the current task, or null. */
67838
- progressPercent: number().min(0).max(100).nullable(),
68213
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67839
68214
  /** Current error code (0 / null = no error). */
67840
- errorCode: number().nullable(),
68215
+ errorCode: number().nullable().meta(readingMeta("none")),
67841
68216
  /** Human label for {@link errorCode}, or null when none / undecodable. */
67842
68217
  errorLabel: string().nullable(),
67843
68218
  /** Ms epoch when the slice was last updated. */
@@ -67915,7 +68290,7 @@ var ValveStatusSchema = object({
67915
68290
  ]),
67916
68291
  /** 0 = fully closed, 100 = fully open. Null when the device has no
67917
68292
  * intermediate position surface. */
67918
- position: number().min(0).max(100).nullable(),
68293
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
67919
68294
  /** Ms epoch when the slice was last updated. */
67920
68295
  lastChangedAt: number()
67921
68296
  });
@@ -68023,9 +68398,9 @@ var vibrationCapability = {
68023
68398
  */
68024
68399
  var WaterHeaterStatusSchema = object({
68025
68400
  /** Current measured temperature. Null when not reported. */
68026
- currentTemp: number().nullable(),
68401
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
68027
68402
  /** Target temperature setpoint. Null when no setpoint surface. */
68028
- targetTemp: number().nullable(),
68403
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
68029
68404
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
68030
68405
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
68031
68406
  * device reports an unknown state. */
@@ -68036,9 +68411,9 @@ var WaterHeaterStatusSchema = object({
68036
68411
  * has no away surface. */
68037
68412
  away: boolean().nullable(),
68038
68413
  /** HA `min_temp` attribute. Null when not reported. */
68039
- minTemp: number().nullable(),
68414
+ minTemp: number().nullable().meta(readingMeta("temperature")),
68040
68415
  /** HA `max_temp` attribute. Null when not reported. */
68041
- maxTemp: number().nullable(),
68416
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
68042
68417
  /** Ms epoch when the slice was last updated. */
68043
68418
  lastChangedAt: number()
68044
68419
  });
@@ -68123,25 +68498,18 @@ var waterHeaterCapability = {
68123
68498
  * Forecast deferred — current conditions only for v1.
68124
68499
  */
68125
68500
  var WeatherStatusSchema = object({
68126
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
68127
- * when no condition has been reported yet. */
68501
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
68128
68502
  condition: string().nullable(),
68129
- /** Current temperature in the reported unit. Null when not provided. */
68130
- temperature: number().nullable(),
68131
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
68132
- temperatureUnit: string().nullable(),
68503
+ /** Current temperature in Celsius. Null when not provided or refused. */
68504
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
68133
68505
  /** Relative humidity (0..100). Null when not provided. */
68134
- humidity: number().min(0).max(100).nullable(),
68135
- /** Barometric pressure in the reported unit. Null when not provided. */
68136
- pressure: number().nullable(),
68137
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
68138
- pressureUnit: string().nullable(),
68139
- /** Wind speed in the reported unit. Null when not provided. */
68140
- windSpeed: number().nullable(),
68141
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
68142
- windSpeedUnit: string().nullable(),
68506
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
68507
+ /** Barometric pressure in hPa. Null when not provided or refused. */
68508
+ pressure: number().nullable().meta(readingMeta("pressure")),
68509
+ /** Wind speed in metres per second. Null when not provided or refused. */
68510
+ windSpeed: number().nullable().meta(readingMeta("speed")),
68143
68511
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
68144
- windBearing: number().nullable(),
68512
+ windBearing: number().nullable().meta(readingMeta("angle")),
68145
68513
  /** Ms epoch when the slice was last updated. */
68146
68514
  lastFetchedAt: number()
68147
68515
  });
@@ -68169,12 +68537,12 @@ var weatherCapability = {
68169
68537
  */
68170
68538
  durability: "session",
68171
68539
  /**
68172
- * History (D850): recorded series. Units follow the sibling `*Unit` fields.
68540
+ * History (D850): recorded series. Numbers are the quantity base (D944).
68173
68541
  */
68174
68542
  history: { fields: {
68175
68543
  temperature: {
68176
68544
  kind: "gauge",
68177
- unitFrom: "temperatureUnit",
68545
+ unit: "°C",
68178
68546
  deadband: .2
68179
68547
  },
68180
68548
  humidity: {
@@ -68184,12 +68552,12 @@ var weatherCapability = {
68184
68552
  },
68185
68553
  pressure: {
68186
68554
  kind: "gauge",
68187
- unitFrom: "pressureUnit",
68555
+ unit: "hPa",
68188
68556
  deadband: .5
68189
68557
  },
68190
68558
  windSpeed: {
68191
68559
  kind: "gauge",
68192
- unitFrom: "windSpeedUnit",
68560
+ unit: "m/s",
68193
68561
  deadband: .5
68194
68562
  }
68195
68563
  } }
@@ -69420,6 +69788,8 @@ var DEVICE_LOCAL_STATE_CAPS = {
69420
69788
  featureProbe: featureProbeCapability,
69421
69789
  flood: floodCapability,
69422
69790
  gas: gasCapability,
69791
+ groupCommands: groupCommandsCapability,
69792
+ groupStatus: groupStatusCapability,
69423
69793
  heatingValve: heatingValveCapability,
69424
69794
  humidifier: humidifierCapability,
69425
69795
  humiditySensor: humiditySensorCapability,
@@ -70313,6 +70683,13 @@ Object.freeze({
70313
70683
  access: "view",
70314
70684
  role: "admin"
70315
70685
  },
70686
+ "addons.getPackageDocs": {
70687
+ capName: "addons",
70688
+ capScope: "system",
70689
+ addonId: null,
70690
+ access: "view",
70691
+ role: "admin"
70692
+ },
70316
70693
  "addons.getVersions": {
70317
70694
  capName: "addons",
70318
70695
  capScope: "system",
@@ -73421,6 +73798,20 @@ Object.freeze({
73421
73798
  access: "view",
73422
73799
  role: "viewer"
73423
73800
  },
73801
+ "groupCommands.getStatus": {
73802
+ capName: "group-commands",
73803
+ capScope: "device",
73804
+ addonId: null,
73805
+ access: "view",
73806
+ role: "viewer"
73807
+ },
73808
+ "groupStatus.getStatus": {
73809
+ capName: "group-status",
73810
+ capScope: "device",
73811
+ addonId: null,
73812
+ access: "view",
73813
+ role: "viewer"
73814
+ },
73424
73815
  "heatingValve.getStatus": {
73425
73816
  capName: "heating-valve",
73426
73817
  capScope: "device",
@@ -83243,6 +83634,37 @@ var SENSOR_CAP_VOCABULARY = {
83243
83634
  icon: "flame"
83244
83635
  }]
83245
83636
  },
83637
+ "group-commands": {
83638
+ scope: "device",
83639
+ noun: t("Group command", "Comando di gruppo")
83640
+ },
83641
+ "group-status": {
83642
+ scope: "device",
83643
+ noun: t("Group", "Gruppo"),
83644
+ state: [
83645
+ {
83646
+ kind: "numeric",
83647
+ field: "included",
83648
+ quantity: t("the members reporting", "i membri che rispondono"),
83649
+ label: t("Reporting", "Rispondono"),
83650
+ icon: "users"
83651
+ },
83652
+ {
83653
+ kind: "numeric",
83654
+ field: "excluded",
83655
+ quantity: t("the members left out", "i membri esclusi"),
83656
+ label: t("Excluded", "Esclusi"),
83657
+ icon: "users"
83658
+ },
83659
+ {
83660
+ kind: "numeric",
83661
+ field: "total",
83662
+ quantity: t("the member count", "il numero di membri"),
83663
+ label: t("Members", "Membri"),
83664
+ icon: "sigma"
83665
+ }
83666
+ ]
83667
+ },
83246
83668
  "humidity-sensor": {
83247
83669
  scope: "device",
83248
83670
  noun: t("Humidity", "Umidità"),
@@ -83276,7 +83698,6 @@ var SENSOR_CAP_VOCABULARY = {
83276
83698
  field: "value",
83277
83699
  quantity: t("the reading", "il valore"),
83278
83700
  label: t("Reading", "Valore"),
83279
- unitField: "unit",
83280
83701
  icon: "gauge"
83281
83702
  }]
83282
83703
  },
@@ -83412,7 +83833,7 @@ var SENSOR_CAP_VOCABULARY = {
83412
83833
  field: "temperature",
83413
83834
  quantity: t("the outdoor temperature", "la temperatura esterna"),
83414
83835
  label: t("Temperature", "Temperatura"),
83415
- unitField: "temperatureUnit",
83836
+ unit: "°C",
83416
83837
  icon: "thermometer"
83417
83838
  },
83418
83839
  {
@@ -83428,7 +83849,7 @@ var SENSOR_CAP_VOCABULARY = {
83428
83849
  field: "windSpeed",
83429
83850
  quantity: t("the wind speed", "la velocità del vento"),
83430
83851
  label: t("Wind", "Vento"),
83431
- unitField: "windSpeedUnit",
83852
+ unit: "m/s",
83432
83853
  icon: "wind"
83433
83854
  },
83434
83855
  {
@@ -83436,7 +83857,7 @@ var SENSOR_CAP_VOCABULARY = {
83436
83857
  field: "pressure",
83437
83858
  quantity: t("the air pressure", "la pressione atmosferica"),
83438
83859
  label: t("Pressure", "Pressione"),
83439
- unitField: "pressureUnit",
83860
+ unit: "hPa",
83440
83861
  icon: "gauge"
83441
83862
  }
83442
83863
  ]
@@ -84709,6 +85130,16 @@ Object.freeze({
84709
85130
  form: "single",
84710
85131
  optional: false
84711
85132
  }],
85133
+ "groupCommands.getStatus": [{
85134
+ name: "deviceId",
85135
+ form: "single",
85136
+ optional: false
85137
+ }],
85138
+ "groupStatus.getStatus": [{
85139
+ name: "deviceId",
85140
+ form: "single",
85141
+ optional: false
85142
+ }],
84712
85143
  "heatingValve.getStatus": [{
84713
85144
  name: "deviceId",
84714
85145
  form: "single",
@@ -86693,6 +87124,8 @@ new Map(Object.entries({
86693
87124
  "detection-blind": "device-events",
86694
87125
  "motion-noisy": "device-events",
86695
87126
  "motion-calm": "device-events",
87127
+ "package-llm-silent": "device-events",
87128
+ "package-llm-answered": "device-events",
86696
87129
  "recording-stalled": "device-events",
86697
87130
  "recording-healthy": "device-events",
86698
87131
  "recording-degraded": "device-events",
@@ -86776,6 +87209,12 @@ var NC_SYSTEM_EVENT_KIND_GROUP_SPECS = [
86776
87209
  label: "Motion without a subject",
86777
87210
  kinds: ["motion-noisy", "motion-calm"]
86778
87211
  },
87212
+ {
87213
+ id: "package-llm",
87214
+ family: "device-events",
87215
+ label: "Package confirm",
87216
+ kinds: ["package-llm-silent", "package-llm-answered"]
87217
+ },
86779
87218
  {
86780
87219
  id: "recording",
86781
87220
  family: "device-events",
@@ -86957,6 +87396,8 @@ var DEVICE_EVENTS = [
86957
87396
  "detection-blind",
86958
87397
  "motion-noisy",
86959
87398
  "motion-calm",
87399
+ "package-llm-silent",
87400
+ "package-llm-answered",
86960
87401
  "recording-stalled",
86961
87402
  "recording-healthy",
86962
87403
  "recording-degraded",
@@ -87225,6 +87666,15 @@ new Map([
87225
87666
  snapshotLabel: "Cameras with constant motion now",
87226
87667
  snapshotSection: null
87227
87668
  },
87669
+ {
87670
+ id: "package-llm-confirm",
87671
+ family: "device-events",
87672
+ label: "Package confirm (not answering / answering again)",
87673
+ bad: ["package-llm-silent"],
87674
+ recovery: ["package-llm-answered"],
87675
+ snapshotLabel: "Cameras whose package confirm is not answering",
87676
+ snapshotSection: null
87677
+ },
87228
87678
  {
87229
87679
  id: "recording-health",
87230
87680
  family: "device-events",