@camstack/addon-training 0.1.82 → 0.1.84

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.
Files changed (20) hide show
  1. package/dist/{MotionZonesSettings-Ca-6XXfI.mjs → MotionZonesSettings-B2LROgdO.mjs} +2 -2
  2. package/dist/{PrivacyMaskSettings-BCUM9T8S.mjs → PrivacyMaskSettings-BI-wLtO5.mjs} +2 -2
  3. package/dist/{SceneMonitorEditor-CjJ891SK.mjs → SceneMonitorEditor-BsXMgLZ2.mjs} +1 -1
  4. package/dist/_stub.js +5358 -5290
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_training_page-vEOlZUQt.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_training_page-B1uLtRe1.mjs} +3 -3
  6. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CCYqyYqc.mjs +26 -0
  7. package/dist/{dist-VJTyi7FX.js → dist-DIvqaK45.js} +717 -430
  8. package/dist/{dist-CQOH32eF.mjs → dist-vmoGj7S3.mjs} +717 -430
  9. package/dist/{hostInit-CvOJdSqn.mjs → hostInit-DjoeThKQ.mjs} +2 -2
  10. package/dist/index.js +1 -1
  11. package/dist/orchestrator/training.addon.js +1 -1
  12. package/dist/orchestrator/training.addon.mjs +1 -1
  13. package/dist/{player-overlays-Bk8LUHwl.mjs → player-overlays-D6Na2rHN.mjs} +1 -1
  14. package/dist/providers/modal/training-modal.addon.js +1 -1
  15. package/dist/providers/modal/training-modal.addon.mjs +1 -1
  16. package/dist/remoteEntry.js +1 -1
  17. package/dist/{responsive-DkRlf-GQ.mjs → responsive-CagNQyj-.mjs} +16 -13
  18. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_training_page__remoteEntry_js-DNg-tEZh.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_training_page__remoteEntry_js-OkrY_yeX.mjs} +1 -1
  19. package/package.json +1 -1
  20. package/dist/_virtual_mf___mfe_internal__addon_training_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-9NmkfCw8.mjs +0 -26
@@ -5600,7 +5600,7 @@ var ZodIssueCode = {
5600
5600
  var ZodFirstPartyTypeKind;
5601
5601
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5602
5602
  //#endregion
5603
- //#region ../types/dist/sleep-BErAxhul.mjs
5603
+ //#region ../types/dist/sleep-B81gwQuR.mjs
5604
5604
  /**
5605
5605
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5606
5606
  * window to float samples (D455).
@@ -8335,6 +8335,99 @@ function defineEntryKind(name, spec) {
8335
8335
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8336
8336
  return defined;
8337
8337
  }
8338
+ var READING_QUANTITIES = [
8339
+ "temperature",
8340
+ "pressure",
8341
+ "speed",
8342
+ "precipitation",
8343
+ "precipitation-rate",
8344
+ "length",
8345
+ "area",
8346
+ "illuminance",
8347
+ "irradiance",
8348
+ "power",
8349
+ "energy",
8350
+ "voltage",
8351
+ "current",
8352
+ "apparent-power",
8353
+ "humidity",
8354
+ "percentage",
8355
+ "concentration-volume",
8356
+ "concentration-mass",
8357
+ "concentration-ppb",
8358
+ "volume",
8359
+ "duration",
8360
+ "duration-ms",
8361
+ "information",
8362
+ "sound-level",
8363
+ "signal-level",
8364
+ "colour-temperature",
8365
+ "angle",
8366
+ "bitrate",
8367
+ "rate",
8368
+ "mass",
8369
+ "index",
8370
+ "ratio",
8371
+ "count",
8372
+ "none",
8373
+ "device"
8374
+ ];
8375
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8376
+ new Set(READING_QUANTITIES);
8377
+ /**
8378
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8379
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8380
+ */
8381
+ var QUANTITY_BASE = {
8382
+ temperature: "°C",
8383
+ pressure: "hPa",
8384
+ speed: "m/s",
8385
+ precipitation: "mm",
8386
+ "precipitation-rate": "mm/h",
8387
+ length: "m",
8388
+ area: "m²",
8389
+ illuminance: "lx",
8390
+ irradiance: "W/m²",
8391
+ power: "W",
8392
+ energy: "kWh",
8393
+ voltage: "V",
8394
+ current: "A",
8395
+ "apparent-power": "VA",
8396
+ humidity: "%",
8397
+ percentage: "%",
8398
+ "concentration-volume": "ppm",
8399
+ "concentration-mass": "µg/m³",
8400
+ "concentration-ppb": "ppb",
8401
+ volume: "L",
8402
+ duration: "s",
8403
+ "duration-ms": "ms",
8404
+ information: "B",
8405
+ "sound-level": "dBFS",
8406
+ "signal-level": "dBm",
8407
+ "colour-temperature": "mired",
8408
+ angle: "°",
8409
+ bitrate: "kbps",
8410
+ rate: "fps",
8411
+ mass: "kg",
8412
+ index: "none",
8413
+ ratio: "none",
8414
+ count: "none",
8415
+ none: "none",
8416
+ device: "device"
8417
+ };
8418
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8419
+ function baseUnitOf(quantity) {
8420
+ const symbol = QUANTITY_BASE[quantity];
8421
+ if (symbol === "none" || symbol === "device") return void 0;
8422
+ return symbol;
8423
+ }
8424
+ function readingMeta(quantity, extras) {
8425
+ return {
8426
+ quantity,
8427
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8428
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8429
+ };
8430
+ }
8338
8431
  /** Shared Zod schemas used across streaming capabilities. */
8339
8432
  var CamProfileSchema = _enum([
8340
8433
  "high",
@@ -8362,8 +8455,8 @@ var CamStreamKindSchema = _enum([
8362
8455
  "derived"
8363
8456
  ]);
8364
8457
  var CamStreamResolutionSchema = object({
8365
- width: number().int().positive(),
8366
- height: number().int().positive()
8458
+ width: number().int().positive().meta(readingMeta("none")),
8459
+ height: number().int().positive().meta(readingMeta("none"))
8367
8460
  });
8368
8461
  var CameraStreamSchema = object({
8369
8462
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8877,6 +8970,7 @@ var CAP_NAMES = Object.freeze([
8877
8970
  "filesystem-browse",
8878
8971
  "flood",
8879
8972
  "gas",
8973
+ "group-status",
8880
8974
  "heating-valve",
8881
8975
  "humidifier",
8882
8976
  "humidity-sensor",
@@ -9006,14 +9100,6 @@ Object.freeze({
9006
9100
  {
9007
9101
  "path": "aqi",
9008
9102
  "valueType": "number"
9009
- },
9010
- {
9011
- "path": "unit",
9012
- "valueType": "string"
9013
- },
9014
- {
9015
- "path": "precision",
9016
- "valueType": "number"
9017
9103
  }
9018
9104
  ],
9019
9105
  "alarm-panel": [
@@ -9049,20 +9135,10 @@ Object.freeze({
9049
9135
  "valueType": "boolean"
9050
9136
  }
9051
9137
  ],
9052
- "ambient-light-sensor": [
9053
- {
9054
- "path": "lux",
9055
- "valueType": "number"
9056
- },
9057
- {
9058
- "path": "unit",
9059
- "valueType": "string"
9060
- },
9061
- {
9062
- "path": "precision",
9063
- "valueType": "number"
9064
- }
9065
- ],
9138
+ "ambient-light-sensor": [{
9139
+ "path": "lux",
9140
+ "valueType": "number"
9141
+ }],
9066
9142
  "audio-metrics": [
9067
9143
  {
9068
9144
  "path": "ts",
@@ -9142,6 +9218,10 @@ Object.freeze({
9142
9218
  "path": "lastUpdated",
9143
9219
  "valueType": "number"
9144
9220
  },
9221
+ {
9222
+ "path": "volts",
9223
+ "valueType": "number"
9224
+ },
9145
9225
  {
9146
9226
  "path": "lastContactAt",
9147
9227
  "valueType": "number"
@@ -9384,10 +9464,6 @@ Object.freeze({
9384
9464
  "datetime"
9385
9465
  ]
9386
9466
  },
9387
- {
9388
- "path": "unit",
9389
- "valueType": "string"
9390
- },
9391
9467
  {
9392
9468
  "path": "min",
9393
9469
  "valueType": "number"
@@ -9399,19 +9475,6 @@ Object.freeze({
9399
9475
  {
9400
9476
  "path": "step",
9401
9477
  "valueType": "number"
9402
- },
9403
- {
9404
- "path": "precision",
9405
- "valueType": "number"
9406
- },
9407
- {
9408
- "path": "format",
9409
- "valueType": "enum",
9410
- "enumValues": [
9411
- "date",
9412
- "time",
9413
- "datetime"
9414
- ]
9415
9478
  }
9416
9479
  ],
9417
9480
  "cover": [
@@ -9652,13 +9715,8 @@ Object.freeze({
9652
9715
  "path": "value",
9653
9716
  "valueType": "string"
9654
9717
  }, {
9655
- "path": "format",
9656
- "valueType": "enum",
9657
- "enumValues": [
9658
- "date",
9659
- "time",
9660
- "datetime"
9661
- ]
9718
+ "path": "timestamp",
9719
+ "valueType": "boolean"
9662
9720
  }],
9663
9721
  "event-emitter": [
9664
9722
  {
@@ -9731,6 +9789,20 @@ Object.freeze({
9731
9789
  "path": "detected",
9732
9790
  "valueType": "boolean"
9733
9791
  }],
9792
+ "group-status": [
9793
+ {
9794
+ "path": "included",
9795
+ "valueType": "number"
9796
+ },
9797
+ {
9798
+ "path": "excluded",
9799
+ "valueType": "number"
9800
+ },
9801
+ {
9802
+ "path": "total",
9803
+ "valueType": "number"
9804
+ }
9805
+ ],
9734
9806
  "heating-valve": [{
9735
9807
  "path": "position",
9736
9808
  "valueType": "number"
@@ -9765,20 +9837,10 @@ Object.freeze({
9765
9837
  "valueType": "number"
9766
9838
  }
9767
9839
  ],
9768
- "humidity-sensor": [
9769
- {
9770
- "path": "percent",
9771
- "valueType": "number"
9772
- },
9773
- {
9774
- "path": "unit",
9775
- "valueType": "string"
9776
- },
9777
- {
9778
- "path": "precision",
9779
- "valueType": "number"
9780
- }
9781
- ],
9840
+ "humidity-sensor": [{
9841
+ "path": "percent",
9842
+ "valueType": "number"
9843
+ }],
9782
9844
  "image": [{
9783
9845
  "path": "url",
9784
9846
  "valueType": "string"
@@ -10117,20 +10179,10 @@ Object.freeze({
10117
10179
  "valueType": "boolean"
10118
10180
  }
10119
10181
  ],
10120
- "numeric-sensor": [
10121
- {
10122
- "path": "value",
10123
- "valueType": "number"
10124
- },
10125
- {
10126
- "path": "unit",
10127
- "valueType": "string"
10128
- },
10129
- {
10130
- "path": "precision",
10131
- "valueType": "number"
10132
- }
10133
- ],
10182
+ "numeric-sensor": [{
10183
+ "path": "value",
10184
+ "valueType": "number"
10185
+ }],
10134
10186
  "pet-feeder": [
10135
10187
  {
10136
10188
  "path": "foodLevel",
@@ -10222,14 +10274,6 @@ Object.freeze({
10222
10274
  {
10223
10275
  "path": "powerFactor",
10224
10276
  "valueType": "number"
10225
- },
10226
- {
10227
- "path": "unit",
10228
- "valueType": "string"
10229
- },
10230
- {
10231
- "path": "precision",
10232
- "valueType": "number"
10233
10277
  }
10234
10278
  ],
10235
10279
  "presence": [
@@ -10258,20 +10302,10 @@ Object.freeze({
10258
10302
  "valueType": "number"
10259
10303
  }
10260
10304
  ],
10261
- "pressure-sensor": [
10262
- {
10263
- "path": "hpa",
10264
- "valueType": "number"
10265
- },
10266
- {
10267
- "path": "unit",
10268
- "valueType": "string"
10269
- },
10270
- {
10271
- "path": "precision",
10272
- "valueType": "number"
10273
- }
10274
- ],
10305
+ "pressure-sensor": [{
10306
+ "path": "hpa",
10307
+ "valueType": "number"
10308
+ }],
10275
10309
  "privacy-mask": [{
10276
10310
  "path": "enabled",
10277
10311
  "valueType": "boolean"
@@ -10582,20 +10616,10 @@ Object.freeze({
10582
10616
  "path": "tampered",
10583
10617
  "valueType": "boolean"
10584
10618
  }],
10585
- "temperature-sensor": [
10586
- {
10587
- "path": "celsius",
10588
- "valueType": "number"
10589
- },
10590
- {
10591
- "path": "unit",
10592
- "valueType": "string"
10593
- },
10594
- {
10595
- "path": "precision",
10596
- "valueType": "number"
10597
- }
10598
- ],
10619
+ "temperature-sensor": [{
10620
+ "path": "celsius",
10621
+ "valueType": "number"
10622
+ }],
10599
10623
  "update": [
10600
10624
  {
10601
10625
  "path": "currentVersion",
@@ -10706,10 +10730,6 @@ Object.freeze({
10706
10730
  "path": "temperature",
10707
10731
  "valueType": "number"
10708
10732
  },
10709
- {
10710
- "path": "temperatureUnit",
10711
- "valueType": "string"
10712
- },
10713
10733
  {
10714
10734
  "path": "humidity",
10715
10735
  "valueType": "number"
@@ -10718,18 +10738,10 @@ Object.freeze({
10718
10738
  "path": "pressure",
10719
10739
  "valueType": "number"
10720
10740
  },
10721
- {
10722
- "path": "pressureUnit",
10723
- "valueType": "string"
10724
- },
10725
10741
  {
10726
10742
  "path": "windSpeed",
10727
10743
  "valueType": "number"
10728
10744
  },
10729
- {
10730
- "path": "windSpeedUnit",
10731
- "valueType": "string"
10732
- },
10733
10745
  {
10734
10746
  "path": "windBearing",
10735
10747
  "valueType": "number"
@@ -11122,7 +11134,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11122
11134
  return DeviceRole;
11123
11135
  }({});
11124
11136
  //#endregion
11125
- //#region ../types/dist/composition-kEl9mevu.mjs
11137
+ //#region ../types/dist/composition-B85l9j1M.mjs
11126
11138
  /**
11127
11139
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11128
11140
  *
@@ -11877,15 +11889,67 @@ function validateExpressionSource(src) {
11877
11889
  }
11878
11890
  return null;
11879
11891
  }
11880
- /** A composed device's stableId is this prefix plus the block id. */
11881
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11882
- /** Longest snippet a `code` source may carry (slice 4). */
11883
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11892
+ /**
11893
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11894
+ * id. Split out so helper schemas can name it without a cycle through
11895
+ * `composition.ts` (D659).
11896
+ */
11884
11897
  var CompositionSourceRefSchema = object({
11885
11898
  /** The owning addon; `stableId` is unique only within it. */
11886
11899
  addonId: string().min(1),
11887
11900
  stableId: string().min(1)
11888
11901
  });
11902
+ var GROUP_TEMPLATE_IDS = [
11903
+ "temperature",
11904
+ "humidity",
11905
+ "pressure",
11906
+ "illuminance",
11907
+ "power",
11908
+ "energy",
11909
+ "battery",
11910
+ "numeric",
11911
+ "motion",
11912
+ "contact",
11913
+ "leak",
11914
+ "smoke",
11915
+ "occupancy",
11916
+ "binary"
11917
+ ];
11918
+ var NUMERIC_AGGREGATES = [
11919
+ "mean",
11920
+ "median",
11921
+ "min",
11922
+ "max",
11923
+ "sum"
11924
+ ];
11925
+ var BINARY_AGGREGATES = ["any", "all"];
11926
+ var AGGREGATE_REDUCERS = [
11927
+ "mean",
11928
+ "median",
11929
+ "min",
11930
+ "max",
11931
+ "sum",
11932
+ "any",
11933
+ "all"
11934
+ ];
11935
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11936
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11937
+ /** One member. The field is stored so occupancy can name motion or binary. */
11938
+ var HelperMemberSchema = object({
11939
+ source: CompositionSourceRefSchema,
11940
+ cap: string().min(1),
11941
+ fieldPath: string().min(1)
11942
+ });
11943
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11944
+ kind: literal("group"),
11945
+ templateId: GroupTemplateIdSchema,
11946
+ aggregate: AggregateReducerSchema,
11947
+ members: array(HelperMemberSchema).min(1).max(64)
11948
+ })]);
11949
+ /** A composed device's stableId is this prefix plus the block id. */
11950
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11951
+ /** Longest snippet a `code` source may carry (slice 4). */
11952
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11889
11953
  /** One field of one capability of one source device. */
11890
11954
  var CompositionFieldReadSchema = object({
11891
11955
  source: CompositionSourceRefSchema,
@@ -11929,7 +11993,33 @@ var CompositionCodeSourceSchema = object({
11929
11993
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11930
11994
  CompositionFromSourceSchema,
11931
11995
  CompositionExpressionSourceSchema,
11932
- CompositionCodeSourceSchema
11996
+ CompositionCodeSourceSchema,
11997
+ object({
11998
+ kind: literal("aggregate"),
11999
+ reducer: _enum([
12000
+ "mean",
12001
+ "median",
12002
+ "min",
12003
+ "max",
12004
+ "sum",
12005
+ "any",
12006
+ "all"
12007
+ ]),
12008
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12009
+ /** `value` is the reading; the others are the member counts. */
12010
+ publish: _enum([
12011
+ "value",
12012
+ "included",
12013
+ "excluded",
12014
+ "total"
12015
+ ]),
12016
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12017
+ empty: _enum([
12018
+ "null",
12019
+ "omit",
12020
+ "offline"
12021
+ ])
12022
+ })
11933
12023
  ]);
11934
12024
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11935
12025
  kind: literal("forward"),
@@ -11975,7 +12065,13 @@ var CompositionSchema = object({
11975
12065
  kind: literal("existing"),
11976
12066
  device: CompositionSourceRefSchema
11977
12067
  })]),
11978
- features: array(CompositionFeatureSchema).min(1).max(16)
12068
+ features: array(CompositionFeatureSchema).min(1).max(16),
12069
+ /**
12070
+ * Present when the block is a helper template. Absent on a free
12071
+ * composition (the room example, device 4603). The editor reads this,
12072
+ * never the compiled features.
12073
+ */
12074
+ helper: HelperSpecSchema.optional()
11979
12075
  }).superRefine((composition, ctx) => {
11980
12076
  const seen = /* @__PURE__ */ new Set();
11981
12077
  composition.features.forEach((feature, index) => {
@@ -18112,7 +18208,11 @@ var CompositionProblemSchema = object({
18112
18208
  "item-field-reserved",
18113
18209
  "target-missing",
18114
18210
  "target-unreadable",
18115
- "not-replaceable"
18211
+ "not-replaceable",
18212
+ "native-capability-replace",
18213
+ "mixed-quantity",
18214
+ "helper-mismatch",
18215
+ "member-constraint"
18116
18216
  ]),
18117
18217
  path: string(),
18118
18218
  message: string()
@@ -18133,7 +18233,8 @@ var CompositionUnavailableModeSchema = _enum([
18133
18233
  "null",
18134
18234
  "offline",
18135
18235
  "unknown",
18136
- "release"
18236
+ "release",
18237
+ "omit"
18137
18238
  ]);
18138
18239
  /**
18139
18240
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18170,9 +18271,11 @@ var CompositionFieldPlanSchema = object({
18170
18271
  reads: array(CompositionFieldReadSchema)
18171
18272
  });
18172
18273
  /**
18173
- * What a feature does to its target (D663): `new` builds a capability on a new
18174
- * device; on an existing device it `add`s a capability the device lacks, or
18175
- * `replace`s named fields of one it has natively.
18274
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18275
+ * new device; on an existing device it `add`s a capability the device lacks.
18276
+ * Replacing a capability the device already serves is refused
18277
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18278
+ * wire for a claim the hub already holds.
18176
18279
  */
18177
18280
  var CompositionFeatureModeSchema = _enum([
18178
18281
  "new",
@@ -18345,6 +18448,235 @@ Object.freeze({
18345
18448
  result: ["number", "null"]
18346
18449
  }
18347
18450
  });
18451
+ /**
18452
+ * One catalog row per base reading a group helper can build (D944 §3).
18453
+ * Data, not a form default written into the block.
18454
+ */
18455
+ var NOW = {
18456
+ fieldPath: "lastFetchedAt",
18457
+ expr: "now"
18458
+ };
18459
+ var CHANGED = {
18460
+ fieldPath: "lastChangedAt",
18461
+ expr: "now"
18462
+ };
18463
+ function numeric(input) {
18464
+ return {
18465
+ ...input,
18466
+ family: "numeric",
18467
+ deviceType: DeviceType.Sensor,
18468
+ memberDeviceType: DeviceType.Sensor,
18469
+ baseUnit: baseUnitOf(input.quantity),
18470
+ aggregates: NUMERIC_AGGREGATES,
18471
+ accept: [{
18472
+ cap: input.outputCap,
18473
+ fieldPath: input.outputField
18474
+ }]
18475
+ };
18476
+ }
18477
+ function binary(input) {
18478
+ return {
18479
+ ...input,
18480
+ family: "binary",
18481
+ deviceType: DeviceType.Sensor,
18482
+ memberDeviceType: DeviceType.Sensor,
18483
+ quantity: "none",
18484
+ baseUnit: void 0,
18485
+ aggregates: BINARY_AGGREGATES,
18486
+ empty: "offline"
18487
+ };
18488
+ }
18489
+ var GROUP_TEMPLATES = [
18490
+ numeric({
18491
+ id: "temperature",
18492
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18493
+ role: DeviceRole.TemperatureSensor,
18494
+ outputCap: "temperature-sensor",
18495
+ outputField: "celsius",
18496
+ quantity: "temperature",
18497
+ defaultAggregate: "mean",
18498
+ empty: "null",
18499
+ companions: [NOW]
18500
+ }),
18501
+ numeric({
18502
+ id: "humidity",
18503
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18504
+ role: DeviceRole.HumiditySensor,
18505
+ outputCap: "humidity-sensor",
18506
+ outputField: "percent",
18507
+ quantity: "humidity",
18508
+ defaultAggregate: "mean",
18509
+ empty: "null",
18510
+ companions: [NOW]
18511
+ }),
18512
+ numeric({
18513
+ id: "pressure",
18514
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18515
+ role: DeviceRole.PressureSensor,
18516
+ outputCap: "pressure-sensor",
18517
+ outputField: "hpa",
18518
+ quantity: "pressure",
18519
+ defaultAggregate: "mean",
18520
+ empty: "null",
18521
+ companions: [NOW]
18522
+ }),
18523
+ numeric({
18524
+ id: "illuminance",
18525
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18526
+ role: DeviceRole.AmbientLightSensor,
18527
+ outputCap: "ambient-light-sensor",
18528
+ outputField: "lux",
18529
+ quantity: "illuminance",
18530
+ defaultAggregate: "mean",
18531
+ empty: "null",
18532
+ companions: [NOW]
18533
+ }),
18534
+ numeric({
18535
+ id: "power",
18536
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18537
+ role: DeviceRole.PowerSensor,
18538
+ outputCap: "power-meter",
18539
+ outputField: "watts",
18540
+ quantity: "power",
18541
+ defaultAggregate: "sum",
18542
+ empty: "omit",
18543
+ companions: [NOW]
18544
+ }),
18545
+ numeric({
18546
+ id: "energy",
18547
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18548
+ role: DeviceRole.EnergySensor,
18549
+ outputCap: "power-meter",
18550
+ outputField: "kwhTotal",
18551
+ quantity: "energy",
18552
+ defaultAggregate: "sum",
18553
+ empty: "omit",
18554
+ companions: [NOW]
18555
+ }),
18556
+ numeric({
18557
+ id: "battery",
18558
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18559
+ role: DeviceRole.BatterySensor,
18560
+ outputCap: "battery",
18561
+ outputField: "percentage",
18562
+ quantity: "percentage",
18563
+ defaultAggregate: "mean",
18564
+ empty: "null",
18565
+ companions: [
18566
+ {
18567
+ fieldPath: "charging",
18568
+ expr: "\"unknown\""
18569
+ },
18570
+ {
18571
+ fieldPath: "sleeping",
18572
+ expr: "false"
18573
+ },
18574
+ {
18575
+ fieldPath: "lastUpdated",
18576
+ expr: "now"
18577
+ }
18578
+ ]
18579
+ }),
18580
+ numeric({
18581
+ id: "numeric",
18582
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18583
+ role: DeviceRole.NumericSensor,
18584
+ outputCap: "numeric-sensor",
18585
+ outputField: "value",
18586
+ quantity: "device",
18587
+ defaultAggregate: "mean",
18588
+ empty: "null",
18589
+ companions: [NOW]
18590
+ }),
18591
+ binary({
18592
+ id: "motion",
18593
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18594
+ role: DeviceRole.MotionSensor,
18595
+ outputCap: "motion",
18596
+ outputField: "detected",
18597
+ defaultAggregate: "any",
18598
+ accept: [{
18599
+ cap: "motion",
18600
+ fieldPath: "detected"
18601
+ }],
18602
+ companions: [{
18603
+ fieldPath: "lastDetectedAt",
18604
+ expr: "null"
18605
+ }, {
18606
+ fieldPath: "autoClearAfterMs",
18607
+ expr: "null"
18608
+ }]
18609
+ }),
18610
+ binary({
18611
+ id: "contact",
18612
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18613
+ role: DeviceRole.ContactSensor,
18614
+ outputCap: "contact",
18615
+ outputField: "entryOpen",
18616
+ defaultAggregate: "any",
18617
+ accept: [{
18618
+ cap: "contact",
18619
+ fieldPath: "entryOpen"
18620
+ }],
18621
+ companions: [CHANGED]
18622
+ }),
18623
+ binary({
18624
+ id: "leak",
18625
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18626
+ role: DeviceRole.LeakSensor,
18627
+ outputCap: "flood",
18628
+ outputField: "flooded",
18629
+ defaultAggregate: "any",
18630
+ accept: [{
18631
+ cap: "flood",
18632
+ fieldPath: "flooded"
18633
+ }],
18634
+ companions: [CHANGED]
18635
+ }),
18636
+ binary({
18637
+ id: "smoke",
18638
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18639
+ role: DeviceRole.SmokeSensor,
18640
+ outputCap: "smoke",
18641
+ outputField: "detected",
18642
+ defaultAggregate: "any",
18643
+ accept: [{
18644
+ cap: "smoke",
18645
+ fieldPath: "detected"
18646
+ }],
18647
+ companions: [CHANGED]
18648
+ }),
18649
+ binary({
18650
+ id: "occupancy",
18651
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18652
+ role: DeviceRole.BinarySensor,
18653
+ outputCap: "binary",
18654
+ outputField: "on",
18655
+ defaultAggregate: "any",
18656
+ accept: [{
18657
+ cap: "motion",
18658
+ fieldPath: "detected"
18659
+ }, {
18660
+ cap: "binary",
18661
+ fieldPath: "on"
18662
+ }],
18663
+ companions: [CHANGED]
18664
+ }),
18665
+ binary({
18666
+ id: "binary",
18667
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18668
+ role: DeviceRole.BinarySensor,
18669
+ outputCap: "binary",
18670
+ outputField: "on",
18671
+ defaultAggregate: "any",
18672
+ accept: [{
18673
+ cap: "binary",
18674
+ fieldPath: "on"
18675
+ }],
18676
+ companions: [CHANGED]
18677
+ })
18678
+ ];
18679
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18348
18680
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18349
18681
  /**
18350
18682
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20058,12 +20390,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20058
20390
  * providers detect renames by matching `uniqueId` and patch `id`
20059
20391
  * in-place so dispatch keeps working.
20060
20392
  *
20061
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20062
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20063
- * state push and are the single source of truth. The `device_class` tag is
20064
- * an HA-specific input that is consumed at the discovery boundary
20065
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20066
- * stored after that point.
20393
+ * **Display unit and precision are NOT stored here.** They are a read-time
20394
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20395
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20396
+ * neither is repeated on each sample. The `device_class` tag is an
20397
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20398
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20067
20399
  *
20068
20400
  * The `raw` escape hatch carries provider-specific attributes that don't
20069
20401
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20077,6 +20409,14 @@ var SourceInfoSchema = object({
20077
20409
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20078
20410
  * Used to detect rename when `id` changes. */
20079
20411
  uniqueId: string().optional(),
20412
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20413
+ quantity: ReadingQuantitySchema.optional(),
20414
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20415
+ dateTimeShape: _enum([
20416
+ "date",
20417
+ "time",
20418
+ "datetime"
20419
+ ]).optional(),
20080
20420
  /** Provider-specific extras that don't fit the structured slots.
20081
20421
  * Whitelist what goes here per provider — do NOT dump entire HA
20082
20422
  * attribute blobs (would explode the meta JSON column). */
@@ -40989,7 +41329,8 @@ method(object({
40989
41329
  "enum"
40990
41330
  ]),
40991
41331
  enumValues: array(string()).optional(),
40992
- item: boolean().optional()
41332
+ item: boolean().optional(),
41333
+ quantity: ReadingQuantitySchema.optional()
40993
41334
  })).readonly(),
40994
41335
  itemArray: object({
40995
41336
  path: string(),
@@ -44789,8 +45130,8 @@ var MaskPolygonShapeSchema = object({
44789
45130
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44790
45131
  var MaskGridShapeSchema = object({
44791
45132
  kind: literal("grid"),
44792
- gridWidth: number(),
44793
- gridHeight: number(),
45133
+ gridWidth: number().meta(readingMeta("none")),
45134
+ gridHeight: number().meta(readingMeta("none")),
44794
45135
  cells: array(boolean())
44795
45136
  });
44796
45137
  discriminatedUnion("kind", [
@@ -56184,46 +56525,27 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56184
56525
  }), LogStreamEntrySchema, { kind: "subscription" });
56185
56526
  object({
56186
56527
  /** Carbon dioxide concentration in ppm. */
56187
- co2Ppm: number().min(0).optional(),
56528
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56188
56529
  /** Total volatile organic compounds in ppb. */
56189
- vocPpb: number().min(0).optional(),
56530
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56190
56531
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56191
- pm25: number().min(0).optional(),
56532
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56192
56533
  /** Particulate matter ≤ 10 μm in µg/m³. */
56193
- pm10: number().min(0).optional(),
56534
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56194
56535
  /** Composite AQI value (typically 0..500). */
56195
- aqi: number().optional(),
56536
+ aqi: number().optional().meta(readingMeta("index")),
56196
56537
  /** Ms epoch when the slice was last updated. */
56197
- lastFetchedAt: number(),
56198
- /** Live display unit of the single metric this slice carries (e.g. HA
56199
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56200
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56201
- * per slice is unambiguous. */
56202
- unit: string().optional(),
56203
- /** Suggested decimal places for numeric display.
56204
- * Populated live from the upstream source when provided (e.g. HA
56205
- * `attributes.suggested_display_precision`). Falls back to
56206
- * auto-formatting when absent. */
56207
- precision: number().int().min(0).max(10).optional()
56538
+ lastFetchedAt: number()
56208
56539
  });
56209
56540
  DeviceType.Sensor;
56210
56541
  object({
56211
- /** Current illuminance in lux (lx). */
56212
- lux: number().min(0).meta({
56213
- unit: "lx",
56542
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56543
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56544
+ precision: 0,
56214
56545
  label: "Illuminance"
56215
- }),
56546
+ })),
56216
56547
  /** Ms epoch when the slice was last updated. */
56217
- lastFetchedAt: number(),
56218
- /** Live display unit from the upstream source (e.g. HA
56219
- * `attributes.unit_of_measurement`). The UI prefers this over the
56220
- * role's canonical unit. Absent → fall back to the canonical unit. */
56221
- unit: string().optional(),
56222
- /** Suggested decimal places for numeric display.
56223
- * Populated live from the upstream source when provided (e.g. HA
56224
- * `attributes.suggested_display_precision`). Falls back to
56225
- * auto-formatting when absent. */
56226
- precision: number().int().min(0).max(10).optional()
56548
+ lastFetchedAt: number()
56227
56549
  });
56228
56550
  DeviceType.Sensor;
56229
56551
  /**
@@ -56368,10 +56690,7 @@ var BatteryStatusSchema = object({
56368
56690
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56369
56691
  * declines to band a non-finite reading, which is the correct shape.
56370
56692
  */
56371
- percentage: number().min(0).max(100).nullable().meta({
56372
- unit: "%",
56373
- label: "Charge"
56374
- }),
56693
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56375
56694
  /**
56376
56695
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56377
56696
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56398,7 +56717,14 @@ var BatteryStatusSchema = object({
56398
56717
  */
56399
56718
  sleeping: boolean().meta({ label: "Sleeping" }),
56400
56719
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56401
- lastUpdated: number(),
56720
+ lastUpdated: number().meta(readingMeta("none")),
56721
+ /**
56722
+ * Battery voltage in volts, for a cell the firmware reports as voltage
56723
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
56724
+ * that only report charge. A second reading on this cap, not a second
56725
+ * `numeric-sensor` on the same device.
56726
+ */
56727
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56402
56728
  /**
56403
56729
  * Ms epoch of the last time the device PROVED it was reachable — a
56404
56730
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56458,7 +56784,7 @@ object({
56458
56784
  DeviceType.Sensor;
56459
56785
  object({
56460
56786
  /** Current level as 0..100 inclusive. Firmware-reported. */
56461
- percentage: number().min(0).max(100),
56787
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56462
56788
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56463
56789
  lastChangedAt: number()
56464
56790
  });
@@ -56843,21 +57169,21 @@ object({
56843
57169
  /** Single setpoint in Celsius. Used by single-target modes
56844
57170
  * (heat / cool / dry). Null when the device is in a range mode
56845
57171
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
56846
- target: number().nullable(),
57172
+ target: number().nullable().meta(readingMeta("temperature")),
56847
57173
  /** Upper bound of the dual setpoint in Celsius. Populated only
56848
57174
  * in `heat_cool` mode. */
56849
- targetHigh: number().nullable(),
57175
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
56850
57176
  /** Lower bound of the dual setpoint in Celsius. Populated only
56851
57177
  * in `heat_cool` mode. */
56852
- targetLow: number().nullable(),
57178
+ targetLow: number().nullable().meta(readingMeta("temperature")),
56853
57179
  /** Target relative humidity (0..100). Null when the device has no
56854
57180
  * humidity control. */
56855
- targetHumidity: number().min(0).max(100).nullable(),
57181
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56856
57182
  /** Current measured temperature in Celsius. Read-only — pushed by
56857
57183
  * the device's internal sensor. */
56858
- currentTemp: number().nullable(),
57184
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
56859
57185
  /** Current measured relative humidity (0..100). Read-only. */
56860
- currentHumidity: number().min(0).max(100).nullable(),
57186
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56861
57187
  /** Vertical louver swing on/off. Null when the device has no vertical
56862
57188
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
56863
57189
  swingVertical: boolean().nullable(),
@@ -56865,11 +57191,11 @@ object({
56865
57191
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
56866
57192
  swingHorizontal: boolean().nullable(),
56867
57193
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
56868
- minTemp: number().nullable().optional(),
57194
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
56869
57195
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
56870
- maxTemp: number().nullable().optional(),
57196
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
56871
57197
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
56872
- tempStep: number().positive().nullable().optional(),
57198
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
56873
57199
  /** Ms epoch when the slice was last updated (push or command). */
56874
57200
  lastFetchedAt: number()
56875
57201
  });
@@ -56948,17 +57274,17 @@ DeviceType.Thermostat, DeviceType.Climate, method(object({
56948
57274
  * modes to expose in the picker without a slice fetch.
56949
57275
  */
56950
57276
  var RgbTripletSchema = object({
56951
- r: number().int().min(0).max(255),
56952
- g: number().int().min(0).max(255),
56953
- b: number().int().min(0).max(255)
57277
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57278
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57279
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
56954
57280
  });
56955
57281
  var HsvTripletSchema = object({
56956
57282
  /** Hue in degrees, 0..360. */
56957
- h: number().min(0).max(360),
57283
+ h: number().min(0).max(360).meta(readingMeta("none")),
56958
57284
  /** Saturation as 0..100 inclusive. */
56959
- s: number().min(0).max(100),
57285
+ s: number().min(0).max(100).meta(readingMeta("none")),
56960
57286
  /** Value/brightness as 0..100 inclusive. */
56961
- v: number().min(0).max(100)
57287
+ v: number().min(0).max(100).meta(readingMeta("none"))
56962
57288
  });
56963
57289
  var ColorInputSchema = discriminatedUnion("mode", [
56964
57290
  object({
@@ -56987,7 +57313,7 @@ object({
56987
57313
  /** Populated when `mode === 'hsv'`. */
56988
57314
  hsv: HsvTripletSchema.optional(),
56989
57315
  /** Populated when `mode === 'mired'`. */
56990
- mireds: number().int().optional(),
57316
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
56991
57317
  /** Ms epoch of the last operator-driven change. */
56992
57318
  lastChangedAt: number()
56993
57319
  });
@@ -57111,18 +57437,12 @@ _enum([
57111
57437
  "empty",
57112
57438
  "full"
57113
57439
  ]);
57114
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57115
- * each. */
57116
- var ConsumableRemainingUnitSchema = _enum([
57117
- "days",
57118
- "hours",
57119
- "cycles"
57120
- ]);
57121
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57122
- * days). `value` null = unknown (D393). */
57440
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57441
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57442
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57123
57443
  var ConsumableRemainingSchema = object({
57124
- value: number().min(0).nullable(),
57125
- unit: ConsumableRemainingUnitSchema
57444
+ value: number().min(0).nullable().meta(readingMeta("device")),
57445
+ kind: ConsumableRemainingKindSchema
57126
57446
  });
57127
57447
  var ConsumablesStatusSchema = object({
57128
57448
  items: array(object({
@@ -57131,7 +57451,7 @@ var ConsumablesStatusSchema = object({
57131
57451
  /** Display name. */
57132
57452
  label: string().min(1),
57133
57453
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57134
- level: number().min(0).max(100).nullable(),
57454
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57135
57455
  /** The provider's discrete verdict when known (binary mode). */
57136
57456
  status: ConsumableStatusSchema.nullable(),
57137
57457
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57148,7 +57468,7 @@ var ConsumablesStatusSchema = object({
57148
57468
  })),
57149
57469
  lastChangedAt: number()
57150
57470
  });
57151
- Object.values(DeviceType), [...ConsumableRemainingUnitSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57471
+ Object.values(DeviceType), [...ConsumableRemainingKindSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57152
57472
  Object.values(DeviceType), method(object({
57153
57473
  deviceId: number().int().nonnegative(),
57154
57474
  key: string().min(1)
@@ -57173,39 +57493,17 @@ object({
57173
57493
  /** Current value. Numeric kind → number; select/text/datetime → string.
57174
57494
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
57175
57495
  * or time-only — HA accepts all three forms). */
57176
- value: union([number(), string()]),
57496
+ value: union([number().meta(readingMeta("device")), string()]),
57177
57497
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
57178
57498
  options: array(string()),
57179
57499
  /** Ms epoch when the slice was last updated. */
57180
57500
  lastChangedAt: number(),
57181
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
57182
- * Populated live from HA `attributes.unit_of_measurement` on each push.
57183
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57184
- unit: string().optional(),
57185
- /** Minimum allowed value. Populated live from HA `attributes.min`.
57186
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57187
- min: number().optional(),
57188
- /** Maximum allowed value. Populated live from HA `attributes.max`.
57189
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57190
- max: number().optional(),
57191
- /** Step increment. Populated live from HA `attributes.step`.
57192
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57193
- step: number().optional(),
57194
- /** Suggested decimal places for numeric display. Populated live from HA
57195
- * `attributes.suggested_display_precision`, or derived from `step` when the
57196
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
57197
- * other kinds. Falls back to auto-formatting when absent. */
57198
- precision: number().int().min(0).max(10).optional(),
57199
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
57200
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
57201
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
57202
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
57203
- * `has_date`/`has_time` attributes on each push. */
57204
- format: _enum([
57205
- "date",
57206
- "time",
57207
- "datetime"
57208
- ]).optional()
57501
+ /** Minimum allowed value, in the device quantity's base unit. */
57502
+ min: number().optional().meta(readingMeta("device")),
57503
+ /** Maximum allowed value, in the device quantity's base unit. */
57504
+ max: number().optional().meta(readingMeta("device")),
57505
+ /** Step increment, in the device quantity's base unit. */
57506
+ step: number().optional().meta(readingMeta("device"))
57209
57507
  });
57210
57508
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
57211
57509
  object({
@@ -57243,9 +57541,9 @@ object({
57243
57541
  ]),
57244
57542
  /** 0 = fully closed, 100 = fully open. Null when the device has
57245
57543
  * no intermediate position surface. */
57246
- position: number().min(0).max(100).nullable(),
57544
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57247
57545
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
57248
- tiltPosition: number().min(0).max(100).nullable(),
57546
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57249
57547
  /** Ms epoch when the slice was last updated. */
57250
57548
  lastChangedAt: number()
57251
57549
  });
@@ -57303,9 +57601,9 @@ var NormalizedRangeSchema$1 = object({
57303
57601
  object({
57304
57602
  mode: DayNightModeSchema,
57305
57603
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
57306
- sensitivity: number().optional(),
57604
+ sensitivity: number().optional().meta(readingMeta("none")),
57307
57605
  /** Delay before the IR-cut filter flips, in seconds. */
57308
- switchDelaySec: number().optional(),
57606
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
57309
57607
  lastFetchedAt: number()
57310
57608
  });
57311
57609
  /**
@@ -57460,11 +57758,11 @@ object({
57460
57758
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
57461
57759
  availablePrograms: array(string()),
57462
57760
  /** Seconds remaining on the active cycle, or null when not running / not known. */
57463
- remainingSeconds: number().int().nonnegative().nullable(),
57761
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57464
57762
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
57465
- delayedStartSeconds: number().int().nonnegative().nullable(),
57763
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57466
57764
  /** Cycle completion 0..100, or null when not applicable. */
57467
- progressPercent: number().min(0).max(100).nullable(),
57765
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57468
57766
  /** True when the door is reported open; null when the door state is not known. */
57469
57767
  doorOpen: boolean().nullable(),
57470
57768
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -57488,7 +57786,7 @@ object({
57488
57786
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57489
57787
  machineCareDue: boolean().nullable(),
57490
57788
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57491
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
57789
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
57492
57790
  /** Option states: null when the appliance does not expose the option or has not said. */
57493
57791
  halfLoad: boolean().nullable(),
57494
57792
  hygienePlus: boolean().nullable(),
@@ -57503,7 +57801,7 @@ object({
57503
57801
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
57504
57802
  activeEvents: array(string()),
57505
57803
  /** Numeric error code when the appliance reports one; null otherwise. */
57506
- errorCode: number().nullable(),
57804
+ errorCode: number().nullable().meta(readingMeta("none")),
57507
57805
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
57508
57806
  errorLabel: string().nullable(),
57509
57807
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -57649,24 +57947,14 @@ object({
57649
57947
  /** Ms epoch of the last press. null = never observed since this provider started. */
57650
57948
  lastPressedAt: number().nullable(),
57651
57949
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
57652
- pressCountSinceStart: number()
57950
+ pressCountSinceStart: number().meta(readingMeta("count"))
57653
57951
  });
57654
57952
  object({
57655
57953
  deviceId: number(),
57656
57954
  timestamp: number()
57657
57955
  });
57658
57956
  DeviceType.Button, DeviceType.Camera;
57659
- /**
57660
- * Enum-state sensor — a string value picked from a finite option set.
57661
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
57662
- * states, weather conditions, contact-source channels, mode strings).
57663
- * The option set is stable across observations; it is persisted in the
57664
- * device config blob (via the control slice at adoption) and is NOT
57665
- * stored in `sourceInfo`.
57666
- */
57667
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
57668
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
57669
- var EnumSensorDateTimeFormatSchema = _enum([
57957
+ _enum([
57670
57958
  "date",
57671
57959
  "time",
57672
57960
  "datetime"
@@ -57674,12 +57962,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
57674
57962
  object({
57675
57963
  value: string(),
57676
57964
  /**
57677
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
57678
- * as a locale date/time/datetime; absent for plain enum sensors. HA
57679
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
57680
- * a time-only sensor → `'time'`.
57965
+ * Set when `value` is a timestamp rather than an enum state, so history
57966
+ * does not learn ISO strings as states. The display shape (date / time /
57967
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
57681
57968
  */
57682
- format: EnumSensorDateTimeFormatSchema.optional(),
57969
+ timestamp: boolean().optional(),
57683
57970
  /** Ms epoch when the slice was last updated. */
57684
57971
  lastFetchedAt: number()
57685
57972
  });
@@ -57698,13 +57985,13 @@ var EventFireSchema = object({
57698
57985
  deviceId: number(),
57699
57986
  eventType: string(),
57700
57987
  data: record(string(), unknown()).nullable(),
57701
- timestamp: number(),
57702
- seq: number()
57988
+ timestamp: number().meta(readingMeta("none")),
57989
+ seq: number().meta(readingMeta("count"))
57703
57990
  });
57704
57991
  object({
57705
57992
  eventTypes: array(string()),
57706
57993
  lastEvent: EventFireSchema.nullable(),
57707
- eventCountSinceStart: number()
57994
+ eventCountSinceStart: number().meta(readingMeta("count"))
57708
57995
  });
57709
57996
  DeviceType.EventEmitter;
57710
57997
  var IdentitySchema = object({
@@ -58093,10 +58380,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
58093
58380
  object({
58094
58381
  /** Active speed as 0..100 inclusive. Null when the device has no
58095
58382
  * speed surface (single-speed fan). */
58096
- percentage: number().min(0).max(100).nullable(),
58383
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58097
58384
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
58098
58385
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
58099
- percentageStep: number().positive().optional(),
58386
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
58100
58387
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
58101
58388
  * — empty string when no preset is active or supported. */
58102
58389
  preset: string(),
@@ -58178,7 +58465,7 @@ var FeatureProbeStatusSchema = object({
58178
58465
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
58179
58466
  model: string().nullable(),
58180
58467
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
58181
- channelCount: number().nullable(),
58468
+ channelCount: number().nullable().meta(readingMeta("count")),
58182
58469
  /**
58183
58470
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
58184
58471
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -58210,9 +58497,27 @@ object({
58210
58497
  lastChangedAt: number()
58211
58498
  });
58212
58499
  DeviceType.Sensor;
58500
+ object({
58501
+ /** Members whose current value entered the aggregate. */
58502
+ included: number().int().nonnegative().meta(readingMeta("count", {
58503
+ precision: 0,
58504
+ label: "Reporting"
58505
+ })),
58506
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
58507
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
58508
+ precision: 0,
58509
+ label: "Excluded"
58510
+ })),
58511
+ /** `included` + `excluded`. */
58512
+ total: number().int().nonnegative().meta(readingMeta("count", {
58513
+ precision: 0,
58514
+ label: "Members"
58515
+ }))
58516
+ });
58517
+ DeviceType.Sensor;
58213
58518
  object({
58214
58519
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
58215
- position: number().min(0).max(100).nullable(),
58520
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58216
58521
  /** Ms epoch when the slice was last updated (push or poll). */
58217
58522
  lastFetchedAt: number()
58218
58523
  });
@@ -58221,9 +58526,9 @@ object({
58221
58526
  /** Whether the humidifier is currently on. */
58222
58527
  on: boolean(),
58223
58528
  /** Current measured relative humidity (0..100). Null when not reported. */
58224
- currentHumidity: number().min(0).max(100).nullable(),
58529
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58225
58530
  /** Target relative humidity (0..100). Null when no setpoint surface. */
58226
- targetHumidity: number().min(0).max(100).nullable(),
58531
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58227
58532
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
58228
58533
  * device has no mode surface. */
58229
58534
  mode: string().nullable(),
@@ -58233,9 +58538,9 @@ object({
58233
58538
  * `idle` / `off`). Null when the device doesn't report it. */
58234
58539
  action: string().nullable(),
58235
58540
  /** HA `min_humidity` attribute. Null → UI uses 0. */
58236
- minHumidity: number().nullable(),
58541
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
58237
58542
  /** HA `max_humidity` attribute. Null → UI uses 100. */
58238
- maxHumidity: number().nullable(),
58543
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
58239
58544
  /** Ms epoch when the slice was last updated. */
58240
58545
  lastChangedAt: number()
58241
58546
  });
@@ -58259,22 +58564,13 @@ DeviceType.Humidifier, method(object({
58259
58564
  auth: "protected"
58260
58565
  });
58261
58566
  object({
58262
- /** Current relative humidity, 0..100. */
58263
- percent: number().min(0).max(100).meta({
58264
- unit: "%",
58567
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
58568
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
58569
+ precision: 0,
58265
58570
  label: "Humidity"
58266
- }),
58571
+ })),
58267
58572
  /** Ms epoch when the slice was last updated. */
58268
- lastFetchedAt: number(),
58269
- /** Live display unit from the upstream source (e.g. HA
58270
- * `attributes.unit_of_measurement`). The UI prefers this over the
58271
- * role's canonical unit. Absent → fall back to the canonical unit. */
58272
- unit: string().optional(),
58273
- /** Suggested decimal places for numeric display.
58274
- * Populated live from the upstream source when provided (e.g. HA
58275
- * `attributes.suggested_display_precision`). Falls back to
58276
- * auto-formatting when absent. */
58277
- precision: number().int().min(0).max(10).optional()
58573
+ lastFetchedAt: number()
58278
58574
  });
58279
58575
  DeviceType.Sensor, DeviceType.Thermostat;
58280
58576
  object({
@@ -58282,7 +58578,7 @@ object({
58282
58578
  * entity exposes no `entity_picture` (yet). */
58283
58579
  url: string().nullable(),
58284
58580
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
58285
- lastUpdated: number().nullable()
58581
+ lastUpdated: number().nullable().meta(readingMeta("none"))
58286
58582
  });
58287
58583
  DeviceType.Image;
58288
58584
  /**
@@ -58338,19 +58634,19 @@ var NormalizedRangeSchema = object({
58338
58634
  });
58339
58635
  object({
58340
58636
  /** Normalized 0–100. */
58341
- brightness: number().optional(),
58637
+ brightness: number().optional().meta(readingMeta("none")),
58342
58638
  /** Normalized 0–100. */
58343
- contrast: number().optional(),
58639
+ contrast: number().optional().meta(readingMeta("none")),
58344
58640
  /** Normalized 0–100. */
58345
- saturation: number().optional(),
58641
+ saturation: number().optional().meta(readingMeta("none")),
58346
58642
  /** Normalized 0–100. */
58347
- sharpness: number().optional(),
58643
+ sharpness: number().optional().meta(readingMeta("none")),
58348
58644
  mirror: boolean().optional(),
58349
58645
  flip: boolean().optional(),
58350
58646
  rotate: ImageRotateSchema.optional(),
58351
58647
  whiteBalance: WhiteBalanceModeSchema.optional(),
58352
58648
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
58353
- warmth: number().optional(),
58649
+ warmth: number().optional().meta(readingMeta("none")),
58354
58650
  exposureMode: ExposureModeSchema.optional(),
58355
58651
  backlightMode: BacklightModeSchema.optional(),
58356
58652
  lastFetchedAt: number()
@@ -58595,10 +58891,10 @@ var IntercomAbilitySchema = object({
58595
58891
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
58596
58892
  codecs: array(string()),
58597
58893
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
58598
- sampleRate: number(),
58894
+ sampleRate: number().meta(readingMeta("none")),
58599
58895
  duplex: _enum(["half", "full"]),
58600
58896
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
58601
- maxBacklogMs: number()
58897
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
58602
58898
  });
58603
58899
  var IntercomStatusSchema = object({
58604
58900
  /** True while a talk session is open. */
@@ -58709,13 +59005,13 @@ object({
58709
59005
  activity: LawnMowerActivitySchema,
58710
59006
  /** 0..100 battery percentage. Null when the device has no battery
58711
59007
  * reading. */
58712
- batteryLevel: number().min(0).max(100).nullable(),
59008
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58713
59009
  /** 0..100 mowing-completion percentage of the current task, or null when no
58714
59010
  * task is active / progress is unavailable. */
58715
- progressPercent: number().min(0).max(100).nullable(),
59011
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58716
59012
  /** Current device/event code (dynamic — mostly status, sometimes an error),
58717
59013
  * or null when unknown. */
58718
- currentCode: number().nullable(),
59014
+ currentCode: number().nullable().meta(readingMeta("none")),
58719
59015
  /** Human label for {@link currentCode}, or null when undecodable. */
58720
59016
  currentCodeLabel: string().nullable(),
58721
59017
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -59177,7 +59473,7 @@ object({
59177
59473
  state: MediaPlayerStateSchema,
59178
59474
  /** Volume as 0..100 inclusive. Null when the device has no volume
59179
59475
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
59180
- volumeLevel: number().min(0).max(100).nullable(),
59476
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59181
59477
  /** Mute toggle distinct from volume=0. Null when no mute surface.
59182
59478
  * Pair with `DeviceFeature.MediaPlayerMute`. */
59183
59479
  isMuted: boolean().nullable(),
@@ -59190,10 +59486,10 @@ object({
59190
59486
  currentMedia: MediaInfoSchema.nullable(),
59191
59487
  /** Current playback position in ms. Null when not seekable or
59192
59488
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
59193
- positionMs: number().int().nonnegative().nullable(),
59489
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59194
59490
  /** Total duration of the current media in ms. Null when unknown
59195
59491
  * (live stream) or nothing is playing. */
59196
- durationMs: number().int().nonnegative().nullable(),
59492
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59197
59493
  /** Shuffle toggle. Null when no shuffle surface.
59198
59494
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
59199
59495
  shuffle: boolean().nullable(),
@@ -59493,7 +59789,7 @@ object({
59493
59789
  * it, every provider seeded it at boot, and every session left it
59494
59790
  * `detected: true` after its detach.
59495
59791
  */
59496
- autoClearAfterMs: number().nullable()
59792
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
59497
59793
  });
59498
59794
  /**
59499
59795
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -59543,7 +59839,7 @@ var MotionZoneRegionSchema = object({
59543
59839
  });
59544
59840
  object({
59545
59841
  enabled: boolean(),
59546
- sensitivity: number(),
59842
+ sensitivity: number().meta(readingMeta("none")),
59547
59843
  /** Grid region(s). Today exactly one `grid` shape. */
59548
59844
  regions: array(MotionZoneRegionSchema),
59549
59845
  lastFetchedAt: number()
@@ -59847,7 +60143,7 @@ var NavigationStatusSchema = object({
59847
60143
  following: boolean(),
59848
60144
  flash: boolean(),
59849
60145
  lightMode: NavigationLightModeSchema,
59850
- lightLevel: number().min(40).max(100),
60146
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
59851
60147
  /** Ms epoch when the slice was last updated. */
59852
60148
  lastChangedAt: number()
59853
60149
  });
@@ -59893,13 +60189,13 @@ var NetworkLinkStatusSchema = object({
59893
60189
  * one whose reading has not landed must not be drawn at 0 %. Consumers
59894
60190
  * SKIP a null rather than coerce it.
59895
60191
  */
59896
- signalPercent: number().min(0).max(100).nullable(),
60192
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59897
60193
  /** Raw received signal strength in dBm, when the firmware reports one. */
59898
- rssiDbm: number().optional(),
60194
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
59899
60195
  /** Network name of a wireless link, when the firmware reports it. */
59900
60196
  ssid: string().optional(),
59901
60197
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
59902
- lastUpdated: number()
60198
+ lastUpdated: number().meta(readingMeta("none"))
59903
60199
  });
59904
60200
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
59905
60201
  deviceId: number(),
@@ -60218,7 +60514,7 @@ object({
60218
60514
  lastError: string().nullable(),
60219
60515
  /** Number of deliveries currently buffered server-side (rate-limited
60220
60516
  * notifiers may queue). 0 when the provider sends synchronously. */
60221
- queueDepth: number().int().nonnegative(),
60517
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
60222
60518
  /** Per-feature capability matrix — authoritative source for the
60223
60519
  * compose-form field gating. */
60224
60520
  supports: NotifierSupportsSchema
@@ -60268,15 +60564,8 @@ DeviceType.Notifier, method(NotifierSendInputSchema, NotifierSendResultSchema, {
60268
60564
  acceptedAt: number()
60269
60565
  });
60270
60566
  object({
60271
- value: number(),
60272
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
60273
- * Populated live from the upstream source on each state push.
60274
- * Absent for unitless sensors. */
60275
- unit: string().optional(),
60276
- /** Suggested decimal places for numeric display.
60277
- * Populated live from the upstream source when provided.
60278
- * Falls back to auto-formatting when absent. */
60279
- precision: number().int().min(0).max(10).optional(),
60567
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
60568
+ value: number().nullable().meta(readingMeta("device")),
60280
60569
  /** Ms epoch when the slice was last updated. */
60281
60570
  lastFetchedAt: number()
60282
60571
  });
@@ -60566,23 +60855,26 @@ object({
60566
60855
  /** Food currently in the bowl (grams). Null when the device has not
60567
60856
  * reported a reading yet. On dual-hopper models this is the combined
60568
60857
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
60569
- foodLevel: number().nullable(),
60858
+ foodLevel: number().nullable().meta(readingMeta("none")),
60570
60859
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
60571
60860
  * single-hopper models. */
60572
- food1: number().nullable(),
60861
+ food1: number().nullable().meta(readingMeta("none")),
60573
60862
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
60574
60863
  * single-hopper models. */
60575
- food2: number().nullable(),
60864
+ food2: number().nullable().meta(readingMeta("none")),
60576
60865
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
60577
60866
  * (`device_class: problem`, on = low). True when the bowl is empty /
60578
60867
  * below the feeder's low threshold. */
60579
60868
  lowFood: boolean(),
60580
60869
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
60581
60870
  * device has no battery reading. */
60582
- batteryPower: number().min(0).max(100).nullable(),
60583
- /** Days of desiccant life remaining. Null when the model has no
60584
- * desiccant sensor. */
60585
- desiccantLeftDays: number().nullable(),
60871
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60872
+ /**
60873
+ * Desiccant life remaining, stored in seconds (duration base, D944).
60874
+ * Providers convert from the days the feeder reports. Null when the
60875
+ * model has no desiccant sensor.
60876
+ */
60877
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
60586
60878
  /** True while a feed is in progress. */
60587
60879
  feeding: boolean(),
60588
60880
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -60593,7 +60885,7 @@ object({
60593
60885
  * with `errorCode` for consumers that want the raw integer. */
60594
60886
  error: string().nullable(),
60595
60887
  /** Raw device error code (0 / null = no error). */
60596
- errorCode: number().nullable(),
60888
+ errorCode: number().nullable().meta(readingMeta("none")),
60597
60889
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
60598
60890
  isDualHopper: boolean(),
60599
60891
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -60603,7 +60895,7 @@ object({
60603
60895
  /** Play a chime when dispensing. */
60604
60896
  feedSound: boolean(),
60605
60897
  /** Speaker / prompt volume level (device-scaled integer). */
60606
- volume: number(),
60898
+ volume: number().meta(readingMeta("none")),
60607
60899
  /** Ms epoch when the slice was last refreshed from the cloud. */
60608
60900
  lastFetchedAt: number()
60609
60901
  });
@@ -61040,36 +61332,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
61040
61332
  });
61041
61333
  object({
61042
61334
  /** Instantaneous power draw in watts. */
61043
- watts: number().optional().meta({
61044
- unit: "W",
61045
- label: "Power"
61046
- }),
61335
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
61047
61336
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
61048
- kwhTotal: number().optional().meta({
61049
- unit: "kWh",
61050
- label: "Energy"
61051
- }),
61337
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
61052
61338
  /** Voltage in volts. */
61053
- volts: number().optional(),
61339
+ volts: number().optional().meta(readingMeta("voltage")),
61054
61340
  /** Current in amperes. */
61055
- amps: number().optional(),
61056
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
61057
- va: number().optional(),
61341
+ amps: number().optional().meta(readingMeta("current")),
61342
+ /** Apparent power in volt-amperes. */
61343
+ va: number().optional().meta(readingMeta("apparent-power")),
61058
61344
  /** Power factor (0..1) if reported. */
61059
- powerFactor: number().min(0).max(1).optional(),
61345
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
61060
61346
  /** Ms epoch when the slice was last updated. */
61061
- lastFetchedAt: number(),
61062
- /** Live display unit of the single metric this slice carries (e.g. HA
61063
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
61064
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
61065
- * unit per slice is unambiguous. The UI prefers this over the role's
61066
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
61067
- unit: string().optional(),
61068
- /** Suggested decimal places for numeric display.
61069
- * Populated live from the upstream source when provided (e.g. HA
61070
- * `attributes.suggested_display_precision`). Falls back to
61071
- * auto-formatting when absent. */
61072
- precision: number().int().min(0).max(10).optional()
61347
+ lastFetchedAt: number()
61073
61348
  });
61074
61349
  DeviceType.Sensor;
61075
61350
  /**
@@ -61090,11 +61365,11 @@ DeviceType.Sensor;
61090
61365
  */
61091
61366
  var GpsLocationSchema = object({
61092
61367
  /** Latitude in decimal degrees, -90..90. */
61093
- latitude: number().min(-90).max(90),
61368
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
61094
61369
  /** Longitude in decimal degrees, -180..180. */
61095
- longitude: number().min(-180).max(180),
61370
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
61096
61371
  /** Reported accuracy in meters (lower = better). */
61097
- accuracyMeters: number().nonnegative()
61372
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
61098
61373
  });
61099
61374
  object({
61100
61375
  /** `home` / `not_home` / any user-defined zone name. */
@@ -61106,25 +61381,16 @@ object({
61106
61381
  gps: GpsLocationSchema.nullable(),
61107
61382
  /** Optional battery level of the tracking device (0..100). Null
61108
61383
  * when not reported. */
61109
- batteryPercent: number().min(0).max(100).nullable(),
61384
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61110
61385
  /** Ms epoch when the slice was last updated. */
61111
61386
  lastChangedAt: number()
61112
61387
  });
61113
61388
  DeviceType.Presence;
61114
61389
  object({
61115
- /** Current pressure in hPa. */
61116
- hpa: number(),
61390
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
61391
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
61117
61392
  /** Ms epoch when the slice was last updated. */
61118
- lastFetchedAt: number(),
61119
- /** Live display unit from the upstream source (e.g. HA
61120
- * `attributes.unit_of_measurement`). The UI prefers this over the
61121
- * role's canonical unit. Absent → fall back to the canonical unit. */
61122
- unit: string().optional(),
61123
- /** Suggested decimal places for numeric display.
61124
- * Populated live from the upstream source when provided (e.g. HA
61125
- * `attributes.suggested_display_precision`). Falls back to
61126
- * auto-formatting when absent. */
61127
- precision: number().int().min(0).max(10).optional()
61393
+ lastFetchedAt: number()
61128
61394
  });
61129
61395
  DeviceType.Sensor;
61130
61396
  /**
@@ -61457,8 +61723,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
61457
61723
  }), _void(), { kind: "mutation" });
61458
61724
  var PtzAutotrackSettingsSchema = object({
61459
61725
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
61460
- stopDelaySeconds: number().int().min(0).max(300),
61461
- disappearDelaySeconds: number().int().min(0).max(300)
61726
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
61727
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
61462
61728
  });
61463
61729
  /**
61464
61730
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -61515,7 +61781,7 @@ object({
61515
61781
  /** null = unknown — never coerced to false (D393). */
61516
61782
  running: boolean().nullable(),
61517
61783
  /** Epoch ms the current boolean began; null while `running` is null. */
61518
- since: number().nullable(),
61784
+ since: number().nullable().meta(readingMeta("none")),
61519
61785
  source: RunningStateSourceSchema.meta({ label: "Source" }),
61520
61786
  phase: string().nullable(),
61521
61787
  /** null until a reading has been published (D393). */
@@ -62713,9 +62979,9 @@ var RecordTriggerSchema = _enum([
62713
62979
  */
62714
62980
  var RecordWindowSchema = object({
62715
62981
  trigger: RecordTriggerSchema,
62716
- day: number().int().min(0).max(6),
62717
- startMinute: number().int().min(0).max(1439),
62718
- endMinute: number().int().min(1).max(1440)
62982
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
62983
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
62984
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
62719
62985
  });
62720
62986
  /** Status of one physical volume, as the camera itself describes it. */
62721
62987
  var OnboardStorageVolumeSchema = object({
@@ -62784,10 +63050,10 @@ object({
62784
63050
  /** Master "record to the card at all" switch. */
62785
63051
  enabled: boolean().nullable(),
62786
63052
  overwriteWhenFull: boolean().nullable(),
62787
- preRecordSec: number().nullable(),
62788
- postRecordSec: number().nullable(),
63053
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
63054
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
62789
63055
  /** Length of one recorded file, in minutes. */
62790
- segmentMinutes: number().nullable(),
63056
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
62791
63057
  /** The primary track's weekly windows, flattened. */
62792
63058
  windows: array(RecordWindowSchema),
62793
63059
  /**
@@ -62803,7 +63069,7 @@ object({
62803
63069
  * provider reporting a non-zero count also reports the schedule as not
62804
63070
  * writable.
62805
63071
  */
62806
- unreadableWindows: number(),
63072
+ unreadableWindows: number().meta(readingMeta("count")),
62807
63073
  /**
62808
63074
  * The camera is scheduled to record and has NO usable storage.
62809
63075
  *
@@ -63440,7 +63706,7 @@ var CamStreamDescriptorSchema = object({
63440
63706
  url: string().optional(),
63441
63707
  codec: string().optional(),
63442
63708
  resolution: CamStreamResolutionSchema.optional(),
63443
- fps: number().positive().optional(),
63709
+ fps: number().positive().optional().meta(readingMeta("rate")),
63444
63710
  label: string().optional(),
63445
63711
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
63446
63712
  deviceFeatures: array(string()).optional(),
@@ -63470,18 +63736,18 @@ var StreamProfileSchema = _enum([
63470
63736
  ]);
63471
63737
  /** The current encoding config of one stream profile. */
63472
63738
  var StreamProfileConfigSchema = object({
63473
- width: number(),
63474
- height: number(),
63739
+ width: number().meta(readingMeta("none")),
63740
+ height: number().meta(readingMeta("none")),
63475
63741
  codec: _enum(["h264", "h265"]),
63476
- framerate: number(),
63477
- bitrate: number(),
63742
+ framerate: number().meta(readingMeta("rate")),
63743
+ bitrate: number().meta(readingMeta("bitrate")),
63478
63744
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
63479
63745
  encoderProfile: _enum([
63480
63746
  "high",
63481
63747
  "main",
63482
63748
  "baseline"
63483
63749
  ]).optional(),
63484
- gop: number().optional(),
63750
+ gop: number().optional().meta(readingMeta("none")),
63485
63751
  /**
63486
63752
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
63487
63753
  *
@@ -64124,23 +64390,13 @@ object({
64124
64390
  });
64125
64391
  DeviceType.Sensor;
64126
64392
  object({
64127
- /** Current temperature in Celsius. */
64128
- celsius: number().meta({
64129
- unit: "°C",
64393
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
64394
+ celsius: number().nullable().meta(readingMeta("temperature", {
64395
+ precision: 1,
64130
64396
  label: "Temperature"
64131
- }),
64397
+ })),
64132
64398
  /** Ms epoch when the slice was last updated (push or poll). */
64133
- lastFetchedAt: number(),
64134
- /** Live display unit from the upstream source (e.g. HA
64135
- * `attributes.unit_of_measurement`). The UI prefers this over the
64136
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
64137
- * Absent → the UI falls back to the role's canonical unit. */
64138
- unit: string().optional(),
64139
- /** Suggested decimal places for numeric display.
64140
- * Populated live from the upstream source when provided (e.g. HA
64141
- * `attributes.suggested_display_precision`). Falls back to
64142
- * auto-formatting when absent. */
64143
- precision: number().int().min(0).max(10).optional()
64399
+ lastFetchedAt: number()
64144
64400
  });
64145
64401
  DeviceType.Sensor, DeviceType.Thermostat;
64146
64402
  /**
@@ -64392,16 +64648,16 @@ object({
64392
64648
  ]),
64393
64649
  /** 0..100 battery percentage. Null when the device has no battery
64394
64650
  * reading. */
64395
- batteryLevel: number().min(0).max(100).nullable(),
64651
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64396
64652
  /** Current fan-speed token (provider-verbatim). Null when unknown or
64397
64653
  * the vacuum has no speed control. */
64398
64654
  fanSpeed: string().nullable(),
64399
64655
  /** Speed tokens the hardware accepts — drives the UI selector. */
64400
64656
  availableFanSpeeds: array(string()),
64401
64657
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64402
- progressPercent: number().min(0).max(100).nullable(),
64658
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64403
64659
  /** Current error code (0 / null = no error). */
64404
- errorCode: number().nullable(),
64660
+ errorCode: number().nullable().meta(readingMeta("none")),
64405
64661
  /** Human label for {@link errorCode}, or null when none / undecodable. */
64406
64662
  errorLabel: string().nullable(),
64407
64663
  /** Ms epoch when the slice was last updated. */
@@ -64440,7 +64696,7 @@ object({
64440
64696
  ]),
64441
64697
  /** 0 = fully closed, 100 = fully open. Null when the device has no
64442
64698
  * intermediate position surface. */
64443
- position: number().min(0).max(100).nullable(),
64699
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64444
64700
  /** Ms epoch when the slice was last updated. */
64445
64701
  lastChangedAt: number()
64446
64702
  });
@@ -64468,9 +64724,9 @@ object({
64468
64724
  DeviceType.Sensor;
64469
64725
  object({
64470
64726
  /** Current measured temperature. Null when not reported. */
64471
- currentTemp: number().nullable(),
64727
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
64472
64728
  /** Target temperature setpoint. Null when no setpoint surface. */
64473
- targetTemp: number().nullable(),
64729
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
64474
64730
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
64475
64731
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
64476
64732
  * device reports an unknown state. */
@@ -64481,9 +64737,9 @@ object({
64481
64737
  * has no away surface. */
64482
64738
  away: boolean().nullable(),
64483
64739
  /** HA `min_temp` attribute. Null when not reported. */
64484
- minTemp: number().nullable(),
64740
+ minTemp: number().nullable().meta(readingMeta("temperature")),
64485
64741
  /** HA `max_temp` attribute. Null when not reported. */
64486
- maxTemp: number().nullable(),
64742
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
64487
64743
  /** Ms epoch when the slice was last updated. */
64488
64744
  lastChangedAt: number()
64489
64745
  });
@@ -64507,25 +64763,18 @@ DeviceType.WaterHeater, method(object({
64507
64763
  auth: "protected"
64508
64764
  });
64509
64765
  object({
64510
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
64511
- * when no condition has been reported yet. */
64766
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
64512
64767
  condition: string().nullable(),
64513
- /** Current temperature in the reported unit. Null when not provided. */
64514
- temperature: number().nullable(),
64515
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
64516
- temperatureUnit: string().nullable(),
64768
+ /** Current temperature in Celsius. Null when not provided or refused. */
64769
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
64517
64770
  /** Relative humidity (0..100). Null when not provided. */
64518
- humidity: number().min(0).max(100).nullable(),
64519
- /** Barometric pressure in the reported unit. Null when not provided. */
64520
- pressure: number().nullable(),
64521
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
64522
- pressureUnit: string().nullable(),
64523
- /** Wind speed in the reported unit. Null when not provided. */
64524
- windSpeed: number().nullable(),
64525
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
64526
- windSpeedUnit: string().nullable(),
64771
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
64772
+ /** Barometric pressure in hPa. Null when not provided or refused. */
64773
+ pressure: number().nullable().meta(readingMeta("pressure")),
64774
+ /** Wind speed in metres per second. Null when not provided or refused. */
64775
+ windSpeed: number().nullable().meta(readingMeta("speed")),
64527
64776
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
64528
- windBearing: number().nullable(),
64777
+ windBearing: number().nullable().meta(readingMeta("angle")),
64529
64778
  /** Ms epoch when the slice was last updated. */
64530
64779
  lastFetchedAt: number()
64531
64780
  });
@@ -68647,6 +68896,13 @@ Object.freeze({
68647
68896
  access: "view",
68648
68897
  role: "viewer"
68649
68898
  },
68899
+ "groupStatus.getStatus": {
68900
+ capName: "group-status",
68901
+ capScope: "device",
68902
+ addonId: null,
68903
+ access: "view",
68904
+ role: "viewer"
68905
+ },
68650
68906
  "heatingValve.getStatus": {
68651
68907
  capName: "heating-valve",
68652
68908
  capScope: "device",
@@ -78469,6 +78725,33 @@ var SENSOR_CAP_VOCABULARY = {
78469
78725
  icon: "flame"
78470
78726
  }]
78471
78727
  },
78728
+ "group-status": {
78729
+ scope: "device",
78730
+ noun: t("Group", "Gruppo"),
78731
+ state: [
78732
+ {
78733
+ kind: "numeric",
78734
+ field: "included",
78735
+ quantity: t("the members reporting", "i membri che rispondono"),
78736
+ label: t("Reporting", "Rispondono"),
78737
+ icon: "users"
78738
+ },
78739
+ {
78740
+ kind: "numeric",
78741
+ field: "excluded",
78742
+ quantity: t("the members left out", "i membri esclusi"),
78743
+ label: t("Excluded", "Esclusi"),
78744
+ icon: "users"
78745
+ },
78746
+ {
78747
+ kind: "numeric",
78748
+ field: "total",
78749
+ quantity: t("the member count", "il numero di membri"),
78750
+ label: t("Members", "Membri"),
78751
+ icon: "sigma"
78752
+ }
78753
+ ]
78754
+ },
78472
78755
  "humidity-sensor": {
78473
78756
  scope: "device",
78474
78757
  noun: t("Humidity", "Umidità"),
@@ -78502,7 +78785,6 @@ var SENSOR_CAP_VOCABULARY = {
78502
78785
  field: "value",
78503
78786
  quantity: t("the reading", "il valore"),
78504
78787
  label: t("Reading", "Valore"),
78505
- unitField: "unit",
78506
78788
  icon: "gauge"
78507
78789
  }]
78508
78790
  },
@@ -78638,7 +78920,7 @@ var SENSOR_CAP_VOCABULARY = {
78638
78920
  field: "temperature",
78639
78921
  quantity: t("the outdoor temperature", "la temperatura esterna"),
78640
78922
  label: t("Temperature", "Temperatura"),
78641
- unitField: "temperatureUnit",
78923
+ unit: "°C",
78642
78924
  icon: "thermometer"
78643
78925
  },
78644
78926
  {
@@ -78654,7 +78936,7 @@ var SENSOR_CAP_VOCABULARY = {
78654
78936
  field: "windSpeed",
78655
78937
  quantity: t("the wind speed", "la velocità del vento"),
78656
78938
  label: t("Wind", "Vento"),
78657
- unitField: "windSpeedUnit",
78939
+ unit: "m/s",
78658
78940
  icon: "wind"
78659
78941
  },
78660
78942
  {
@@ -78662,7 +78944,7 @@ var SENSOR_CAP_VOCABULARY = {
78662
78944
  field: "pressure",
78663
78945
  quantity: t("the air pressure", "la pressione atmosferica"),
78664
78946
  label: t("Pressure", "Pressione"),
78665
- unitField: "pressureUnit",
78947
+ unit: "hPa",
78666
78948
  icon: "gauge"
78667
78949
  }
78668
78950
  ]
@@ -79935,6 +80217,11 @@ Object.freeze({
79935
80217
  form: "single",
79936
80218
  optional: false
79937
80219
  }],
80220
+ "groupStatus.getStatus": [{
80221
+ name: "deviceId",
80222
+ form: "single",
80223
+ optional: false
80224
+ }],
79938
80225
  "heatingValve.getStatus": [{
79939
80226
  name: "deviceId",
79940
80227
  form: "single",