@camstack/addon-agent-ui 1.2.269 → 1.2.271

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
@@ -5435,7 +5435,7 @@ var ZodIssueCode = {
5435
5435
  var ZodFirstPartyTypeKind;
5436
5436
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5437
5437
  //#endregion
5438
- //#region ../types/dist/sleep-BErAxhul.mjs
5438
+ //#region ../types/dist/sleep-B81gwQuR.mjs
5439
5439
  /**
5440
5440
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5441
5441
  * window to float samples (D455).
@@ -7922,6 +7922,99 @@ var RecordKindDeclarationSchema = object({
7922
7922
  }).strict();
7923
7923
  /** The kind names: lower-case, dash separated, short enough for an index name. */
7924
7924
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
7925
+ var READING_QUANTITIES = [
7926
+ "temperature",
7927
+ "pressure",
7928
+ "speed",
7929
+ "precipitation",
7930
+ "precipitation-rate",
7931
+ "length",
7932
+ "area",
7933
+ "illuminance",
7934
+ "irradiance",
7935
+ "power",
7936
+ "energy",
7937
+ "voltage",
7938
+ "current",
7939
+ "apparent-power",
7940
+ "humidity",
7941
+ "percentage",
7942
+ "concentration-volume",
7943
+ "concentration-mass",
7944
+ "concentration-ppb",
7945
+ "volume",
7946
+ "duration",
7947
+ "duration-ms",
7948
+ "information",
7949
+ "sound-level",
7950
+ "signal-level",
7951
+ "colour-temperature",
7952
+ "angle",
7953
+ "bitrate",
7954
+ "rate",
7955
+ "mass",
7956
+ "index",
7957
+ "ratio",
7958
+ "count",
7959
+ "none",
7960
+ "device"
7961
+ ];
7962
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
7963
+ new Set(READING_QUANTITIES);
7964
+ /**
7965
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
7966
+ * (or an index / ratio / count). `device` means the cap has no single unit.
7967
+ */
7968
+ var QUANTITY_BASE = {
7969
+ temperature: "°C",
7970
+ pressure: "hPa",
7971
+ speed: "m/s",
7972
+ precipitation: "mm",
7973
+ "precipitation-rate": "mm/h",
7974
+ length: "m",
7975
+ area: "m²",
7976
+ illuminance: "lx",
7977
+ irradiance: "W/m²",
7978
+ power: "W",
7979
+ energy: "kWh",
7980
+ voltage: "V",
7981
+ current: "A",
7982
+ "apparent-power": "VA",
7983
+ humidity: "%",
7984
+ percentage: "%",
7985
+ "concentration-volume": "ppm",
7986
+ "concentration-mass": "µg/m³",
7987
+ "concentration-ppb": "ppb",
7988
+ volume: "L",
7989
+ duration: "s",
7990
+ "duration-ms": "ms",
7991
+ information: "B",
7992
+ "sound-level": "dBFS",
7993
+ "signal-level": "dBm",
7994
+ "colour-temperature": "mired",
7995
+ angle: "°",
7996
+ bitrate: "kbps",
7997
+ rate: "fps",
7998
+ mass: "kg",
7999
+ index: "none",
8000
+ ratio: "none",
8001
+ count: "none",
8002
+ none: "none",
8003
+ device: "device"
8004
+ };
8005
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8006
+ function baseUnitOf(quantity) {
8007
+ const symbol = QUANTITY_BASE[quantity];
8008
+ if (symbol === "none" || symbol === "device") return void 0;
8009
+ return symbol;
8010
+ }
8011
+ function readingMeta(quantity, extras) {
8012
+ return {
8013
+ quantity,
8014
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8015
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8016
+ };
8017
+ }
7925
8018
  /** Shared Zod schemas used across streaming capabilities. */
7926
8019
  var CamProfileSchema = _enum([
7927
8020
  "high",
@@ -7949,8 +8042,8 @@ var CamStreamKindSchema = _enum([
7949
8042
  "derived"
7950
8043
  ]);
7951
8044
  var CamStreamResolutionSchema = object({
7952
- width: number().int().positive(),
7953
- height: number().int().positive()
8045
+ width: number().int().positive().meta(readingMeta("none")),
8046
+ height: number().int().positive().meta(readingMeta("none"))
7954
8047
  });
7955
8048
  var CameraStreamSchema = object({
7956
8049
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8482,6 +8575,7 @@ var CAP_NAMES = Object.freeze([
8482
8575
  "filesystem-browse",
8483
8576
  "flood",
8484
8577
  "gas",
8578
+ "group-status",
8485
8579
  "heating-valve",
8486
8580
  "humidifier",
8487
8581
  "humidity-sensor",
@@ -8611,14 +8705,6 @@ Object.freeze({
8611
8705
  {
8612
8706
  "path": "aqi",
8613
8707
  "valueType": "number"
8614
- },
8615
- {
8616
- "path": "unit",
8617
- "valueType": "string"
8618
- },
8619
- {
8620
- "path": "precision",
8621
- "valueType": "number"
8622
8708
  }
8623
8709
  ],
8624
8710
  "alarm-panel": [
@@ -8654,20 +8740,10 @@ Object.freeze({
8654
8740
  "valueType": "boolean"
8655
8741
  }
8656
8742
  ],
8657
- "ambient-light-sensor": [
8658
- {
8659
- "path": "lux",
8660
- "valueType": "number"
8661
- },
8662
- {
8663
- "path": "unit",
8664
- "valueType": "string"
8665
- },
8666
- {
8667
- "path": "precision",
8668
- "valueType": "number"
8669
- }
8670
- ],
8743
+ "ambient-light-sensor": [{
8744
+ "path": "lux",
8745
+ "valueType": "number"
8746
+ }],
8671
8747
  "audio-metrics": [
8672
8748
  {
8673
8749
  "path": "ts",
@@ -8747,6 +8823,10 @@ Object.freeze({
8747
8823
  "path": "lastUpdated",
8748
8824
  "valueType": "number"
8749
8825
  },
8826
+ {
8827
+ "path": "volts",
8828
+ "valueType": "number"
8829
+ },
8750
8830
  {
8751
8831
  "path": "lastContactAt",
8752
8832
  "valueType": "number"
@@ -8989,10 +9069,6 @@ Object.freeze({
8989
9069
  "datetime"
8990
9070
  ]
8991
9071
  },
8992
- {
8993
- "path": "unit",
8994
- "valueType": "string"
8995
- },
8996
9072
  {
8997
9073
  "path": "min",
8998
9074
  "valueType": "number"
@@ -9004,19 +9080,6 @@ Object.freeze({
9004
9080
  {
9005
9081
  "path": "step",
9006
9082
  "valueType": "number"
9007
- },
9008
- {
9009
- "path": "precision",
9010
- "valueType": "number"
9011
- },
9012
- {
9013
- "path": "format",
9014
- "valueType": "enum",
9015
- "enumValues": [
9016
- "date",
9017
- "time",
9018
- "datetime"
9019
- ]
9020
9083
  }
9021
9084
  ],
9022
9085
  "cover": [
@@ -9257,13 +9320,8 @@ Object.freeze({
9257
9320
  "path": "value",
9258
9321
  "valueType": "string"
9259
9322
  }, {
9260
- "path": "format",
9261
- "valueType": "enum",
9262
- "enumValues": [
9263
- "date",
9264
- "time",
9265
- "datetime"
9266
- ]
9323
+ "path": "timestamp",
9324
+ "valueType": "boolean"
9267
9325
  }],
9268
9326
  "event-emitter": [
9269
9327
  {
@@ -9336,6 +9394,20 @@ Object.freeze({
9336
9394
  "path": "detected",
9337
9395
  "valueType": "boolean"
9338
9396
  }],
9397
+ "group-status": [
9398
+ {
9399
+ "path": "included",
9400
+ "valueType": "number"
9401
+ },
9402
+ {
9403
+ "path": "excluded",
9404
+ "valueType": "number"
9405
+ },
9406
+ {
9407
+ "path": "total",
9408
+ "valueType": "number"
9409
+ }
9410
+ ],
9339
9411
  "heating-valve": [{
9340
9412
  "path": "position",
9341
9413
  "valueType": "number"
@@ -9370,20 +9442,10 @@ Object.freeze({
9370
9442
  "valueType": "number"
9371
9443
  }
9372
9444
  ],
9373
- "humidity-sensor": [
9374
- {
9375
- "path": "percent",
9376
- "valueType": "number"
9377
- },
9378
- {
9379
- "path": "unit",
9380
- "valueType": "string"
9381
- },
9382
- {
9383
- "path": "precision",
9384
- "valueType": "number"
9385
- }
9386
- ],
9445
+ "humidity-sensor": [{
9446
+ "path": "percent",
9447
+ "valueType": "number"
9448
+ }],
9387
9449
  "image": [{
9388
9450
  "path": "url",
9389
9451
  "valueType": "string"
@@ -9722,20 +9784,10 @@ Object.freeze({
9722
9784
  "valueType": "boolean"
9723
9785
  }
9724
9786
  ],
9725
- "numeric-sensor": [
9726
- {
9727
- "path": "value",
9728
- "valueType": "number"
9729
- },
9730
- {
9731
- "path": "unit",
9732
- "valueType": "string"
9733
- },
9734
- {
9735
- "path": "precision",
9736
- "valueType": "number"
9737
- }
9738
- ],
9787
+ "numeric-sensor": [{
9788
+ "path": "value",
9789
+ "valueType": "number"
9790
+ }],
9739
9791
  "pet-feeder": [
9740
9792
  {
9741
9793
  "path": "foodLevel",
@@ -9827,14 +9879,6 @@ Object.freeze({
9827
9879
  {
9828
9880
  "path": "powerFactor",
9829
9881
  "valueType": "number"
9830
- },
9831
- {
9832
- "path": "unit",
9833
- "valueType": "string"
9834
- },
9835
- {
9836
- "path": "precision",
9837
- "valueType": "number"
9838
9882
  }
9839
9883
  ],
9840
9884
  "presence": [
@@ -9863,20 +9907,10 @@ Object.freeze({
9863
9907
  "valueType": "number"
9864
9908
  }
9865
9909
  ],
9866
- "pressure-sensor": [
9867
- {
9868
- "path": "hpa",
9869
- "valueType": "number"
9870
- },
9871
- {
9872
- "path": "unit",
9873
- "valueType": "string"
9874
- },
9875
- {
9876
- "path": "precision",
9877
- "valueType": "number"
9878
- }
9879
- ],
9910
+ "pressure-sensor": [{
9911
+ "path": "hpa",
9912
+ "valueType": "number"
9913
+ }],
9880
9914
  "privacy-mask": [{
9881
9915
  "path": "enabled",
9882
9916
  "valueType": "boolean"
@@ -10187,20 +10221,10 @@ Object.freeze({
10187
10221
  "path": "tampered",
10188
10222
  "valueType": "boolean"
10189
10223
  }],
10190
- "temperature-sensor": [
10191
- {
10192
- "path": "celsius",
10193
- "valueType": "number"
10194
- },
10195
- {
10196
- "path": "unit",
10197
- "valueType": "string"
10198
- },
10199
- {
10200
- "path": "precision",
10201
- "valueType": "number"
10202
- }
10203
- ],
10224
+ "temperature-sensor": [{
10225
+ "path": "celsius",
10226
+ "valueType": "number"
10227
+ }],
10204
10228
  "update": [
10205
10229
  {
10206
10230
  "path": "currentVersion",
@@ -10311,10 +10335,6 @@ Object.freeze({
10311
10335
  "path": "temperature",
10312
10336
  "valueType": "number"
10313
10337
  },
10314
- {
10315
- "path": "temperatureUnit",
10316
- "valueType": "string"
10317
- },
10318
10338
  {
10319
10339
  "path": "humidity",
10320
10340
  "valueType": "number"
@@ -10323,18 +10343,10 @@ Object.freeze({
10323
10343
  "path": "pressure",
10324
10344
  "valueType": "number"
10325
10345
  },
10326
- {
10327
- "path": "pressureUnit",
10328
- "valueType": "string"
10329
- },
10330
10346
  {
10331
10347
  "path": "windSpeed",
10332
10348
  "valueType": "number"
10333
10349
  },
10334
- {
10335
- "path": "windSpeedUnit",
10336
- "valueType": "string"
10337
- },
10338
10350
  {
10339
10351
  "path": "windBearing",
10340
10352
  "valueType": "number"
@@ -10727,7 +10739,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
10727
10739
  return DeviceRole;
10728
10740
  }({});
10729
10741
  //#endregion
10730
- //#region ../types/dist/composition-kEl9mevu.mjs
10742
+ //#region ../types/dist/composition-B85l9j1M.mjs
10731
10743
  /**
10732
10744
  * The ffmpeg build every CamStack node downloads, per platform and arch.
10733
10745
  *
@@ -11482,15 +11494,67 @@ function validateExpressionSource(src) {
11482
11494
  }
11483
11495
  return null;
11484
11496
  }
11485
- /** A composed device's stableId is this prefix plus the block id. */
11486
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11487
- /** Longest snippet a `code` source may carry (slice 4). */
11488
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11497
+ /**
11498
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11499
+ * id. Split out so helper schemas can name it without a cycle through
11500
+ * `composition.ts` (D659).
11501
+ */
11489
11502
  var CompositionSourceRefSchema = object({
11490
11503
  /** The owning addon; `stableId` is unique only within it. */
11491
11504
  addonId: string().min(1),
11492
11505
  stableId: string().min(1)
11493
11506
  });
11507
+ var GROUP_TEMPLATE_IDS = [
11508
+ "temperature",
11509
+ "humidity",
11510
+ "pressure",
11511
+ "illuminance",
11512
+ "power",
11513
+ "energy",
11514
+ "battery",
11515
+ "numeric",
11516
+ "motion",
11517
+ "contact",
11518
+ "leak",
11519
+ "smoke",
11520
+ "occupancy",
11521
+ "binary"
11522
+ ];
11523
+ var NUMERIC_AGGREGATES = [
11524
+ "mean",
11525
+ "median",
11526
+ "min",
11527
+ "max",
11528
+ "sum"
11529
+ ];
11530
+ var BINARY_AGGREGATES = ["any", "all"];
11531
+ var AGGREGATE_REDUCERS = [
11532
+ "mean",
11533
+ "median",
11534
+ "min",
11535
+ "max",
11536
+ "sum",
11537
+ "any",
11538
+ "all"
11539
+ ];
11540
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11541
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11542
+ /** One member. The field is stored so occupancy can name motion or binary. */
11543
+ var HelperMemberSchema = object({
11544
+ source: CompositionSourceRefSchema,
11545
+ cap: string().min(1),
11546
+ fieldPath: string().min(1)
11547
+ });
11548
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11549
+ kind: literal("group"),
11550
+ templateId: GroupTemplateIdSchema,
11551
+ aggregate: AggregateReducerSchema,
11552
+ members: array(HelperMemberSchema).min(1).max(64)
11553
+ })]);
11554
+ /** A composed device's stableId is this prefix plus the block id. */
11555
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11556
+ /** Longest snippet a `code` source may carry (slice 4). */
11557
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11494
11558
  /** One field of one capability of one source device. */
11495
11559
  var CompositionFieldReadSchema = object({
11496
11560
  source: CompositionSourceRefSchema,
@@ -11534,7 +11598,33 @@ var CompositionCodeSourceSchema = object({
11534
11598
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11535
11599
  CompositionFromSourceSchema,
11536
11600
  CompositionExpressionSourceSchema,
11537
- CompositionCodeSourceSchema
11601
+ CompositionCodeSourceSchema,
11602
+ object({
11603
+ kind: literal("aggregate"),
11604
+ reducer: _enum([
11605
+ "mean",
11606
+ "median",
11607
+ "min",
11608
+ "max",
11609
+ "sum",
11610
+ "any",
11611
+ "all"
11612
+ ]),
11613
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11614
+ /** `value` is the reading; the others are the member counts. */
11615
+ publish: _enum([
11616
+ "value",
11617
+ "included",
11618
+ "excluded",
11619
+ "total"
11620
+ ]),
11621
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11622
+ empty: _enum([
11623
+ "null",
11624
+ "omit",
11625
+ "offline"
11626
+ ])
11627
+ })
11538
11628
  ]);
11539
11629
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11540
11630
  kind: literal("forward"),
@@ -11580,7 +11670,13 @@ var CompositionSchema = object({
11580
11670
  kind: literal("existing"),
11581
11671
  device: CompositionSourceRefSchema
11582
11672
  })]),
11583
- features: array(CompositionFeatureSchema).min(1).max(16)
11673
+ features: array(CompositionFeatureSchema).min(1).max(16),
11674
+ /**
11675
+ * Present when the block is a helper template. Absent on a free
11676
+ * composition (the room example, device 4603). The editor reads this,
11677
+ * never the compiled features.
11678
+ */
11679
+ helper: HelperSpecSchema.optional()
11584
11680
  }).superRefine((composition, ctx) => {
11585
11681
  const seen = /* @__PURE__ */ new Set();
11586
11682
  composition.features.forEach((feature, index) => {
@@ -17682,7 +17778,11 @@ var CompositionProblemSchema = object({
17682
17778
  "item-field-reserved",
17683
17779
  "target-missing",
17684
17780
  "target-unreadable",
17685
- "not-replaceable"
17781
+ "not-replaceable",
17782
+ "native-capability-replace",
17783
+ "mixed-quantity",
17784
+ "helper-mismatch",
17785
+ "member-constraint"
17686
17786
  ]),
17687
17787
  path: string(),
17688
17788
  message: string()
@@ -17703,7 +17803,8 @@ var CompositionUnavailableModeSchema = _enum([
17703
17803
  "null",
17704
17804
  "offline",
17705
17805
  "unknown",
17706
- "release"
17806
+ "release",
17807
+ "omit"
17707
17808
  ]);
17708
17809
  /**
17709
17810
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -17740,9 +17841,11 @@ var CompositionFieldPlanSchema = object({
17740
17841
  reads: array(CompositionFieldReadSchema)
17741
17842
  });
17742
17843
  /**
17743
- * What a feature does to its target (D663): `new` builds a capability on a new
17744
- * device; on an existing device it `add`s a capability the device lacks, or
17745
- * `replace`s named fields of one it has natively.
17844
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
17845
+ * new device; on an existing device it `add`s a capability the device lacks.
17846
+ * Replacing a capability the device already serves is refused
17847
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
17848
+ * wire for a claim the hub already holds.
17746
17849
  */
17747
17850
  var CompositionFeatureModeSchema = _enum([
17748
17851
  "new",
@@ -17915,6 +18018,235 @@ Object.freeze({
17915
18018
  result: ["number", "null"]
17916
18019
  }
17917
18020
  });
18021
+ /**
18022
+ * One catalog row per base reading a group helper can build (D944 §3).
18023
+ * Data, not a form default written into the block.
18024
+ */
18025
+ var NOW = {
18026
+ fieldPath: "lastFetchedAt",
18027
+ expr: "now"
18028
+ };
18029
+ var CHANGED = {
18030
+ fieldPath: "lastChangedAt",
18031
+ expr: "now"
18032
+ };
18033
+ function numeric(input) {
18034
+ return {
18035
+ ...input,
18036
+ family: "numeric",
18037
+ deviceType: DeviceType.Sensor,
18038
+ memberDeviceType: DeviceType.Sensor,
18039
+ baseUnit: baseUnitOf(input.quantity),
18040
+ aggregates: NUMERIC_AGGREGATES,
18041
+ accept: [{
18042
+ cap: input.outputCap,
18043
+ fieldPath: input.outputField
18044
+ }]
18045
+ };
18046
+ }
18047
+ function binary(input) {
18048
+ return {
18049
+ ...input,
18050
+ family: "binary",
18051
+ deviceType: DeviceType.Sensor,
18052
+ memberDeviceType: DeviceType.Sensor,
18053
+ quantity: "none",
18054
+ baseUnit: void 0,
18055
+ aggregates: BINARY_AGGREGATES,
18056
+ empty: "offline"
18057
+ };
18058
+ }
18059
+ var GROUP_TEMPLATES = [
18060
+ numeric({
18061
+ id: "temperature",
18062
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18063
+ role: DeviceRole.TemperatureSensor,
18064
+ outputCap: "temperature-sensor",
18065
+ outputField: "celsius",
18066
+ quantity: "temperature",
18067
+ defaultAggregate: "mean",
18068
+ empty: "null",
18069
+ companions: [NOW]
18070
+ }),
18071
+ numeric({
18072
+ id: "humidity",
18073
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18074
+ role: DeviceRole.HumiditySensor,
18075
+ outputCap: "humidity-sensor",
18076
+ outputField: "percent",
18077
+ quantity: "humidity",
18078
+ defaultAggregate: "mean",
18079
+ empty: "null",
18080
+ companions: [NOW]
18081
+ }),
18082
+ numeric({
18083
+ id: "pressure",
18084
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18085
+ role: DeviceRole.PressureSensor,
18086
+ outputCap: "pressure-sensor",
18087
+ outputField: "hpa",
18088
+ quantity: "pressure",
18089
+ defaultAggregate: "mean",
18090
+ empty: "null",
18091
+ companions: [NOW]
18092
+ }),
18093
+ numeric({
18094
+ id: "illuminance",
18095
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18096
+ role: DeviceRole.AmbientLightSensor,
18097
+ outputCap: "ambient-light-sensor",
18098
+ outputField: "lux",
18099
+ quantity: "illuminance",
18100
+ defaultAggregate: "mean",
18101
+ empty: "null",
18102
+ companions: [NOW]
18103
+ }),
18104
+ numeric({
18105
+ id: "power",
18106
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18107
+ role: DeviceRole.PowerSensor,
18108
+ outputCap: "power-meter",
18109
+ outputField: "watts",
18110
+ quantity: "power",
18111
+ defaultAggregate: "sum",
18112
+ empty: "omit",
18113
+ companions: [NOW]
18114
+ }),
18115
+ numeric({
18116
+ id: "energy",
18117
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18118
+ role: DeviceRole.EnergySensor,
18119
+ outputCap: "power-meter",
18120
+ outputField: "kwhTotal",
18121
+ quantity: "energy",
18122
+ defaultAggregate: "sum",
18123
+ empty: "omit",
18124
+ companions: [NOW]
18125
+ }),
18126
+ numeric({
18127
+ id: "battery",
18128
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18129
+ role: DeviceRole.BatterySensor,
18130
+ outputCap: "battery",
18131
+ outputField: "percentage",
18132
+ quantity: "percentage",
18133
+ defaultAggregate: "mean",
18134
+ empty: "null",
18135
+ companions: [
18136
+ {
18137
+ fieldPath: "charging",
18138
+ expr: "\"unknown\""
18139
+ },
18140
+ {
18141
+ fieldPath: "sleeping",
18142
+ expr: "false"
18143
+ },
18144
+ {
18145
+ fieldPath: "lastUpdated",
18146
+ expr: "now"
18147
+ }
18148
+ ]
18149
+ }),
18150
+ numeric({
18151
+ id: "numeric",
18152
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18153
+ role: DeviceRole.NumericSensor,
18154
+ outputCap: "numeric-sensor",
18155
+ outputField: "value",
18156
+ quantity: "device",
18157
+ defaultAggregate: "mean",
18158
+ empty: "null",
18159
+ companions: [NOW]
18160
+ }),
18161
+ binary({
18162
+ id: "motion",
18163
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18164
+ role: DeviceRole.MotionSensor,
18165
+ outputCap: "motion",
18166
+ outputField: "detected",
18167
+ defaultAggregate: "any",
18168
+ accept: [{
18169
+ cap: "motion",
18170
+ fieldPath: "detected"
18171
+ }],
18172
+ companions: [{
18173
+ fieldPath: "lastDetectedAt",
18174
+ expr: "null"
18175
+ }, {
18176
+ fieldPath: "autoClearAfterMs",
18177
+ expr: "null"
18178
+ }]
18179
+ }),
18180
+ binary({
18181
+ id: "contact",
18182
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18183
+ role: DeviceRole.ContactSensor,
18184
+ outputCap: "contact",
18185
+ outputField: "entryOpen",
18186
+ defaultAggregate: "any",
18187
+ accept: [{
18188
+ cap: "contact",
18189
+ fieldPath: "entryOpen"
18190
+ }],
18191
+ companions: [CHANGED]
18192
+ }),
18193
+ binary({
18194
+ id: "leak",
18195
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18196
+ role: DeviceRole.LeakSensor,
18197
+ outputCap: "flood",
18198
+ outputField: "flooded",
18199
+ defaultAggregate: "any",
18200
+ accept: [{
18201
+ cap: "flood",
18202
+ fieldPath: "flooded"
18203
+ }],
18204
+ companions: [CHANGED]
18205
+ }),
18206
+ binary({
18207
+ id: "smoke",
18208
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18209
+ role: DeviceRole.SmokeSensor,
18210
+ outputCap: "smoke",
18211
+ outputField: "detected",
18212
+ defaultAggregate: "any",
18213
+ accept: [{
18214
+ cap: "smoke",
18215
+ fieldPath: "detected"
18216
+ }],
18217
+ companions: [CHANGED]
18218
+ }),
18219
+ binary({
18220
+ id: "occupancy",
18221
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18222
+ role: DeviceRole.BinarySensor,
18223
+ outputCap: "binary",
18224
+ outputField: "on",
18225
+ defaultAggregate: "any",
18226
+ accept: [{
18227
+ cap: "motion",
18228
+ fieldPath: "detected"
18229
+ }, {
18230
+ cap: "binary",
18231
+ fieldPath: "on"
18232
+ }],
18233
+ companions: [CHANGED]
18234
+ }),
18235
+ binary({
18236
+ id: "binary",
18237
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18238
+ role: DeviceRole.BinarySensor,
18239
+ outputCap: "binary",
18240
+ outputField: "on",
18241
+ defaultAggregate: "any",
18242
+ accept: [{
18243
+ cap: "binary",
18244
+ fieldPath: "on"
18245
+ }],
18246
+ companions: [CHANGED]
18247
+ })
18248
+ ];
18249
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
17918
18250
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
17919
18251
  /**
17920
18252
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -19628,12 +19960,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
19628
19960
  * providers detect renames by matching `uniqueId` and patch `id`
19629
19961
  * in-place so dispatch keeps working.
19630
19962
  *
19631
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
19632
- * `precision`) live in the cap STATUS SLICE so they flow live with every
19633
- * state push and are the single source of truth. The `device_class` tag is
19634
- * an HA-specific input that is consumed at the discovery boundary
19635
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
19636
- * stored after that point.
19963
+ * **Display unit and precision are NOT stored here.** They are a read-time
19964
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
19965
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
19966
+ * neither is repeated on each sample. The `device_class` tag is an
19967
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
19968
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
19637
19969
  *
19638
19970
  * The `raw` escape hatch carries provider-specific attributes that don't
19639
19971
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -19647,6 +19979,14 @@ var SourceInfoSchema = object({
19647
19979
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
19648
19980
  * Used to detect rename when `id` changes. */
19649
19981
  uniqueId: string().optional(),
19982
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
19983
+ quantity: ReadingQuantitySchema.optional(),
19984
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
19985
+ dateTimeShape: _enum([
19986
+ "date",
19987
+ "time",
19988
+ "datetime"
19989
+ ]).optional(),
19650
19990
  /** Provider-specific extras that don't fit the structured slots.
19651
19991
  * Whitelist what goes here per provider — do NOT dump entire HA
19652
19992
  * attribute blobs (would explode the meta JSON column). */
@@ -40559,7 +40899,8 @@ method(object({
40559
40899
  "enum"
40560
40900
  ]),
40561
40901
  enumValues: array(string()).optional(),
40562
- item: boolean().optional()
40902
+ item: boolean().optional(),
40903
+ quantity: ReadingQuantitySchema.optional()
40563
40904
  })).readonly(),
40564
40905
  itemArray: object({
40565
40906
  path: string(),
@@ -44359,8 +44700,8 @@ var MaskPolygonShapeSchema = object({
44359
44700
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44360
44701
  var MaskGridShapeSchema = object({
44361
44702
  kind: literal("grid"),
44362
- gridWidth: number(),
44363
- gridHeight: number(),
44703
+ gridWidth: number().meta(readingMeta("none")),
44704
+ gridHeight: number().meta(readingMeta("none")),
44364
44705
  cells: array(boolean())
44365
44706
  });
44366
44707
  discriminatedUnion("kind", [
@@ -55754,46 +56095,27 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
55754
56095
  }), LogStreamEntrySchema, { kind: "subscription" });
55755
56096
  object({
55756
56097
  /** Carbon dioxide concentration in ppm. */
55757
- co2Ppm: number().min(0).optional(),
56098
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
55758
56099
  /** Total volatile organic compounds in ppb. */
55759
- vocPpb: number().min(0).optional(),
56100
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
55760
56101
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
55761
- pm25: number().min(0).optional(),
56102
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
55762
56103
  /** Particulate matter ≤ 10 μm in µg/m³. */
55763
- pm10: number().min(0).optional(),
56104
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
55764
56105
  /** Composite AQI value (typically 0..500). */
55765
- aqi: number().optional(),
56106
+ aqi: number().optional().meta(readingMeta("index")),
55766
56107
  /** Ms epoch when the slice was last updated. */
55767
- lastFetchedAt: number(),
55768
- /** Live display unit of the single metric this slice carries (e.g. HA
55769
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
55770
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
55771
- * per slice is unambiguous. */
55772
- unit: string().optional(),
55773
- /** Suggested decimal places for numeric display.
55774
- * Populated live from the upstream source when provided (e.g. HA
55775
- * `attributes.suggested_display_precision`). Falls back to
55776
- * auto-formatting when absent. */
55777
- precision: number().int().min(0).max(10).optional()
56108
+ lastFetchedAt: number()
55778
56109
  });
55779
56110
  DeviceType.Sensor;
55780
56111
  object({
55781
- /** Current illuminance in lux (lx). */
55782
- lux: number().min(0).meta({
55783
- unit: "lx",
56112
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56113
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56114
+ precision: 0,
55784
56115
  label: "Illuminance"
55785
- }),
56116
+ })),
55786
56117
  /** Ms epoch when the slice was last updated. */
55787
- lastFetchedAt: number(),
55788
- /** Live display unit from the upstream source (e.g. HA
55789
- * `attributes.unit_of_measurement`). The UI prefers this over the
55790
- * role's canonical unit. Absent → fall back to the canonical unit. */
55791
- unit: string().optional(),
55792
- /** Suggested decimal places for numeric display.
55793
- * Populated live from the upstream source when provided (e.g. HA
55794
- * `attributes.suggested_display_precision`). Falls back to
55795
- * auto-formatting when absent. */
55796
- precision: number().int().min(0).max(10).optional()
56118
+ lastFetchedAt: number()
55797
56119
  });
55798
56120
  DeviceType.Sensor;
55799
56121
  /**
@@ -55938,10 +56260,7 @@ var BatteryStatusSchema = object({
55938
56260
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
55939
56261
  * declines to band a non-finite reading, which is the correct shape.
55940
56262
  */
55941
- percentage: number().min(0).max(100).nullable().meta({
55942
- unit: "%",
55943
- label: "Charge"
55944
- }),
56263
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
55945
56264
  /**
55946
56265
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
55947
56266
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -55968,7 +56287,14 @@ var BatteryStatusSchema = object({
55968
56287
  */
55969
56288
  sleeping: boolean().meta({ label: "Sleeping" }),
55970
56289
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
55971
- lastUpdated: number(),
56290
+ lastUpdated: number().meta(readingMeta("none")),
56291
+ /**
56292
+ * Battery voltage in volts, for a cell the firmware reports as voltage
56293
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
56294
+ * that only report charge. A second reading on this cap, not a second
56295
+ * `numeric-sensor` on the same device.
56296
+ */
56297
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
55972
56298
  /**
55973
56299
  * Ms epoch of the last time the device PROVED it was reachable — a
55974
56300
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56028,7 +56354,7 @@ object({
56028
56354
  DeviceType.Sensor;
56029
56355
  object({
56030
56356
  /** Current level as 0..100 inclusive. Firmware-reported. */
56031
- percentage: number().min(0).max(100),
56357
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56032
56358
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56033
56359
  lastChangedAt: number()
56034
56360
  });
@@ -56413,21 +56739,21 @@ object({
56413
56739
  /** Single setpoint in Celsius. Used by single-target modes
56414
56740
  * (heat / cool / dry). Null when the device is in a range mode
56415
56741
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
56416
- target: number().nullable(),
56742
+ target: number().nullable().meta(readingMeta("temperature")),
56417
56743
  /** Upper bound of the dual setpoint in Celsius. Populated only
56418
56744
  * in `heat_cool` mode. */
56419
- targetHigh: number().nullable(),
56745
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
56420
56746
  /** Lower bound of the dual setpoint in Celsius. Populated only
56421
56747
  * in `heat_cool` mode. */
56422
- targetLow: number().nullable(),
56748
+ targetLow: number().nullable().meta(readingMeta("temperature")),
56423
56749
  /** Target relative humidity (0..100). Null when the device has no
56424
56750
  * humidity control. */
56425
- targetHumidity: number().min(0).max(100).nullable(),
56751
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56426
56752
  /** Current measured temperature in Celsius. Read-only — pushed by
56427
56753
  * the device's internal sensor. */
56428
- currentTemp: number().nullable(),
56754
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
56429
56755
  /** Current measured relative humidity (0..100). Read-only. */
56430
- currentHumidity: number().min(0).max(100).nullable(),
56756
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56431
56757
  /** Vertical louver swing on/off. Null when the device has no vertical
56432
56758
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
56433
56759
  swingVertical: boolean().nullable(),
@@ -56435,11 +56761,11 @@ object({
56435
56761
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
56436
56762
  swingHorizontal: boolean().nullable(),
56437
56763
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
56438
- minTemp: number().nullable().optional(),
56764
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
56439
56765
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
56440
- maxTemp: number().nullable().optional(),
56766
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
56441
56767
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
56442
- tempStep: number().positive().nullable().optional(),
56768
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
56443
56769
  /** Ms epoch when the slice was last updated (push or command). */
56444
56770
  lastFetchedAt: number()
56445
56771
  });
@@ -56518,17 +56844,17 @@ DeviceType.Thermostat, DeviceType.Climate, method(object({
56518
56844
  * modes to expose in the picker without a slice fetch.
56519
56845
  */
56520
56846
  var RgbTripletSchema = object({
56521
- r: number().int().min(0).max(255),
56522
- g: number().int().min(0).max(255),
56523
- b: number().int().min(0).max(255)
56847
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
56848
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
56849
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
56524
56850
  });
56525
56851
  var HsvTripletSchema = object({
56526
56852
  /** Hue in degrees, 0..360. */
56527
- h: number().min(0).max(360),
56853
+ h: number().min(0).max(360).meta(readingMeta("none")),
56528
56854
  /** Saturation as 0..100 inclusive. */
56529
- s: number().min(0).max(100),
56855
+ s: number().min(0).max(100).meta(readingMeta("none")),
56530
56856
  /** Value/brightness as 0..100 inclusive. */
56531
- v: number().min(0).max(100)
56857
+ v: number().min(0).max(100).meta(readingMeta("none"))
56532
56858
  });
56533
56859
  var ColorInputSchema = discriminatedUnion("mode", [
56534
56860
  object({
@@ -56557,7 +56883,7 @@ object({
56557
56883
  /** Populated when `mode === 'hsv'`. */
56558
56884
  hsv: HsvTripletSchema.optional(),
56559
56885
  /** Populated when `mode === 'mired'`. */
56560
- mireds: number().int().optional(),
56886
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
56561
56887
  /** Ms epoch of the last operator-driven change. */
56562
56888
  lastChangedAt: number()
56563
56889
  });
@@ -56681,18 +57007,12 @@ _enum([
56681
57007
  "empty",
56682
57008
  "full"
56683
57009
  ]);
56684
- /** Units a remaining-life counter is reported in. Closed: the panel renders
56685
- * each. */
56686
- var ConsumableRemainingUnitSchema = _enum([
56687
- "days",
56688
- "hours",
56689
- "cycles"
56690
- ]);
56691
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
56692
- * days). `value` null = unknown (D393). */
57010
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57011
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57012
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
56693
57013
  var ConsumableRemainingSchema = object({
56694
- value: number().min(0).nullable(),
56695
- unit: ConsumableRemainingUnitSchema
57014
+ value: number().min(0).nullable().meta(readingMeta("device")),
57015
+ kind: ConsumableRemainingKindSchema
56696
57016
  });
56697
57017
  var ConsumablesStatusSchema = object({
56698
57018
  items: array(object({
@@ -56701,7 +57021,7 @@ var ConsumablesStatusSchema = object({
56701
57021
  /** Display name. */
56702
57022
  label: string().min(1),
56703
57023
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
56704
- level: number().min(0).max(100).nullable(),
57024
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
56705
57025
  /** The provider's discrete verdict when known (binary mode). */
56706
57026
  status: ConsumableStatusSchema.nullable(),
56707
57027
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -56718,7 +57038,7 @@ var ConsumablesStatusSchema = object({
56718
57038
  })),
56719
57039
  lastChangedAt: number()
56720
57040
  });
56721
- Object.values(DeviceType), [...ConsumableRemainingUnitSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57041
+ Object.values(DeviceType), [...ConsumableRemainingKindSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
56722
57042
  Object.values(DeviceType), method(object({
56723
57043
  deviceId: number().int().nonnegative(),
56724
57044
  key: string().min(1)
@@ -56743,39 +57063,17 @@ object({
56743
57063
  /** Current value. Numeric kind → number; select/text/datetime → string.
56744
57064
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
56745
57065
  * or time-only — HA accepts all three forms). */
56746
- value: union([number(), string()]),
57066
+ value: union([number().meta(readingMeta("device")), string()]),
56747
57067
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
56748
57068
  options: array(string()),
56749
57069
  /** Ms epoch when the slice was last updated. */
56750
57070
  lastChangedAt: number(),
56751
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
56752
- * Populated live from HA `attributes.unit_of_measurement` on each push.
56753
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
56754
- unit: string().optional(),
56755
- /** Minimum allowed value. Populated live from HA `attributes.min`.
56756
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
56757
- min: number().optional(),
56758
- /** Maximum allowed value. Populated live from HA `attributes.max`.
56759
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
56760
- max: number().optional(),
56761
- /** Step increment. Populated live from HA `attributes.step`.
56762
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
56763
- step: number().optional(),
56764
- /** Suggested decimal places for numeric display. Populated live from HA
56765
- * `attributes.suggested_display_precision`, or derived from `step` when the
56766
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
56767
- * other kinds. Falls back to auto-formatting when absent. */
56768
- precision: number().int().min(0).max(10).optional(),
56769
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
56770
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
56771
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
56772
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
56773
- * `has_date`/`has_time` attributes on each push. */
56774
- format: _enum([
56775
- "date",
56776
- "time",
56777
- "datetime"
56778
- ]).optional()
57071
+ /** Minimum allowed value, in the device quantity's base unit. */
57072
+ min: number().optional().meta(readingMeta("device")),
57073
+ /** Maximum allowed value, in the device quantity's base unit. */
57074
+ max: number().optional().meta(readingMeta("device")),
57075
+ /** Step increment, in the device quantity's base unit. */
57076
+ step: number().optional().meta(readingMeta("device"))
56779
57077
  });
56780
57078
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
56781
57079
  object({
@@ -56813,9 +57111,9 @@ object({
56813
57111
  ]),
56814
57112
  /** 0 = fully closed, 100 = fully open. Null when the device has
56815
57113
  * no intermediate position surface. */
56816
- position: number().min(0).max(100).nullable(),
57114
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
56817
57115
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
56818
- tiltPosition: number().min(0).max(100).nullable(),
57116
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
56819
57117
  /** Ms epoch when the slice was last updated. */
56820
57118
  lastChangedAt: number()
56821
57119
  });
@@ -56873,9 +57171,9 @@ var NormalizedRangeSchema$1 = object({
56873
57171
  object({
56874
57172
  mode: DayNightModeSchema,
56875
57173
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
56876
- sensitivity: number().optional(),
57174
+ sensitivity: number().optional().meta(readingMeta("none")),
56877
57175
  /** Delay before the IR-cut filter flips, in seconds. */
56878
- switchDelaySec: number().optional(),
57176
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
56879
57177
  lastFetchedAt: number()
56880
57178
  });
56881
57179
  /**
@@ -57030,11 +57328,11 @@ object({
57030
57328
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
57031
57329
  availablePrograms: array(string()),
57032
57330
  /** Seconds remaining on the active cycle, or null when not running / not known. */
57033
- remainingSeconds: number().int().nonnegative().nullable(),
57331
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57034
57332
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
57035
- delayedStartSeconds: number().int().nonnegative().nullable(),
57333
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57036
57334
  /** Cycle completion 0..100, or null when not applicable. */
57037
- progressPercent: number().min(0).max(100).nullable(),
57335
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57038
57336
  /** True when the door is reported open; null when the door state is not known. */
57039
57337
  doorOpen: boolean().nullable(),
57040
57338
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -57058,7 +57356,7 @@ object({
57058
57356
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57059
57357
  machineCareDue: boolean().nullable(),
57060
57358
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57061
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
57359
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
57062
57360
  /** Option states: null when the appliance does not expose the option or has not said. */
57063
57361
  halfLoad: boolean().nullable(),
57064
57362
  hygienePlus: boolean().nullable(),
@@ -57073,7 +57371,7 @@ object({
57073
57371
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
57074
57372
  activeEvents: array(string()),
57075
57373
  /** Numeric error code when the appliance reports one; null otherwise. */
57076
- errorCode: number().nullable(),
57374
+ errorCode: number().nullable().meta(readingMeta("none")),
57077
57375
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
57078
57376
  errorLabel: string().nullable(),
57079
57377
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -57219,24 +57517,14 @@ object({
57219
57517
  /** Ms epoch of the last press. null = never observed since this provider started. */
57220
57518
  lastPressedAt: number().nullable(),
57221
57519
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
57222
- pressCountSinceStart: number()
57520
+ pressCountSinceStart: number().meta(readingMeta("count"))
57223
57521
  });
57224
57522
  object({
57225
57523
  deviceId: number(),
57226
57524
  timestamp: number()
57227
57525
  });
57228
57526
  DeviceType.Button, DeviceType.Camera;
57229
- /**
57230
- * Enum-state sensor — a string value picked from a finite option set.
57231
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
57232
- * states, weather conditions, contact-source channels, mode strings).
57233
- * The option set is stable across observations; it is persisted in the
57234
- * device config blob (via the control slice at adoption) and is NOT
57235
- * stored in `sourceInfo`.
57236
- */
57237
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
57238
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
57239
- var EnumSensorDateTimeFormatSchema = _enum([
57527
+ _enum([
57240
57528
  "date",
57241
57529
  "time",
57242
57530
  "datetime"
@@ -57244,12 +57532,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
57244
57532
  object({
57245
57533
  value: string(),
57246
57534
  /**
57247
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
57248
- * as a locale date/time/datetime; absent for plain enum sensors. HA
57249
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
57250
- * a time-only sensor → `'time'`.
57535
+ * Set when `value` is a timestamp rather than an enum state, so history
57536
+ * does not learn ISO strings as states. The display shape (date / time /
57537
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
57251
57538
  */
57252
- format: EnumSensorDateTimeFormatSchema.optional(),
57539
+ timestamp: boolean().optional(),
57253
57540
  /** Ms epoch when the slice was last updated. */
57254
57541
  lastFetchedAt: number()
57255
57542
  });
@@ -57268,13 +57555,13 @@ var EventFireSchema = object({
57268
57555
  deviceId: number(),
57269
57556
  eventType: string(),
57270
57557
  data: record(string(), unknown()).nullable(),
57271
- timestamp: number(),
57272
- seq: number()
57558
+ timestamp: number().meta(readingMeta("none")),
57559
+ seq: number().meta(readingMeta("count"))
57273
57560
  });
57274
57561
  object({
57275
57562
  eventTypes: array(string()),
57276
57563
  lastEvent: EventFireSchema.nullable(),
57277
- eventCountSinceStart: number()
57564
+ eventCountSinceStart: number().meta(readingMeta("count"))
57278
57565
  });
57279
57566
  DeviceType.EventEmitter;
57280
57567
  var IdentitySchema = object({
@@ -57663,10 +57950,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
57663
57950
  object({
57664
57951
  /** Active speed as 0..100 inclusive. Null when the device has no
57665
57952
  * speed surface (single-speed fan). */
57666
- percentage: number().min(0).max(100).nullable(),
57953
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57667
57954
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
57668
57955
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
57669
- percentageStep: number().positive().optional(),
57956
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
57670
57957
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
57671
57958
  * — empty string when no preset is active or supported. */
57672
57959
  preset: string(),
@@ -57748,7 +58035,7 @@ var FeatureProbeStatusSchema = object({
57748
58035
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
57749
58036
  model: string().nullable(),
57750
58037
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
57751
- channelCount: number().nullable(),
58038
+ channelCount: number().nullable().meta(readingMeta("count")),
57752
58039
  /**
57753
58040
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
57754
58041
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -57780,9 +58067,27 @@ object({
57780
58067
  lastChangedAt: number()
57781
58068
  });
57782
58069
  DeviceType.Sensor;
58070
+ object({
58071
+ /** Members whose current value entered the aggregate. */
58072
+ included: number().int().nonnegative().meta(readingMeta("count", {
58073
+ precision: 0,
58074
+ label: "Reporting"
58075
+ })),
58076
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
58077
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
58078
+ precision: 0,
58079
+ label: "Excluded"
58080
+ })),
58081
+ /** `included` + `excluded`. */
58082
+ total: number().int().nonnegative().meta(readingMeta("count", {
58083
+ precision: 0,
58084
+ label: "Members"
58085
+ }))
58086
+ });
58087
+ DeviceType.Sensor;
57783
58088
  object({
57784
58089
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
57785
- position: number().min(0).max(100).nullable(),
58090
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57786
58091
  /** Ms epoch when the slice was last updated (push or poll). */
57787
58092
  lastFetchedAt: number()
57788
58093
  });
@@ -57791,9 +58096,9 @@ object({
57791
58096
  /** Whether the humidifier is currently on. */
57792
58097
  on: boolean(),
57793
58098
  /** Current measured relative humidity (0..100). Null when not reported. */
57794
- currentHumidity: number().min(0).max(100).nullable(),
58099
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57795
58100
  /** Target relative humidity (0..100). Null when no setpoint surface. */
57796
- targetHumidity: number().min(0).max(100).nullable(),
58101
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57797
58102
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
57798
58103
  * device has no mode surface. */
57799
58104
  mode: string().nullable(),
@@ -57803,9 +58108,9 @@ object({
57803
58108
  * `idle` / `off`). Null when the device doesn't report it. */
57804
58109
  action: string().nullable(),
57805
58110
  /** HA `min_humidity` attribute. Null → UI uses 0. */
57806
- minHumidity: number().nullable(),
58111
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
57807
58112
  /** HA `max_humidity` attribute. Null → UI uses 100. */
57808
- maxHumidity: number().nullable(),
58113
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
57809
58114
  /** Ms epoch when the slice was last updated. */
57810
58115
  lastChangedAt: number()
57811
58116
  });
@@ -57829,22 +58134,13 @@ DeviceType.Humidifier, method(object({
57829
58134
  auth: "protected"
57830
58135
  });
57831
58136
  object({
57832
- /** Current relative humidity, 0..100. */
57833
- percent: number().min(0).max(100).meta({
57834
- unit: "%",
58137
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
58138
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
58139
+ precision: 0,
57835
58140
  label: "Humidity"
57836
- }),
58141
+ })),
57837
58142
  /** Ms epoch when the slice was last updated. */
57838
- lastFetchedAt: number(),
57839
- /** Live display unit from the upstream source (e.g. HA
57840
- * `attributes.unit_of_measurement`). The UI prefers this over the
57841
- * role's canonical unit. Absent → fall back to the canonical unit. */
57842
- unit: string().optional(),
57843
- /** Suggested decimal places for numeric display.
57844
- * Populated live from the upstream source when provided (e.g. HA
57845
- * `attributes.suggested_display_precision`). Falls back to
57846
- * auto-formatting when absent. */
57847
- precision: number().int().min(0).max(10).optional()
58143
+ lastFetchedAt: number()
57848
58144
  });
57849
58145
  DeviceType.Sensor, DeviceType.Thermostat;
57850
58146
  object({
@@ -57852,7 +58148,7 @@ object({
57852
58148
  * entity exposes no `entity_picture` (yet). */
57853
58149
  url: string().nullable(),
57854
58150
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
57855
- lastUpdated: number().nullable()
58151
+ lastUpdated: number().nullable().meta(readingMeta("none"))
57856
58152
  });
57857
58153
  DeviceType.Image;
57858
58154
  /**
@@ -57908,19 +58204,19 @@ var NormalizedRangeSchema = object({
57908
58204
  });
57909
58205
  object({
57910
58206
  /** Normalized 0–100. */
57911
- brightness: number().optional(),
58207
+ brightness: number().optional().meta(readingMeta("none")),
57912
58208
  /** Normalized 0–100. */
57913
- contrast: number().optional(),
58209
+ contrast: number().optional().meta(readingMeta("none")),
57914
58210
  /** Normalized 0–100. */
57915
- saturation: number().optional(),
58211
+ saturation: number().optional().meta(readingMeta("none")),
57916
58212
  /** Normalized 0–100. */
57917
- sharpness: number().optional(),
58213
+ sharpness: number().optional().meta(readingMeta("none")),
57918
58214
  mirror: boolean().optional(),
57919
58215
  flip: boolean().optional(),
57920
58216
  rotate: ImageRotateSchema.optional(),
57921
58217
  whiteBalance: WhiteBalanceModeSchema.optional(),
57922
58218
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
57923
- warmth: number().optional(),
58219
+ warmth: number().optional().meta(readingMeta("none")),
57924
58220
  exposureMode: ExposureModeSchema.optional(),
57925
58221
  backlightMode: BacklightModeSchema.optional(),
57926
58222
  lastFetchedAt: number()
@@ -58165,10 +58461,10 @@ var IntercomAbilitySchema = object({
58165
58461
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
58166
58462
  codecs: array(string()),
58167
58463
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
58168
- sampleRate: number(),
58464
+ sampleRate: number().meta(readingMeta("none")),
58169
58465
  duplex: _enum(["half", "full"]),
58170
58466
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
58171
- maxBacklogMs: number()
58467
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
58172
58468
  });
58173
58469
  var IntercomStatusSchema = object({
58174
58470
  /** True while a talk session is open. */
@@ -58279,13 +58575,13 @@ object({
58279
58575
  activity: LawnMowerActivitySchema,
58280
58576
  /** 0..100 battery percentage. Null when the device has no battery
58281
58577
  * reading. */
58282
- batteryLevel: number().min(0).max(100).nullable(),
58578
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58283
58579
  /** 0..100 mowing-completion percentage of the current task, or null when no
58284
58580
  * task is active / progress is unavailable. */
58285
- progressPercent: number().min(0).max(100).nullable(),
58581
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58286
58582
  /** Current device/event code (dynamic — mostly status, sometimes an error),
58287
58583
  * or null when unknown. */
58288
- currentCode: number().nullable(),
58584
+ currentCode: number().nullable().meta(readingMeta("none")),
58289
58585
  /** Human label for {@link currentCode}, or null when undecodable. */
58290
58586
  currentCodeLabel: string().nullable(),
58291
58587
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -58747,7 +59043,7 @@ object({
58747
59043
  state: MediaPlayerStateSchema,
58748
59044
  /** Volume as 0..100 inclusive. Null when the device has no volume
58749
59045
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
58750
- volumeLevel: number().min(0).max(100).nullable(),
59046
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58751
59047
  /** Mute toggle distinct from volume=0. Null when no mute surface.
58752
59048
  * Pair with `DeviceFeature.MediaPlayerMute`. */
58753
59049
  isMuted: boolean().nullable(),
@@ -58760,10 +59056,10 @@ object({
58760
59056
  currentMedia: MediaInfoSchema.nullable(),
58761
59057
  /** Current playback position in ms. Null when not seekable or
58762
59058
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
58763
- positionMs: number().int().nonnegative().nullable(),
59059
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
58764
59060
  /** Total duration of the current media in ms. Null when unknown
58765
59061
  * (live stream) or nothing is playing. */
58766
- durationMs: number().int().nonnegative().nullable(),
59062
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
58767
59063
  /** Shuffle toggle. Null when no shuffle surface.
58768
59064
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
58769
59065
  shuffle: boolean().nullable(),
@@ -59063,7 +59359,7 @@ object({
59063
59359
  * it, every provider seeded it at boot, and every session left it
59064
59360
  * `detected: true` after its detach.
59065
59361
  */
59066
- autoClearAfterMs: number().nullable()
59362
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
59067
59363
  });
59068
59364
  /**
59069
59365
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -59113,7 +59409,7 @@ var MotionZoneRegionSchema = object({
59113
59409
  });
59114
59410
  object({
59115
59411
  enabled: boolean(),
59116
- sensitivity: number(),
59412
+ sensitivity: number().meta(readingMeta("none")),
59117
59413
  /** Grid region(s). Today exactly one `grid` shape. */
59118
59414
  regions: array(MotionZoneRegionSchema),
59119
59415
  lastFetchedAt: number()
@@ -59417,7 +59713,7 @@ var NavigationStatusSchema = object({
59417
59713
  following: boolean(),
59418
59714
  flash: boolean(),
59419
59715
  lightMode: NavigationLightModeSchema,
59420
- lightLevel: number().min(40).max(100),
59716
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
59421
59717
  /** Ms epoch when the slice was last updated. */
59422
59718
  lastChangedAt: number()
59423
59719
  });
@@ -59463,13 +59759,13 @@ var NetworkLinkStatusSchema = object({
59463
59759
  * one whose reading has not landed must not be drawn at 0 %. Consumers
59464
59760
  * SKIP a null rather than coerce it.
59465
59761
  */
59466
- signalPercent: number().min(0).max(100).nullable(),
59762
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59467
59763
  /** Raw received signal strength in dBm, when the firmware reports one. */
59468
- rssiDbm: number().optional(),
59764
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
59469
59765
  /** Network name of a wireless link, when the firmware reports it. */
59470
59766
  ssid: string().optional(),
59471
59767
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
59472
- lastUpdated: number()
59768
+ lastUpdated: number().meta(readingMeta("none"))
59473
59769
  });
59474
59770
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
59475
59771
  deviceId: number(),
@@ -59788,7 +60084,7 @@ object({
59788
60084
  lastError: string().nullable(),
59789
60085
  /** Number of deliveries currently buffered server-side (rate-limited
59790
60086
  * notifiers may queue). 0 when the provider sends synchronously. */
59791
- queueDepth: number().int().nonnegative(),
60087
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
59792
60088
  /** Per-feature capability matrix — authoritative source for the
59793
60089
  * compose-form field gating. */
59794
60090
  supports: NotifierSupportsSchema
@@ -59838,15 +60134,8 @@ DeviceType.Notifier, method(NotifierSendInputSchema, NotifierSendResultSchema, {
59838
60134
  acceptedAt: number()
59839
60135
  });
59840
60136
  object({
59841
- value: number(),
59842
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
59843
- * Populated live from the upstream source on each state push.
59844
- * Absent for unitless sensors. */
59845
- unit: string().optional(),
59846
- /** Suggested decimal places for numeric display.
59847
- * Populated live from the upstream source when provided.
59848
- * Falls back to auto-formatting when absent. */
59849
- precision: number().int().min(0).max(10).optional(),
60137
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
60138
+ value: number().nullable().meta(readingMeta("device")),
59850
60139
  /** Ms epoch when the slice was last updated. */
59851
60140
  lastFetchedAt: number()
59852
60141
  });
@@ -60136,23 +60425,26 @@ object({
60136
60425
  /** Food currently in the bowl (grams). Null when the device has not
60137
60426
  * reported a reading yet. On dual-hopper models this is the combined
60138
60427
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
60139
- foodLevel: number().nullable(),
60428
+ foodLevel: number().nullable().meta(readingMeta("none")),
60140
60429
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
60141
60430
  * single-hopper models. */
60142
- food1: number().nullable(),
60431
+ food1: number().nullable().meta(readingMeta("none")),
60143
60432
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
60144
60433
  * single-hopper models. */
60145
- food2: number().nullable(),
60434
+ food2: number().nullable().meta(readingMeta("none")),
60146
60435
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
60147
60436
  * (`device_class: problem`, on = low). True when the bowl is empty /
60148
60437
  * below the feeder's low threshold. */
60149
60438
  lowFood: boolean(),
60150
60439
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
60151
60440
  * device has no battery reading. */
60152
- batteryPower: number().min(0).max(100).nullable(),
60153
- /** Days of desiccant life remaining. Null when the model has no
60154
- * desiccant sensor. */
60155
- desiccantLeftDays: number().nullable(),
60441
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60442
+ /**
60443
+ * Desiccant life remaining, stored in seconds (duration base, D944).
60444
+ * Providers convert from the days the feeder reports. Null when the
60445
+ * model has no desiccant sensor.
60446
+ */
60447
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
60156
60448
  /** True while a feed is in progress. */
60157
60449
  feeding: boolean(),
60158
60450
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -60163,7 +60455,7 @@ object({
60163
60455
  * with `errorCode` for consumers that want the raw integer. */
60164
60456
  error: string().nullable(),
60165
60457
  /** Raw device error code (0 / null = no error). */
60166
- errorCode: number().nullable(),
60458
+ errorCode: number().nullable().meta(readingMeta("none")),
60167
60459
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
60168
60460
  isDualHopper: boolean(),
60169
60461
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -60173,7 +60465,7 @@ object({
60173
60465
  /** Play a chime when dispensing. */
60174
60466
  feedSound: boolean(),
60175
60467
  /** Speaker / prompt volume level (device-scaled integer). */
60176
- volume: number(),
60468
+ volume: number().meta(readingMeta("none")),
60177
60469
  /** Ms epoch when the slice was last refreshed from the cloud. */
60178
60470
  lastFetchedAt: number()
60179
60471
  });
@@ -60610,36 +60902,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
60610
60902
  });
60611
60903
  object({
60612
60904
  /** Instantaneous power draw in watts. */
60613
- watts: number().optional().meta({
60614
- unit: "W",
60615
- label: "Power"
60616
- }),
60905
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
60617
60906
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
60618
- kwhTotal: number().optional().meta({
60619
- unit: "kWh",
60620
- label: "Energy"
60621
- }),
60907
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
60622
60908
  /** Voltage in volts. */
60623
- volts: number().optional(),
60909
+ volts: number().optional().meta(readingMeta("voltage")),
60624
60910
  /** Current in amperes. */
60625
- amps: number().optional(),
60626
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
60627
- va: number().optional(),
60911
+ amps: number().optional().meta(readingMeta("current")),
60912
+ /** Apparent power in volt-amperes. */
60913
+ va: number().optional().meta(readingMeta("apparent-power")),
60628
60914
  /** Power factor (0..1) if reported. */
60629
- powerFactor: number().min(0).max(1).optional(),
60915
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
60630
60916
  /** Ms epoch when the slice was last updated. */
60631
- lastFetchedAt: number(),
60632
- /** Live display unit of the single metric this slice carries (e.g. HA
60633
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
60634
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
60635
- * unit per slice is unambiguous. The UI prefers this over the role's
60636
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
60637
- unit: string().optional(),
60638
- /** Suggested decimal places for numeric display.
60639
- * Populated live from the upstream source when provided (e.g. HA
60640
- * `attributes.suggested_display_precision`). Falls back to
60641
- * auto-formatting when absent. */
60642
- precision: number().int().min(0).max(10).optional()
60917
+ lastFetchedAt: number()
60643
60918
  });
60644
60919
  DeviceType.Sensor;
60645
60920
  /**
@@ -60660,11 +60935,11 @@ DeviceType.Sensor;
60660
60935
  */
60661
60936
  var GpsLocationSchema = object({
60662
60937
  /** Latitude in decimal degrees, -90..90. */
60663
- latitude: number().min(-90).max(90),
60938
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
60664
60939
  /** Longitude in decimal degrees, -180..180. */
60665
- longitude: number().min(-180).max(180),
60940
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
60666
60941
  /** Reported accuracy in meters (lower = better). */
60667
- accuracyMeters: number().nonnegative()
60942
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
60668
60943
  });
60669
60944
  object({
60670
60945
  /** `home` / `not_home` / any user-defined zone name. */
@@ -60676,25 +60951,16 @@ object({
60676
60951
  gps: GpsLocationSchema.nullable(),
60677
60952
  /** Optional battery level of the tracking device (0..100). Null
60678
60953
  * when not reported. */
60679
- batteryPercent: number().min(0).max(100).nullable(),
60954
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60680
60955
  /** Ms epoch when the slice was last updated. */
60681
60956
  lastChangedAt: number()
60682
60957
  });
60683
60958
  DeviceType.Presence;
60684
60959
  object({
60685
- /** Current pressure in hPa. */
60686
- hpa: number(),
60960
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
60961
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
60687
60962
  /** Ms epoch when the slice was last updated. */
60688
- lastFetchedAt: number(),
60689
- /** Live display unit from the upstream source (e.g. HA
60690
- * `attributes.unit_of_measurement`). The UI prefers this over the
60691
- * role's canonical unit. Absent → fall back to the canonical unit. */
60692
- unit: string().optional(),
60693
- /** Suggested decimal places for numeric display.
60694
- * Populated live from the upstream source when provided (e.g. HA
60695
- * `attributes.suggested_display_precision`). Falls back to
60696
- * auto-formatting when absent. */
60697
- precision: number().int().min(0).max(10).optional()
60963
+ lastFetchedAt: number()
60698
60964
  });
60699
60965
  DeviceType.Sensor;
60700
60966
  /**
@@ -61027,8 +61293,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
61027
61293
  }), _void(), { kind: "mutation" });
61028
61294
  var PtzAutotrackSettingsSchema = object({
61029
61295
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
61030
- stopDelaySeconds: number().int().min(0).max(300),
61031
- disappearDelaySeconds: number().int().min(0).max(300)
61296
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
61297
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
61032
61298
  });
61033
61299
  /**
61034
61300
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -61085,7 +61351,7 @@ object({
61085
61351
  /** null = unknown — never coerced to false (D393). */
61086
61352
  running: boolean().nullable(),
61087
61353
  /** Epoch ms the current boolean began; null while `running` is null. */
61088
- since: number().nullable(),
61354
+ since: number().nullable().meta(readingMeta("none")),
61089
61355
  source: RunningStateSourceSchema.meta({ label: "Source" }),
61090
61356
  phase: string().nullable(),
61091
61357
  /** null until a reading has been published (D393). */
@@ -62283,9 +62549,9 @@ var RecordTriggerSchema = _enum([
62283
62549
  */
62284
62550
  var RecordWindowSchema = object({
62285
62551
  trigger: RecordTriggerSchema,
62286
- day: number().int().min(0).max(6),
62287
- startMinute: number().int().min(0).max(1439),
62288
- endMinute: number().int().min(1).max(1440)
62552
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
62553
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
62554
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
62289
62555
  });
62290
62556
  /** Status of one physical volume, as the camera itself describes it. */
62291
62557
  var OnboardStorageVolumeSchema = object({
@@ -62354,10 +62620,10 @@ object({
62354
62620
  /** Master "record to the card at all" switch. */
62355
62621
  enabled: boolean().nullable(),
62356
62622
  overwriteWhenFull: boolean().nullable(),
62357
- preRecordSec: number().nullable(),
62358
- postRecordSec: number().nullable(),
62623
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
62624
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
62359
62625
  /** Length of one recorded file, in minutes. */
62360
- segmentMinutes: number().nullable(),
62626
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
62361
62627
  /** The primary track's weekly windows, flattened. */
62362
62628
  windows: array(RecordWindowSchema),
62363
62629
  /**
@@ -62373,7 +62639,7 @@ object({
62373
62639
  * provider reporting a non-zero count also reports the schedule as not
62374
62640
  * writable.
62375
62641
  */
62376
- unreadableWindows: number(),
62642
+ unreadableWindows: number().meta(readingMeta("count")),
62377
62643
  /**
62378
62644
  * The camera is scheduled to record and has NO usable storage.
62379
62645
  *
@@ -63010,7 +63276,7 @@ var CamStreamDescriptorSchema = object({
63010
63276
  url: string().optional(),
63011
63277
  codec: string().optional(),
63012
63278
  resolution: CamStreamResolutionSchema.optional(),
63013
- fps: number().positive().optional(),
63279
+ fps: number().positive().optional().meta(readingMeta("rate")),
63014
63280
  label: string().optional(),
63015
63281
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
63016
63282
  deviceFeatures: array(string()).optional(),
@@ -63040,18 +63306,18 @@ var StreamProfileSchema = _enum([
63040
63306
  ]);
63041
63307
  /** The current encoding config of one stream profile. */
63042
63308
  var StreamProfileConfigSchema = object({
63043
- width: number(),
63044
- height: number(),
63309
+ width: number().meta(readingMeta("none")),
63310
+ height: number().meta(readingMeta("none")),
63045
63311
  codec: _enum(["h264", "h265"]),
63046
- framerate: number(),
63047
- bitrate: number(),
63312
+ framerate: number().meta(readingMeta("rate")),
63313
+ bitrate: number().meta(readingMeta("bitrate")),
63048
63314
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
63049
63315
  encoderProfile: _enum([
63050
63316
  "high",
63051
63317
  "main",
63052
63318
  "baseline"
63053
63319
  ]).optional(),
63054
- gop: number().optional(),
63320
+ gop: number().optional().meta(readingMeta("none")),
63055
63321
  /**
63056
63322
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
63057
63323
  *
@@ -63694,23 +63960,13 @@ object({
63694
63960
  });
63695
63961
  DeviceType.Sensor;
63696
63962
  object({
63697
- /** Current temperature in Celsius. */
63698
- celsius: number().meta({
63699
- unit: "°C",
63963
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
63964
+ celsius: number().nullable().meta(readingMeta("temperature", {
63965
+ precision: 1,
63700
63966
  label: "Temperature"
63701
- }),
63967
+ })),
63702
63968
  /** Ms epoch when the slice was last updated (push or poll). */
63703
- lastFetchedAt: number(),
63704
- /** Live display unit from the upstream source (e.g. HA
63705
- * `attributes.unit_of_measurement`). The UI prefers this over the
63706
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
63707
- * Absent → the UI falls back to the role's canonical unit. */
63708
- unit: string().optional(),
63709
- /** Suggested decimal places for numeric display.
63710
- * Populated live from the upstream source when provided (e.g. HA
63711
- * `attributes.suggested_display_precision`). Falls back to
63712
- * auto-formatting when absent. */
63713
- precision: number().int().min(0).max(10).optional()
63969
+ lastFetchedAt: number()
63714
63970
  });
63715
63971
  DeviceType.Sensor, DeviceType.Thermostat;
63716
63972
  /**
@@ -63962,16 +64218,16 @@ object({
63962
64218
  ]),
63963
64219
  /** 0..100 battery percentage. Null when the device has no battery
63964
64220
  * reading. */
63965
- batteryLevel: number().min(0).max(100).nullable(),
64221
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63966
64222
  /** Current fan-speed token (provider-verbatim). Null when unknown or
63967
64223
  * the vacuum has no speed control. */
63968
64224
  fanSpeed: string().nullable(),
63969
64225
  /** Speed tokens the hardware accepts — drives the UI selector. */
63970
64226
  availableFanSpeeds: array(string()),
63971
64227
  /** 0..100 cleaning-completion percentage of the current task, or null. */
63972
- progressPercent: number().min(0).max(100).nullable(),
64228
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
63973
64229
  /** Current error code (0 / null = no error). */
63974
- errorCode: number().nullable(),
64230
+ errorCode: number().nullable().meta(readingMeta("none")),
63975
64231
  /** Human label for {@link errorCode}, or null when none / undecodable. */
63976
64232
  errorLabel: string().nullable(),
63977
64233
  /** Ms epoch when the slice was last updated. */
@@ -64010,7 +64266,7 @@ object({
64010
64266
  ]),
64011
64267
  /** 0 = fully closed, 100 = fully open. Null when the device has no
64012
64268
  * intermediate position surface. */
64013
- position: number().min(0).max(100).nullable(),
64269
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64014
64270
  /** Ms epoch when the slice was last updated. */
64015
64271
  lastChangedAt: number()
64016
64272
  });
@@ -64038,9 +64294,9 @@ object({
64038
64294
  DeviceType.Sensor;
64039
64295
  object({
64040
64296
  /** Current measured temperature. Null when not reported. */
64041
- currentTemp: number().nullable(),
64297
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
64042
64298
  /** Target temperature setpoint. Null when no setpoint surface. */
64043
- targetTemp: number().nullable(),
64299
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
64044
64300
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
64045
64301
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
64046
64302
  * device reports an unknown state. */
@@ -64051,9 +64307,9 @@ object({
64051
64307
  * has no away surface. */
64052
64308
  away: boolean().nullable(),
64053
64309
  /** HA `min_temp` attribute. Null when not reported. */
64054
- minTemp: number().nullable(),
64310
+ minTemp: number().nullable().meta(readingMeta("temperature")),
64055
64311
  /** HA `max_temp` attribute. Null when not reported. */
64056
- maxTemp: number().nullable(),
64312
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
64057
64313
  /** Ms epoch when the slice was last updated. */
64058
64314
  lastChangedAt: number()
64059
64315
  });
@@ -64077,25 +64333,18 @@ DeviceType.WaterHeater, method(object({
64077
64333
  auth: "protected"
64078
64334
  });
64079
64335
  object({
64080
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
64081
- * when no condition has been reported yet. */
64336
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
64082
64337
  condition: string().nullable(),
64083
- /** Current temperature in the reported unit. Null when not provided. */
64084
- temperature: number().nullable(),
64085
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
64086
- temperatureUnit: string().nullable(),
64338
+ /** Current temperature in Celsius. Null when not provided or refused. */
64339
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
64087
64340
  /** Relative humidity (0..100). Null when not provided. */
64088
- humidity: number().min(0).max(100).nullable(),
64089
- /** Barometric pressure in the reported unit. Null when not provided. */
64090
- pressure: number().nullable(),
64091
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
64092
- pressureUnit: string().nullable(),
64093
- /** Wind speed in the reported unit. Null when not provided. */
64094
- windSpeed: number().nullable(),
64095
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
64096
- windSpeedUnit: string().nullable(),
64341
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
64342
+ /** Barometric pressure in hPa. Null when not provided or refused. */
64343
+ pressure: number().nullable().meta(readingMeta("pressure")),
64344
+ /** Wind speed in metres per second. Null when not provided or refused. */
64345
+ windSpeed: number().nullable().meta(readingMeta("speed")),
64097
64346
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
64098
- windBearing: number().nullable(),
64347
+ windBearing: number().nullable().meta(readingMeta("angle")),
64099
64348
  /** Ms epoch when the slice was last updated. */
64100
64349
  lastFetchedAt: number()
64101
64350
  });
@@ -68115,6 +68364,13 @@ Object.freeze({
68115
68364
  access: "view",
68116
68365
  role: "viewer"
68117
68366
  },
68367
+ "groupStatus.getStatus": {
68368
+ capName: "group-status",
68369
+ capScope: "device",
68370
+ addonId: null,
68371
+ access: "view",
68372
+ role: "viewer"
68373
+ },
68118
68374
  "heatingValve.getStatus": {
68119
68375
  capName: "heating-valve",
68120
68376
  capScope: "device",
@@ -77937,6 +78193,33 @@ var SENSOR_CAP_VOCABULARY = {
77937
78193
  icon: "flame"
77938
78194
  }]
77939
78195
  },
78196
+ "group-status": {
78197
+ scope: "device",
78198
+ noun: t("Group", "Gruppo"),
78199
+ state: [
78200
+ {
78201
+ kind: "numeric",
78202
+ field: "included",
78203
+ quantity: t("the members reporting", "i membri che rispondono"),
78204
+ label: t("Reporting", "Rispondono"),
78205
+ icon: "users"
78206
+ },
78207
+ {
78208
+ kind: "numeric",
78209
+ field: "excluded",
78210
+ quantity: t("the members left out", "i membri esclusi"),
78211
+ label: t("Excluded", "Esclusi"),
78212
+ icon: "users"
78213
+ },
78214
+ {
78215
+ kind: "numeric",
78216
+ field: "total",
78217
+ quantity: t("the member count", "il numero di membri"),
78218
+ label: t("Members", "Membri"),
78219
+ icon: "sigma"
78220
+ }
78221
+ ]
78222
+ },
77940
78223
  "humidity-sensor": {
77941
78224
  scope: "device",
77942
78225
  noun: t("Humidity", "Umidità"),
@@ -77970,7 +78253,6 @@ var SENSOR_CAP_VOCABULARY = {
77970
78253
  field: "value",
77971
78254
  quantity: t("the reading", "il valore"),
77972
78255
  label: t("Reading", "Valore"),
77973
- unitField: "unit",
77974
78256
  icon: "gauge"
77975
78257
  }]
77976
78258
  },
@@ -78106,7 +78388,7 @@ var SENSOR_CAP_VOCABULARY = {
78106
78388
  field: "temperature",
78107
78389
  quantity: t("the outdoor temperature", "la temperatura esterna"),
78108
78390
  label: t("Temperature", "Temperatura"),
78109
- unitField: "temperatureUnit",
78391
+ unit: "°C",
78110
78392
  icon: "thermometer"
78111
78393
  },
78112
78394
  {
@@ -78122,7 +78404,7 @@ var SENSOR_CAP_VOCABULARY = {
78122
78404
  field: "windSpeed",
78123
78405
  quantity: t("the wind speed", "la velocità del vento"),
78124
78406
  label: t("Wind", "Vento"),
78125
- unitField: "windSpeedUnit",
78407
+ unit: "m/s",
78126
78408
  icon: "wind"
78127
78409
  },
78128
78410
  {
@@ -78130,7 +78412,7 @@ var SENSOR_CAP_VOCABULARY = {
78130
78412
  field: "pressure",
78131
78413
  quantity: t("the air pressure", "la pressione atmosferica"),
78132
78414
  label: t("Pressure", "Pressione"),
78133
- unitField: "pressureUnit",
78415
+ unit: "hPa",
78134
78416
  icon: "gauge"
78135
78417
  }
78136
78418
  ]
@@ -79403,6 +79685,11 @@ Object.freeze({
79403
79685
  form: "single",
79404
79686
  optional: false
79405
79687
  }],
79688
+ "groupStatus.getStatus": [{
79689
+ name: "deviceId",
79690
+ form: "single",
79691
+ optional: false
79692
+ }],
79406
79693
  "heatingValve.getStatus": [{
79407
79694
  name: "deviceId",
79408
79695
  form: "single",
@@ -82518,7 +82805,7 @@ var AgentUIAddon = class extends BaseAddon {
82518
82805
  capability: adminUiCapability,
82519
82806
  provider: {
82520
82807
  getStaticDir: async () => ({ staticDir: path.resolve(__dirname) }),
82521
- getVersion: async () => ({ version: "1.2.269" })
82808
+ getVersion: async () => ({ version: "1.2.271" })
82522
82809
  }
82523
82810
  }];
82524
82811
  }