@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
@@ -5567,7 +5567,7 @@ var ZodIssueCode = {
5567
5567
  var ZodFirstPartyTypeKind;
5568
5568
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5569
5569
  //#endregion
5570
- //#region ../types/dist/sleep-BErAxhul.mjs
5570
+ //#region ../types/dist/sleep-B81gwQuR.mjs
5571
5571
  /**
5572
5572
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5573
5573
  * window to float samples (D455).
@@ -8302,6 +8302,99 @@ function defineEntryKind(name, spec) {
8302
8302
  if (defined === void 0) throw new DeclarationRefusedError(`entry kind "${name}" was not built`);
8303
8303
  return defined;
8304
8304
  }
8305
+ var READING_QUANTITIES = [
8306
+ "temperature",
8307
+ "pressure",
8308
+ "speed",
8309
+ "precipitation",
8310
+ "precipitation-rate",
8311
+ "length",
8312
+ "area",
8313
+ "illuminance",
8314
+ "irradiance",
8315
+ "power",
8316
+ "energy",
8317
+ "voltage",
8318
+ "current",
8319
+ "apparent-power",
8320
+ "humidity",
8321
+ "percentage",
8322
+ "concentration-volume",
8323
+ "concentration-mass",
8324
+ "concentration-ppb",
8325
+ "volume",
8326
+ "duration",
8327
+ "duration-ms",
8328
+ "information",
8329
+ "sound-level",
8330
+ "signal-level",
8331
+ "colour-temperature",
8332
+ "angle",
8333
+ "bitrate",
8334
+ "rate",
8335
+ "mass",
8336
+ "index",
8337
+ "ratio",
8338
+ "count",
8339
+ "none",
8340
+ "device"
8341
+ ];
8342
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8343
+ new Set(READING_QUANTITIES);
8344
+ /**
8345
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8346
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8347
+ */
8348
+ var QUANTITY_BASE = {
8349
+ temperature: "°C",
8350
+ pressure: "hPa",
8351
+ speed: "m/s",
8352
+ precipitation: "mm",
8353
+ "precipitation-rate": "mm/h",
8354
+ length: "m",
8355
+ area: "m²",
8356
+ illuminance: "lx",
8357
+ irradiance: "W/m²",
8358
+ power: "W",
8359
+ energy: "kWh",
8360
+ voltage: "V",
8361
+ current: "A",
8362
+ "apparent-power": "VA",
8363
+ humidity: "%",
8364
+ percentage: "%",
8365
+ "concentration-volume": "ppm",
8366
+ "concentration-mass": "µg/m³",
8367
+ "concentration-ppb": "ppb",
8368
+ volume: "L",
8369
+ duration: "s",
8370
+ "duration-ms": "ms",
8371
+ information: "B",
8372
+ "sound-level": "dBFS",
8373
+ "signal-level": "dBm",
8374
+ "colour-temperature": "mired",
8375
+ angle: "°",
8376
+ bitrate: "kbps",
8377
+ rate: "fps",
8378
+ mass: "kg",
8379
+ index: "none",
8380
+ ratio: "none",
8381
+ count: "none",
8382
+ none: "none",
8383
+ device: "device"
8384
+ };
8385
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8386
+ function baseUnitOf(quantity) {
8387
+ const symbol = QUANTITY_BASE[quantity];
8388
+ if (symbol === "none" || symbol === "device") return void 0;
8389
+ return symbol;
8390
+ }
8391
+ function readingMeta(quantity, extras) {
8392
+ return {
8393
+ quantity,
8394
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8395
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8396
+ };
8397
+ }
8305
8398
  /** Shared Zod schemas used across streaming capabilities. */
8306
8399
  var CamProfileSchema = _enum([
8307
8400
  "high",
@@ -8329,8 +8422,8 @@ var CamStreamKindSchema = _enum([
8329
8422
  "derived"
8330
8423
  ]);
8331
8424
  var CamStreamResolutionSchema = object({
8332
- width: number().int().positive(),
8333
- height: number().int().positive()
8425
+ width: number().int().positive().meta(readingMeta("none")),
8426
+ height: number().int().positive().meta(readingMeta("none"))
8334
8427
  });
8335
8428
  var CameraStreamSchema = object({
8336
8429
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8844,6 +8937,7 @@ var CAP_NAMES = Object.freeze([
8844
8937
  "filesystem-browse",
8845
8938
  "flood",
8846
8939
  "gas",
8940
+ "group-status",
8847
8941
  "heating-valve",
8848
8942
  "humidifier",
8849
8943
  "humidity-sensor",
@@ -8973,14 +9067,6 @@ Object.freeze({
8973
9067
  {
8974
9068
  "path": "aqi",
8975
9069
  "valueType": "number"
8976
- },
8977
- {
8978
- "path": "unit",
8979
- "valueType": "string"
8980
- },
8981
- {
8982
- "path": "precision",
8983
- "valueType": "number"
8984
9070
  }
8985
9071
  ],
8986
9072
  "alarm-panel": [
@@ -9016,20 +9102,10 @@ Object.freeze({
9016
9102
  "valueType": "boolean"
9017
9103
  }
9018
9104
  ],
9019
- "ambient-light-sensor": [
9020
- {
9021
- "path": "lux",
9022
- "valueType": "number"
9023
- },
9024
- {
9025
- "path": "unit",
9026
- "valueType": "string"
9027
- },
9028
- {
9029
- "path": "precision",
9030
- "valueType": "number"
9031
- }
9032
- ],
9105
+ "ambient-light-sensor": [{
9106
+ "path": "lux",
9107
+ "valueType": "number"
9108
+ }],
9033
9109
  "audio-metrics": [
9034
9110
  {
9035
9111
  "path": "ts",
@@ -9109,6 +9185,10 @@ Object.freeze({
9109
9185
  "path": "lastUpdated",
9110
9186
  "valueType": "number"
9111
9187
  },
9188
+ {
9189
+ "path": "volts",
9190
+ "valueType": "number"
9191
+ },
9112
9192
  {
9113
9193
  "path": "lastContactAt",
9114
9194
  "valueType": "number"
@@ -9351,10 +9431,6 @@ Object.freeze({
9351
9431
  "datetime"
9352
9432
  ]
9353
9433
  },
9354
- {
9355
- "path": "unit",
9356
- "valueType": "string"
9357
- },
9358
9434
  {
9359
9435
  "path": "min",
9360
9436
  "valueType": "number"
@@ -9366,19 +9442,6 @@ Object.freeze({
9366
9442
  {
9367
9443
  "path": "step",
9368
9444
  "valueType": "number"
9369
- },
9370
- {
9371
- "path": "precision",
9372
- "valueType": "number"
9373
- },
9374
- {
9375
- "path": "format",
9376
- "valueType": "enum",
9377
- "enumValues": [
9378
- "date",
9379
- "time",
9380
- "datetime"
9381
- ]
9382
9445
  }
9383
9446
  ],
9384
9447
  "cover": [
@@ -9619,13 +9682,8 @@ Object.freeze({
9619
9682
  "path": "value",
9620
9683
  "valueType": "string"
9621
9684
  }, {
9622
- "path": "format",
9623
- "valueType": "enum",
9624
- "enumValues": [
9625
- "date",
9626
- "time",
9627
- "datetime"
9628
- ]
9685
+ "path": "timestamp",
9686
+ "valueType": "boolean"
9629
9687
  }],
9630
9688
  "event-emitter": [
9631
9689
  {
@@ -9698,6 +9756,20 @@ Object.freeze({
9698
9756
  "path": "detected",
9699
9757
  "valueType": "boolean"
9700
9758
  }],
9759
+ "group-status": [
9760
+ {
9761
+ "path": "included",
9762
+ "valueType": "number"
9763
+ },
9764
+ {
9765
+ "path": "excluded",
9766
+ "valueType": "number"
9767
+ },
9768
+ {
9769
+ "path": "total",
9770
+ "valueType": "number"
9771
+ }
9772
+ ],
9701
9773
  "heating-valve": [{
9702
9774
  "path": "position",
9703
9775
  "valueType": "number"
@@ -9732,20 +9804,10 @@ Object.freeze({
9732
9804
  "valueType": "number"
9733
9805
  }
9734
9806
  ],
9735
- "humidity-sensor": [
9736
- {
9737
- "path": "percent",
9738
- "valueType": "number"
9739
- },
9740
- {
9741
- "path": "unit",
9742
- "valueType": "string"
9743
- },
9744
- {
9745
- "path": "precision",
9746
- "valueType": "number"
9747
- }
9748
- ],
9807
+ "humidity-sensor": [{
9808
+ "path": "percent",
9809
+ "valueType": "number"
9810
+ }],
9749
9811
  "image": [{
9750
9812
  "path": "url",
9751
9813
  "valueType": "string"
@@ -10084,20 +10146,10 @@ Object.freeze({
10084
10146
  "valueType": "boolean"
10085
10147
  }
10086
10148
  ],
10087
- "numeric-sensor": [
10088
- {
10089
- "path": "value",
10090
- "valueType": "number"
10091
- },
10092
- {
10093
- "path": "unit",
10094
- "valueType": "string"
10095
- },
10096
- {
10097
- "path": "precision",
10098
- "valueType": "number"
10099
- }
10100
- ],
10149
+ "numeric-sensor": [{
10150
+ "path": "value",
10151
+ "valueType": "number"
10152
+ }],
10101
10153
  "pet-feeder": [
10102
10154
  {
10103
10155
  "path": "foodLevel",
@@ -10189,14 +10241,6 @@ Object.freeze({
10189
10241
  {
10190
10242
  "path": "powerFactor",
10191
10243
  "valueType": "number"
10192
- },
10193
- {
10194
- "path": "unit",
10195
- "valueType": "string"
10196
- },
10197
- {
10198
- "path": "precision",
10199
- "valueType": "number"
10200
10244
  }
10201
10245
  ],
10202
10246
  "presence": [
@@ -10225,20 +10269,10 @@ Object.freeze({
10225
10269
  "valueType": "number"
10226
10270
  }
10227
10271
  ],
10228
- "pressure-sensor": [
10229
- {
10230
- "path": "hpa",
10231
- "valueType": "number"
10232
- },
10233
- {
10234
- "path": "unit",
10235
- "valueType": "string"
10236
- },
10237
- {
10238
- "path": "precision",
10239
- "valueType": "number"
10240
- }
10241
- ],
10272
+ "pressure-sensor": [{
10273
+ "path": "hpa",
10274
+ "valueType": "number"
10275
+ }],
10242
10276
  "privacy-mask": [{
10243
10277
  "path": "enabled",
10244
10278
  "valueType": "boolean"
@@ -10549,20 +10583,10 @@ Object.freeze({
10549
10583
  "path": "tampered",
10550
10584
  "valueType": "boolean"
10551
10585
  }],
10552
- "temperature-sensor": [
10553
- {
10554
- "path": "celsius",
10555
- "valueType": "number"
10556
- },
10557
- {
10558
- "path": "unit",
10559
- "valueType": "string"
10560
- },
10561
- {
10562
- "path": "precision",
10563
- "valueType": "number"
10564
- }
10565
- ],
10586
+ "temperature-sensor": [{
10587
+ "path": "celsius",
10588
+ "valueType": "number"
10589
+ }],
10566
10590
  "update": [
10567
10591
  {
10568
10592
  "path": "currentVersion",
@@ -10673,10 +10697,6 @@ Object.freeze({
10673
10697
  "path": "temperature",
10674
10698
  "valueType": "number"
10675
10699
  },
10676
- {
10677
- "path": "temperatureUnit",
10678
- "valueType": "string"
10679
- },
10680
10700
  {
10681
10701
  "path": "humidity",
10682
10702
  "valueType": "number"
@@ -10685,18 +10705,10 @@ Object.freeze({
10685
10705
  "path": "pressure",
10686
10706
  "valueType": "number"
10687
10707
  },
10688
- {
10689
- "path": "pressureUnit",
10690
- "valueType": "string"
10691
- },
10692
10708
  {
10693
10709
  "path": "windSpeed",
10694
10710
  "valueType": "number"
10695
10711
  },
10696
- {
10697
- "path": "windSpeedUnit",
10698
- "valueType": "string"
10699
- },
10700
10712
  {
10701
10713
  "path": "windBearing",
10702
10714
  "valueType": "number"
@@ -11089,7 +11101,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11089
11101
  return DeviceRole;
11090
11102
  }({});
11091
11103
  //#endregion
11092
- //#region ../types/dist/composition-kEl9mevu.mjs
11104
+ //#region ../types/dist/composition-B85l9j1M.mjs
11093
11105
  /**
11094
11106
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11095
11107
  *
@@ -11844,15 +11856,67 @@ function validateExpressionSource(src) {
11844
11856
  }
11845
11857
  return null;
11846
11858
  }
11847
- /** A composed device's stableId is this prefix plus the block id. */
11848
- var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11849
- /** Longest snippet a `code` source may carry (slice 4). */
11850
- var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11859
+ /**
11860
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11861
+ * id. Split out so helper schemas can name it without a cycle through
11862
+ * `composition.ts` (D659).
11863
+ */
11851
11864
  var CompositionSourceRefSchema = object({
11852
11865
  /** The owning addon; `stableId` is unique only within it. */
11853
11866
  addonId: string().min(1),
11854
11867
  stableId: string().min(1)
11855
11868
  });
11869
+ var GROUP_TEMPLATE_IDS = [
11870
+ "temperature",
11871
+ "humidity",
11872
+ "pressure",
11873
+ "illuminance",
11874
+ "power",
11875
+ "energy",
11876
+ "battery",
11877
+ "numeric",
11878
+ "motion",
11879
+ "contact",
11880
+ "leak",
11881
+ "smoke",
11882
+ "occupancy",
11883
+ "binary"
11884
+ ];
11885
+ var NUMERIC_AGGREGATES = [
11886
+ "mean",
11887
+ "median",
11888
+ "min",
11889
+ "max",
11890
+ "sum"
11891
+ ];
11892
+ var BINARY_AGGREGATES = ["any", "all"];
11893
+ var AGGREGATE_REDUCERS = [
11894
+ "mean",
11895
+ "median",
11896
+ "min",
11897
+ "max",
11898
+ "sum",
11899
+ "any",
11900
+ "all"
11901
+ ];
11902
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11903
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11904
+ /** One member. The field is stored so occupancy can name motion or binary. */
11905
+ var HelperMemberSchema = object({
11906
+ source: CompositionSourceRefSchema,
11907
+ cap: string().min(1),
11908
+ fieldPath: string().min(1)
11909
+ });
11910
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11911
+ kind: literal("group"),
11912
+ templateId: GroupTemplateIdSchema,
11913
+ aggregate: AggregateReducerSchema,
11914
+ members: array(HelperMemberSchema).min(1).max(64)
11915
+ })]);
11916
+ /** A composed device's stableId is this prefix plus the block id. */
11917
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11918
+ /** Longest snippet a `code` source may carry (slice 4). */
11919
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11856
11920
  /** One field of one capability of one source device. */
11857
11921
  var CompositionFieldReadSchema = object({
11858
11922
  source: CompositionSourceRefSchema,
@@ -11896,7 +11960,33 @@ var CompositionCodeSourceSchema = object({
11896
11960
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11897
11961
  CompositionFromSourceSchema,
11898
11962
  CompositionExpressionSourceSchema,
11899
- CompositionCodeSourceSchema
11963
+ CompositionCodeSourceSchema,
11964
+ object({
11965
+ kind: literal("aggregate"),
11966
+ reducer: _enum([
11967
+ "mean",
11968
+ "median",
11969
+ "min",
11970
+ "max",
11971
+ "sum",
11972
+ "any",
11973
+ "all"
11974
+ ]),
11975
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
11976
+ /** `value` is the reading; the others are the member counts. */
11977
+ publish: _enum([
11978
+ "value",
11979
+ "included",
11980
+ "excluded",
11981
+ "total"
11982
+ ]),
11983
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
11984
+ empty: _enum([
11985
+ "null",
11986
+ "omit",
11987
+ "offline"
11988
+ ])
11989
+ })
11900
11990
  ]);
11901
11991
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11902
11992
  kind: literal("forward"),
@@ -11942,7 +12032,13 @@ var CompositionSchema = object({
11942
12032
  kind: literal("existing"),
11943
12033
  device: CompositionSourceRefSchema
11944
12034
  })]),
11945
- features: array(CompositionFeatureSchema).min(1).max(16)
12035
+ features: array(CompositionFeatureSchema).min(1).max(16),
12036
+ /**
12037
+ * Present when the block is a helper template. Absent on a free
12038
+ * composition (the room example, device 4603). The editor reads this,
12039
+ * never the compiled features.
12040
+ */
12041
+ helper: HelperSpecSchema.optional()
11946
12042
  }).superRefine((composition, ctx) => {
11947
12043
  const seen = /* @__PURE__ */ new Set();
11948
12044
  composition.features.forEach((feature, index) => {
@@ -18079,7 +18175,11 @@ var CompositionProblemSchema = object({
18079
18175
  "item-field-reserved",
18080
18176
  "target-missing",
18081
18177
  "target-unreadable",
18082
- "not-replaceable"
18178
+ "not-replaceable",
18179
+ "native-capability-replace",
18180
+ "mixed-quantity",
18181
+ "helper-mismatch",
18182
+ "member-constraint"
18083
18183
  ]),
18084
18184
  path: string(),
18085
18185
  message: string()
@@ -18100,7 +18200,8 @@ var CompositionUnavailableModeSchema = _enum([
18100
18200
  "null",
18101
18201
  "offline",
18102
18202
  "unknown",
18103
- "release"
18203
+ "release",
18204
+ "omit"
18104
18205
  ]);
18105
18206
  /**
18106
18207
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18137,9 +18238,11 @@ var CompositionFieldPlanSchema = object({
18137
18238
  reads: array(CompositionFieldReadSchema)
18138
18239
  });
18139
18240
  /**
18140
- * What a feature does to its target (D663): `new` builds a capability on a new
18141
- * device; on an existing device it `add`s a capability the device lacks, or
18142
- * `replace`s named fields of one it has natively.
18241
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18242
+ * new device; on an existing device it `add`s a capability the device lacks.
18243
+ * Replacing a capability the device already serves is refused
18244
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18245
+ * wire for a claim the hub already holds.
18143
18246
  */
18144
18247
  var CompositionFeatureModeSchema = _enum([
18145
18248
  "new",
@@ -18312,6 +18415,235 @@ Object.freeze({
18312
18415
  result: ["number", "null"]
18313
18416
  }
18314
18417
  });
18418
+ /**
18419
+ * One catalog row per base reading a group helper can build (D944 §3).
18420
+ * Data, not a form default written into the block.
18421
+ */
18422
+ var NOW = {
18423
+ fieldPath: "lastFetchedAt",
18424
+ expr: "now"
18425
+ };
18426
+ var CHANGED = {
18427
+ fieldPath: "lastChangedAt",
18428
+ expr: "now"
18429
+ };
18430
+ function numeric(input) {
18431
+ return {
18432
+ ...input,
18433
+ family: "numeric",
18434
+ deviceType: DeviceType.Sensor,
18435
+ memberDeviceType: DeviceType.Sensor,
18436
+ baseUnit: baseUnitOf(input.quantity),
18437
+ aggregates: NUMERIC_AGGREGATES,
18438
+ accept: [{
18439
+ cap: input.outputCap,
18440
+ fieldPath: input.outputField
18441
+ }]
18442
+ };
18443
+ }
18444
+ function binary(input) {
18445
+ return {
18446
+ ...input,
18447
+ family: "binary",
18448
+ deviceType: DeviceType.Sensor,
18449
+ memberDeviceType: DeviceType.Sensor,
18450
+ quantity: "none",
18451
+ baseUnit: void 0,
18452
+ aggregates: BINARY_AGGREGATES,
18453
+ empty: "offline"
18454
+ };
18455
+ }
18456
+ var GROUP_TEMPLATES = [
18457
+ numeric({
18458
+ id: "temperature",
18459
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18460
+ role: DeviceRole.TemperatureSensor,
18461
+ outputCap: "temperature-sensor",
18462
+ outputField: "celsius",
18463
+ quantity: "temperature",
18464
+ defaultAggregate: "mean",
18465
+ empty: "null",
18466
+ companions: [NOW]
18467
+ }),
18468
+ numeric({
18469
+ id: "humidity",
18470
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18471
+ role: DeviceRole.HumiditySensor,
18472
+ outputCap: "humidity-sensor",
18473
+ outputField: "percent",
18474
+ quantity: "humidity",
18475
+ defaultAggregate: "mean",
18476
+ empty: "null",
18477
+ companions: [NOW]
18478
+ }),
18479
+ numeric({
18480
+ id: "pressure",
18481
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18482
+ role: DeviceRole.PressureSensor,
18483
+ outputCap: "pressure-sensor",
18484
+ outputField: "hpa",
18485
+ quantity: "pressure",
18486
+ defaultAggregate: "mean",
18487
+ empty: "null",
18488
+ companions: [NOW]
18489
+ }),
18490
+ numeric({
18491
+ id: "illuminance",
18492
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18493
+ role: DeviceRole.AmbientLightSensor,
18494
+ outputCap: "ambient-light-sensor",
18495
+ outputField: "lux",
18496
+ quantity: "illuminance",
18497
+ defaultAggregate: "mean",
18498
+ empty: "null",
18499
+ companions: [NOW]
18500
+ }),
18501
+ numeric({
18502
+ id: "power",
18503
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18504
+ role: DeviceRole.PowerSensor,
18505
+ outputCap: "power-meter",
18506
+ outputField: "watts",
18507
+ quantity: "power",
18508
+ defaultAggregate: "sum",
18509
+ empty: "omit",
18510
+ companions: [NOW]
18511
+ }),
18512
+ numeric({
18513
+ id: "energy",
18514
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18515
+ role: DeviceRole.EnergySensor,
18516
+ outputCap: "power-meter",
18517
+ outputField: "kwhTotal",
18518
+ quantity: "energy",
18519
+ defaultAggregate: "sum",
18520
+ empty: "omit",
18521
+ companions: [NOW]
18522
+ }),
18523
+ numeric({
18524
+ id: "battery",
18525
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18526
+ role: DeviceRole.BatterySensor,
18527
+ outputCap: "battery",
18528
+ outputField: "percentage",
18529
+ quantity: "percentage",
18530
+ defaultAggregate: "mean",
18531
+ empty: "null",
18532
+ companions: [
18533
+ {
18534
+ fieldPath: "charging",
18535
+ expr: "\"unknown\""
18536
+ },
18537
+ {
18538
+ fieldPath: "sleeping",
18539
+ expr: "false"
18540
+ },
18541
+ {
18542
+ fieldPath: "lastUpdated",
18543
+ expr: "now"
18544
+ }
18545
+ ]
18546
+ }),
18547
+ numeric({
18548
+ id: "numeric",
18549
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18550
+ role: DeviceRole.NumericSensor,
18551
+ outputCap: "numeric-sensor",
18552
+ outputField: "value",
18553
+ quantity: "device",
18554
+ defaultAggregate: "mean",
18555
+ empty: "null",
18556
+ companions: [NOW]
18557
+ }),
18558
+ binary({
18559
+ id: "motion",
18560
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18561
+ role: DeviceRole.MotionSensor,
18562
+ outputCap: "motion",
18563
+ outputField: "detected",
18564
+ defaultAggregate: "any",
18565
+ accept: [{
18566
+ cap: "motion",
18567
+ fieldPath: "detected"
18568
+ }],
18569
+ companions: [{
18570
+ fieldPath: "lastDetectedAt",
18571
+ expr: "null"
18572
+ }, {
18573
+ fieldPath: "autoClearAfterMs",
18574
+ expr: "null"
18575
+ }]
18576
+ }),
18577
+ binary({
18578
+ id: "contact",
18579
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18580
+ role: DeviceRole.ContactSensor,
18581
+ outputCap: "contact",
18582
+ outputField: "entryOpen",
18583
+ defaultAggregate: "any",
18584
+ accept: [{
18585
+ cap: "contact",
18586
+ fieldPath: "entryOpen"
18587
+ }],
18588
+ companions: [CHANGED]
18589
+ }),
18590
+ binary({
18591
+ id: "leak",
18592
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18593
+ role: DeviceRole.LeakSensor,
18594
+ outputCap: "flood",
18595
+ outputField: "flooded",
18596
+ defaultAggregate: "any",
18597
+ accept: [{
18598
+ cap: "flood",
18599
+ fieldPath: "flooded"
18600
+ }],
18601
+ companions: [CHANGED]
18602
+ }),
18603
+ binary({
18604
+ id: "smoke",
18605
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18606
+ role: DeviceRole.SmokeSensor,
18607
+ outputCap: "smoke",
18608
+ outputField: "detected",
18609
+ defaultAggregate: "any",
18610
+ accept: [{
18611
+ cap: "smoke",
18612
+ fieldPath: "detected"
18613
+ }],
18614
+ companions: [CHANGED]
18615
+ }),
18616
+ binary({
18617
+ id: "occupancy",
18618
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18619
+ role: DeviceRole.BinarySensor,
18620
+ outputCap: "binary",
18621
+ outputField: "on",
18622
+ defaultAggregate: "any",
18623
+ accept: [{
18624
+ cap: "motion",
18625
+ fieldPath: "detected"
18626
+ }, {
18627
+ cap: "binary",
18628
+ fieldPath: "on"
18629
+ }],
18630
+ companions: [CHANGED]
18631
+ }),
18632
+ binary({
18633
+ id: "binary",
18634
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18635
+ role: DeviceRole.BinarySensor,
18636
+ outputCap: "binary",
18637
+ outputField: "on",
18638
+ defaultAggregate: "any",
18639
+ accept: [{
18640
+ cap: "binary",
18641
+ fieldPath: "on"
18642
+ }],
18643
+ companions: [CHANGED]
18644
+ })
18645
+ ];
18646
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18315
18647
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18316
18648
  /**
18317
18649
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20025,12 +20357,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20025
20357
  * providers detect renames by matching `uniqueId` and patch `id`
20026
20358
  * in-place so dispatch keeps working.
20027
20359
  *
20028
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20029
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20030
- * state push and are the single source of truth. The `device_class` tag is
20031
- * an HA-specific input that is consumed at the discovery boundary
20032
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20033
- * stored after that point.
20360
+ * **Display unit and precision are NOT stored here.** They are a read-time
20361
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20362
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20363
+ * neither is repeated on each sample. The `device_class` tag is an
20364
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20365
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20034
20366
  *
20035
20367
  * The `raw` escape hatch carries provider-specific attributes that don't
20036
20368
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20044,6 +20376,14 @@ var SourceInfoSchema = object({
20044
20376
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20045
20377
  * Used to detect rename when `id` changes. */
20046
20378
  uniqueId: string().optional(),
20379
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20380
+ quantity: ReadingQuantitySchema.optional(),
20381
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20382
+ dateTimeShape: _enum([
20383
+ "date",
20384
+ "time",
20385
+ "datetime"
20386
+ ]).optional(),
20047
20387
  /** Provider-specific extras that don't fit the structured slots.
20048
20388
  * Whitelist what goes here per provider — do NOT dump entire HA
20049
20389
  * attribute blobs (would explode the meta JSON column). */
@@ -40956,7 +41296,8 @@ method(object({
40956
41296
  "enum"
40957
41297
  ]),
40958
41298
  enumValues: array(string()).optional(),
40959
- item: boolean().optional()
41299
+ item: boolean().optional(),
41300
+ quantity: ReadingQuantitySchema.optional()
40960
41301
  })).readonly(),
40961
41302
  itemArray: object({
40962
41303
  path: string(),
@@ -44756,8 +45097,8 @@ var MaskPolygonShapeSchema = object({
44756
45097
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44757
45098
  var MaskGridShapeSchema = object({
44758
45099
  kind: literal("grid"),
44759
- gridWidth: number(),
44760
- gridHeight: number(),
45100
+ gridWidth: number().meta(readingMeta("none")),
45101
+ gridHeight: number().meta(readingMeta("none")),
44761
45102
  cells: array(boolean())
44762
45103
  });
44763
45104
  discriminatedUnion("kind", [
@@ -56151,46 +56492,27 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56151
56492
  }), LogStreamEntrySchema, { kind: "subscription" });
56152
56493
  object({
56153
56494
  /** Carbon dioxide concentration in ppm. */
56154
- co2Ppm: number().min(0).optional(),
56495
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56155
56496
  /** Total volatile organic compounds in ppb. */
56156
- vocPpb: number().min(0).optional(),
56497
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56157
56498
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56158
- pm25: number().min(0).optional(),
56499
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56159
56500
  /** Particulate matter ≤ 10 μm in µg/m³. */
56160
- pm10: number().min(0).optional(),
56501
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56161
56502
  /** Composite AQI value (typically 0..500). */
56162
- aqi: number().optional(),
56503
+ aqi: number().optional().meta(readingMeta("index")),
56163
56504
  /** Ms epoch when the slice was last updated. */
56164
- lastFetchedAt: number(),
56165
- /** Live display unit of the single metric this slice carries (e.g. HA
56166
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56167
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56168
- * per slice is unambiguous. */
56169
- unit: string().optional(),
56170
- /** Suggested decimal places for numeric display.
56171
- * Populated live from the upstream source when provided (e.g. HA
56172
- * `attributes.suggested_display_precision`). Falls back to
56173
- * auto-formatting when absent. */
56174
- precision: number().int().min(0).max(10).optional()
56505
+ lastFetchedAt: number()
56175
56506
  });
56176
56507
  DeviceType.Sensor;
56177
56508
  object({
56178
- /** Current illuminance in lux (lx). */
56179
- lux: number().min(0).meta({
56180
- unit: "lx",
56509
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56510
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56511
+ precision: 0,
56181
56512
  label: "Illuminance"
56182
- }),
56513
+ })),
56183
56514
  /** Ms epoch when the slice was last updated. */
56184
- lastFetchedAt: number(),
56185
- /** Live display unit from the upstream source (e.g. HA
56186
- * `attributes.unit_of_measurement`). The UI prefers this over the
56187
- * role's canonical unit. Absent → fall back to the canonical unit. */
56188
- unit: string().optional(),
56189
- /** Suggested decimal places for numeric display.
56190
- * Populated live from the upstream source when provided (e.g. HA
56191
- * `attributes.suggested_display_precision`). Falls back to
56192
- * auto-formatting when absent. */
56193
- precision: number().int().min(0).max(10).optional()
56515
+ lastFetchedAt: number()
56194
56516
  });
56195
56517
  DeviceType.Sensor;
56196
56518
  /**
@@ -56335,10 +56657,7 @@ var BatteryStatusSchema = object({
56335
56657
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56336
56658
  * declines to band a non-finite reading, which is the correct shape.
56337
56659
  */
56338
- percentage: number().min(0).max(100).nullable().meta({
56339
- unit: "%",
56340
- label: "Charge"
56341
- }),
56660
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56342
56661
  /**
56343
56662
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56344
56663
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56365,7 +56684,14 @@ var BatteryStatusSchema = object({
56365
56684
  */
56366
56685
  sleeping: boolean().meta({ label: "Sleeping" }),
56367
56686
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56368
- lastUpdated: number(),
56687
+ lastUpdated: number().meta(readingMeta("none")),
56688
+ /**
56689
+ * Battery voltage in volts, for a cell the firmware reports as voltage
56690
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
56691
+ * that only report charge. A second reading on this cap, not a second
56692
+ * `numeric-sensor` on the same device.
56693
+ */
56694
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56369
56695
  /**
56370
56696
  * Ms epoch of the last time the device PROVED it was reachable — a
56371
56697
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56425,7 +56751,7 @@ object({
56425
56751
  DeviceType.Sensor;
56426
56752
  object({
56427
56753
  /** Current level as 0..100 inclusive. Firmware-reported. */
56428
- percentage: number().min(0).max(100),
56754
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56429
56755
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56430
56756
  lastChangedAt: number()
56431
56757
  });
@@ -56810,21 +57136,21 @@ object({
56810
57136
  /** Single setpoint in Celsius. Used by single-target modes
56811
57137
  * (heat / cool / dry). Null when the device is in a range mode
56812
57138
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
56813
- target: number().nullable(),
57139
+ target: number().nullable().meta(readingMeta("temperature")),
56814
57140
  /** Upper bound of the dual setpoint in Celsius. Populated only
56815
57141
  * in `heat_cool` mode. */
56816
- targetHigh: number().nullable(),
57142
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
56817
57143
  /** Lower bound of the dual setpoint in Celsius. Populated only
56818
57144
  * in `heat_cool` mode. */
56819
- targetLow: number().nullable(),
57145
+ targetLow: number().nullable().meta(readingMeta("temperature")),
56820
57146
  /** Target relative humidity (0..100). Null when the device has no
56821
57147
  * humidity control. */
56822
- targetHumidity: number().min(0).max(100).nullable(),
57148
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56823
57149
  /** Current measured temperature in Celsius. Read-only — pushed by
56824
57150
  * the device's internal sensor. */
56825
- currentTemp: number().nullable(),
57151
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
56826
57152
  /** Current measured relative humidity (0..100). Read-only. */
56827
- currentHumidity: number().min(0).max(100).nullable(),
57153
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
56828
57154
  /** Vertical louver swing on/off. Null when the device has no vertical
56829
57155
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
56830
57156
  swingVertical: boolean().nullable(),
@@ -56832,11 +57158,11 @@ object({
56832
57158
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
56833
57159
  swingHorizontal: boolean().nullable(),
56834
57160
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
56835
- minTemp: number().nullable().optional(),
57161
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
56836
57162
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
56837
- maxTemp: number().nullable().optional(),
57163
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
56838
57164
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
56839
- tempStep: number().positive().nullable().optional(),
57165
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
56840
57166
  /** Ms epoch when the slice was last updated (push or command). */
56841
57167
  lastFetchedAt: number()
56842
57168
  });
@@ -56915,17 +57241,17 @@ DeviceType.Thermostat, DeviceType.Climate, method(object({
56915
57241
  * modes to expose in the picker without a slice fetch.
56916
57242
  */
56917
57243
  var RgbTripletSchema = object({
56918
- r: number().int().min(0).max(255),
56919
- g: number().int().min(0).max(255),
56920
- b: number().int().min(0).max(255)
57244
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57245
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57246
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
56921
57247
  });
56922
57248
  var HsvTripletSchema = object({
56923
57249
  /** Hue in degrees, 0..360. */
56924
- h: number().min(0).max(360),
57250
+ h: number().min(0).max(360).meta(readingMeta("none")),
56925
57251
  /** Saturation as 0..100 inclusive. */
56926
- s: number().min(0).max(100),
57252
+ s: number().min(0).max(100).meta(readingMeta("none")),
56927
57253
  /** Value/brightness as 0..100 inclusive. */
56928
- v: number().min(0).max(100)
57254
+ v: number().min(0).max(100).meta(readingMeta("none"))
56929
57255
  });
56930
57256
  var ColorInputSchema = discriminatedUnion("mode", [
56931
57257
  object({
@@ -56954,7 +57280,7 @@ object({
56954
57280
  /** Populated when `mode === 'hsv'`. */
56955
57281
  hsv: HsvTripletSchema.optional(),
56956
57282
  /** Populated when `mode === 'mired'`. */
56957
- mireds: number().int().optional(),
57283
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
56958
57284
  /** Ms epoch of the last operator-driven change. */
56959
57285
  lastChangedAt: number()
56960
57286
  });
@@ -57078,18 +57404,12 @@ _enum([
57078
57404
  "empty",
57079
57405
  "full"
57080
57406
  ]);
57081
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57082
- * each. */
57083
- var ConsumableRemainingUnitSchema = _enum([
57084
- "days",
57085
- "hours",
57086
- "cycles"
57087
- ]);
57088
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57089
- * days). `value` null = unknown (D393). */
57407
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57408
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57409
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57090
57410
  var ConsumableRemainingSchema = object({
57091
- value: number().min(0).nullable(),
57092
- unit: ConsumableRemainingUnitSchema
57411
+ value: number().min(0).nullable().meta(readingMeta("device")),
57412
+ kind: ConsumableRemainingKindSchema
57093
57413
  });
57094
57414
  var ConsumablesStatusSchema = object({
57095
57415
  items: array(object({
@@ -57098,7 +57418,7 @@ var ConsumablesStatusSchema = object({
57098
57418
  /** Display name. */
57099
57419
  label: string().min(1),
57100
57420
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57101
- level: number().min(0).max(100).nullable(),
57421
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57102
57422
  /** The provider's discrete verdict when known (binary mode). */
57103
57423
  status: ConsumableStatusSchema.nullable(),
57104
57424
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57115,7 +57435,7 @@ var ConsumablesStatusSchema = object({
57115
57435
  })),
57116
57436
  lastChangedAt: number()
57117
57437
  });
57118
- Object.values(DeviceType), [...ConsumableRemainingUnitSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57438
+ Object.values(DeviceType), [...ConsumableRemainingKindSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57119
57439
  Object.values(DeviceType), method(object({
57120
57440
  deviceId: number().int().nonnegative(),
57121
57441
  key: string().min(1)
@@ -57140,39 +57460,17 @@ object({
57140
57460
  /** Current value. Numeric kind → number; select/text/datetime → string.
57141
57461
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
57142
57462
  * or time-only — HA accepts all three forms). */
57143
- value: union([number(), string()]),
57463
+ value: union([number().meta(readingMeta("device")), string()]),
57144
57464
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
57145
57465
  options: array(string()),
57146
57466
  /** Ms epoch when the slice was last updated. */
57147
57467
  lastChangedAt: number(),
57148
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
57149
- * Populated live from HA `attributes.unit_of_measurement` on each push.
57150
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57151
- unit: string().optional(),
57152
- /** Minimum allowed value. Populated live from HA `attributes.min`.
57153
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57154
- min: number().optional(),
57155
- /** Maximum allowed value. Populated live from HA `attributes.max`.
57156
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57157
- max: number().optional(),
57158
- /** Step increment. Populated live from HA `attributes.step`.
57159
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57160
- step: number().optional(),
57161
- /** Suggested decimal places for numeric display. Populated live from HA
57162
- * `attributes.suggested_display_precision`, or derived from `step` when the
57163
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
57164
- * other kinds. Falls back to auto-formatting when absent. */
57165
- precision: number().int().min(0).max(10).optional(),
57166
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
57167
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
57168
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
57169
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
57170
- * `has_date`/`has_time` attributes on each push. */
57171
- format: _enum([
57172
- "date",
57173
- "time",
57174
- "datetime"
57175
- ]).optional()
57468
+ /** Minimum allowed value, in the device quantity's base unit. */
57469
+ min: number().optional().meta(readingMeta("device")),
57470
+ /** Maximum allowed value, in the device quantity's base unit. */
57471
+ max: number().optional().meta(readingMeta("device")),
57472
+ /** Step increment, in the device quantity's base unit. */
57473
+ step: number().optional().meta(readingMeta("device"))
57176
57474
  });
57177
57475
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
57178
57476
  object({
@@ -57210,9 +57508,9 @@ object({
57210
57508
  ]),
57211
57509
  /** 0 = fully closed, 100 = fully open. Null when the device has
57212
57510
  * no intermediate position surface. */
57213
- position: number().min(0).max(100).nullable(),
57511
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57214
57512
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
57215
- tiltPosition: number().min(0).max(100).nullable(),
57513
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57216
57514
  /** Ms epoch when the slice was last updated. */
57217
57515
  lastChangedAt: number()
57218
57516
  });
@@ -57270,9 +57568,9 @@ var NormalizedRangeSchema$1 = object({
57270
57568
  object({
57271
57569
  mode: DayNightModeSchema,
57272
57570
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
57273
- sensitivity: number().optional(),
57571
+ sensitivity: number().optional().meta(readingMeta("none")),
57274
57572
  /** Delay before the IR-cut filter flips, in seconds. */
57275
- switchDelaySec: number().optional(),
57573
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
57276
57574
  lastFetchedAt: number()
57277
57575
  });
57278
57576
  /**
@@ -57427,11 +57725,11 @@ object({
57427
57725
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
57428
57726
  availablePrograms: array(string()),
57429
57727
  /** Seconds remaining on the active cycle, or null when not running / not known. */
57430
- remainingSeconds: number().int().nonnegative().nullable(),
57728
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57431
57729
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
57432
- delayedStartSeconds: number().int().nonnegative().nullable(),
57730
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57433
57731
  /** Cycle completion 0..100, or null when not applicable. */
57434
- progressPercent: number().min(0).max(100).nullable(),
57732
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57435
57733
  /** True when the door is reported open; null when the door state is not known. */
57436
57734
  doorOpen: boolean().nullable(),
57437
57735
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -57455,7 +57753,7 @@ object({
57455
57753
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57456
57754
  machineCareDue: boolean().nullable(),
57457
57755
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57458
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
57756
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
57459
57757
  /** Option states: null when the appliance does not expose the option or has not said. */
57460
57758
  halfLoad: boolean().nullable(),
57461
57759
  hygienePlus: boolean().nullable(),
@@ -57470,7 +57768,7 @@ object({
57470
57768
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
57471
57769
  activeEvents: array(string()),
57472
57770
  /** Numeric error code when the appliance reports one; null otherwise. */
57473
- errorCode: number().nullable(),
57771
+ errorCode: number().nullable().meta(readingMeta("none")),
57474
57772
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
57475
57773
  errorLabel: string().nullable(),
57476
57774
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -57616,24 +57914,14 @@ object({
57616
57914
  /** Ms epoch of the last press. null = never observed since this provider started. */
57617
57915
  lastPressedAt: number().nullable(),
57618
57916
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
57619
- pressCountSinceStart: number()
57917
+ pressCountSinceStart: number().meta(readingMeta("count"))
57620
57918
  });
57621
57919
  object({
57622
57920
  deviceId: number(),
57623
57921
  timestamp: number()
57624
57922
  });
57625
57923
  DeviceType.Button, DeviceType.Camera;
57626
- /**
57627
- * Enum-state sensor — a string value picked from a finite option set.
57628
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
57629
- * states, weather conditions, contact-source channels, mode strings).
57630
- * The option set is stable across observations; it is persisted in the
57631
- * device config blob (via the control slice at adoption) and is NOT
57632
- * stored in `sourceInfo`.
57633
- */
57634
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
57635
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
57636
- var EnumSensorDateTimeFormatSchema = _enum([
57924
+ _enum([
57637
57925
  "date",
57638
57926
  "time",
57639
57927
  "datetime"
@@ -57641,12 +57929,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
57641
57929
  object({
57642
57930
  value: string(),
57643
57931
  /**
57644
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
57645
- * as a locale date/time/datetime; absent for plain enum sensors. HA
57646
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
57647
- * a time-only sensor → `'time'`.
57932
+ * Set when `value` is a timestamp rather than an enum state, so history
57933
+ * does not learn ISO strings as states. The display shape (date / time /
57934
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
57648
57935
  */
57649
- format: EnumSensorDateTimeFormatSchema.optional(),
57936
+ timestamp: boolean().optional(),
57650
57937
  /** Ms epoch when the slice was last updated. */
57651
57938
  lastFetchedAt: number()
57652
57939
  });
@@ -57665,13 +57952,13 @@ var EventFireSchema = object({
57665
57952
  deviceId: number(),
57666
57953
  eventType: string(),
57667
57954
  data: record(string(), unknown()).nullable(),
57668
- timestamp: number(),
57669
- seq: number()
57955
+ timestamp: number().meta(readingMeta("none")),
57956
+ seq: number().meta(readingMeta("count"))
57670
57957
  });
57671
57958
  object({
57672
57959
  eventTypes: array(string()),
57673
57960
  lastEvent: EventFireSchema.nullable(),
57674
- eventCountSinceStart: number()
57961
+ eventCountSinceStart: number().meta(readingMeta("count"))
57675
57962
  });
57676
57963
  DeviceType.EventEmitter;
57677
57964
  var IdentitySchema = object({
@@ -58060,10 +58347,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
58060
58347
  object({
58061
58348
  /** Active speed as 0..100 inclusive. Null when the device has no
58062
58349
  * speed surface (single-speed fan). */
58063
- percentage: number().min(0).max(100).nullable(),
58350
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58064
58351
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
58065
58352
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
58066
- percentageStep: number().positive().optional(),
58353
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
58067
58354
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
58068
58355
  * — empty string when no preset is active or supported. */
58069
58356
  preset: string(),
@@ -58145,7 +58432,7 @@ var FeatureProbeStatusSchema = object({
58145
58432
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
58146
58433
  model: string().nullable(),
58147
58434
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
58148
- channelCount: number().nullable(),
58435
+ channelCount: number().nullable().meta(readingMeta("count")),
58149
58436
  /**
58150
58437
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
58151
58438
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -58177,9 +58464,27 @@ object({
58177
58464
  lastChangedAt: number()
58178
58465
  });
58179
58466
  DeviceType.Sensor;
58467
+ object({
58468
+ /** Members whose current value entered the aggregate. */
58469
+ included: number().int().nonnegative().meta(readingMeta("count", {
58470
+ precision: 0,
58471
+ label: "Reporting"
58472
+ })),
58473
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
58474
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
58475
+ precision: 0,
58476
+ label: "Excluded"
58477
+ })),
58478
+ /** `included` + `excluded`. */
58479
+ total: number().int().nonnegative().meta(readingMeta("count", {
58480
+ precision: 0,
58481
+ label: "Members"
58482
+ }))
58483
+ });
58484
+ DeviceType.Sensor;
58180
58485
  object({
58181
58486
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
58182
- position: number().min(0).max(100).nullable(),
58487
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58183
58488
  /** Ms epoch when the slice was last updated (push or poll). */
58184
58489
  lastFetchedAt: number()
58185
58490
  });
@@ -58188,9 +58493,9 @@ object({
58188
58493
  /** Whether the humidifier is currently on. */
58189
58494
  on: boolean(),
58190
58495
  /** Current measured relative humidity (0..100). Null when not reported. */
58191
- currentHumidity: number().min(0).max(100).nullable(),
58496
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58192
58497
  /** Target relative humidity (0..100). Null when no setpoint surface. */
58193
- targetHumidity: number().min(0).max(100).nullable(),
58498
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58194
58499
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
58195
58500
  * device has no mode surface. */
58196
58501
  mode: string().nullable(),
@@ -58200,9 +58505,9 @@ object({
58200
58505
  * `idle` / `off`). Null when the device doesn't report it. */
58201
58506
  action: string().nullable(),
58202
58507
  /** HA `min_humidity` attribute. Null → UI uses 0. */
58203
- minHumidity: number().nullable(),
58508
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
58204
58509
  /** HA `max_humidity` attribute. Null → UI uses 100. */
58205
- maxHumidity: number().nullable(),
58510
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
58206
58511
  /** Ms epoch when the slice was last updated. */
58207
58512
  lastChangedAt: number()
58208
58513
  });
@@ -58226,22 +58531,13 @@ DeviceType.Humidifier, method(object({
58226
58531
  auth: "protected"
58227
58532
  });
58228
58533
  object({
58229
- /** Current relative humidity, 0..100. */
58230
- percent: number().min(0).max(100).meta({
58231
- unit: "%",
58534
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
58535
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
58536
+ precision: 0,
58232
58537
  label: "Humidity"
58233
- }),
58538
+ })),
58234
58539
  /** Ms epoch when the slice was last updated. */
58235
- lastFetchedAt: number(),
58236
- /** Live display unit from the upstream source (e.g. HA
58237
- * `attributes.unit_of_measurement`). The UI prefers this over the
58238
- * role's canonical unit. Absent → fall back to the canonical unit. */
58239
- unit: string().optional(),
58240
- /** Suggested decimal places for numeric display.
58241
- * Populated live from the upstream source when provided (e.g. HA
58242
- * `attributes.suggested_display_precision`). Falls back to
58243
- * auto-formatting when absent. */
58244
- precision: number().int().min(0).max(10).optional()
58540
+ lastFetchedAt: number()
58245
58541
  });
58246
58542
  DeviceType.Sensor, DeviceType.Thermostat;
58247
58543
  object({
@@ -58249,7 +58545,7 @@ object({
58249
58545
  * entity exposes no `entity_picture` (yet). */
58250
58546
  url: string().nullable(),
58251
58547
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
58252
- lastUpdated: number().nullable()
58548
+ lastUpdated: number().nullable().meta(readingMeta("none"))
58253
58549
  });
58254
58550
  DeviceType.Image;
58255
58551
  /**
@@ -58305,19 +58601,19 @@ var NormalizedRangeSchema = object({
58305
58601
  });
58306
58602
  object({
58307
58603
  /** Normalized 0–100. */
58308
- brightness: number().optional(),
58604
+ brightness: number().optional().meta(readingMeta("none")),
58309
58605
  /** Normalized 0–100. */
58310
- contrast: number().optional(),
58606
+ contrast: number().optional().meta(readingMeta("none")),
58311
58607
  /** Normalized 0–100. */
58312
- saturation: number().optional(),
58608
+ saturation: number().optional().meta(readingMeta("none")),
58313
58609
  /** Normalized 0–100. */
58314
- sharpness: number().optional(),
58610
+ sharpness: number().optional().meta(readingMeta("none")),
58315
58611
  mirror: boolean().optional(),
58316
58612
  flip: boolean().optional(),
58317
58613
  rotate: ImageRotateSchema.optional(),
58318
58614
  whiteBalance: WhiteBalanceModeSchema.optional(),
58319
58615
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
58320
- warmth: number().optional(),
58616
+ warmth: number().optional().meta(readingMeta("none")),
58321
58617
  exposureMode: ExposureModeSchema.optional(),
58322
58618
  backlightMode: BacklightModeSchema.optional(),
58323
58619
  lastFetchedAt: number()
@@ -58562,10 +58858,10 @@ var IntercomAbilitySchema = object({
58562
58858
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
58563
58859
  codecs: array(string()),
58564
58860
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
58565
- sampleRate: number(),
58861
+ sampleRate: number().meta(readingMeta("none")),
58566
58862
  duplex: _enum(["half", "full"]),
58567
58863
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
58568
- maxBacklogMs: number()
58864
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
58569
58865
  });
58570
58866
  var IntercomStatusSchema = object({
58571
58867
  /** True while a talk session is open. */
@@ -58676,13 +58972,13 @@ object({
58676
58972
  activity: LawnMowerActivitySchema,
58677
58973
  /** 0..100 battery percentage. Null when the device has no battery
58678
58974
  * reading. */
58679
- batteryLevel: number().min(0).max(100).nullable(),
58975
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58680
58976
  /** 0..100 mowing-completion percentage of the current task, or null when no
58681
58977
  * task is active / progress is unavailable. */
58682
- progressPercent: number().min(0).max(100).nullable(),
58978
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58683
58979
  /** Current device/event code (dynamic — mostly status, sometimes an error),
58684
58980
  * or null when unknown. */
58685
- currentCode: number().nullable(),
58981
+ currentCode: number().nullable().meta(readingMeta("none")),
58686
58982
  /** Human label for {@link currentCode}, or null when undecodable. */
58687
58983
  currentCodeLabel: string().nullable(),
58688
58984
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -59144,7 +59440,7 @@ object({
59144
59440
  state: MediaPlayerStateSchema,
59145
59441
  /** Volume as 0..100 inclusive. Null when the device has no volume
59146
59442
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
59147
- volumeLevel: number().min(0).max(100).nullable(),
59443
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59148
59444
  /** Mute toggle distinct from volume=0. Null when no mute surface.
59149
59445
  * Pair with `DeviceFeature.MediaPlayerMute`. */
59150
59446
  isMuted: boolean().nullable(),
@@ -59157,10 +59453,10 @@ object({
59157
59453
  currentMedia: MediaInfoSchema.nullable(),
59158
59454
  /** Current playback position in ms. Null when not seekable or
59159
59455
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
59160
- positionMs: number().int().nonnegative().nullable(),
59456
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59161
59457
  /** Total duration of the current media in ms. Null when unknown
59162
59458
  * (live stream) or nothing is playing. */
59163
- durationMs: number().int().nonnegative().nullable(),
59459
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59164
59460
  /** Shuffle toggle. Null when no shuffle surface.
59165
59461
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
59166
59462
  shuffle: boolean().nullable(),
@@ -59460,7 +59756,7 @@ object({
59460
59756
  * it, every provider seeded it at boot, and every session left it
59461
59757
  * `detected: true` after its detach.
59462
59758
  */
59463
- autoClearAfterMs: number().nullable()
59759
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
59464
59760
  });
59465
59761
  /**
59466
59762
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -59510,7 +59806,7 @@ var MotionZoneRegionSchema = object({
59510
59806
  });
59511
59807
  object({
59512
59808
  enabled: boolean(),
59513
- sensitivity: number(),
59809
+ sensitivity: number().meta(readingMeta("none")),
59514
59810
  /** Grid region(s). Today exactly one `grid` shape. */
59515
59811
  regions: array(MotionZoneRegionSchema),
59516
59812
  lastFetchedAt: number()
@@ -59814,7 +60110,7 @@ var NavigationStatusSchema = object({
59814
60110
  following: boolean(),
59815
60111
  flash: boolean(),
59816
60112
  lightMode: NavigationLightModeSchema,
59817
- lightLevel: number().min(40).max(100),
60113
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
59818
60114
  /** Ms epoch when the slice was last updated. */
59819
60115
  lastChangedAt: number()
59820
60116
  });
@@ -59860,13 +60156,13 @@ var NetworkLinkStatusSchema = object({
59860
60156
  * one whose reading has not landed must not be drawn at 0 %. Consumers
59861
60157
  * SKIP a null rather than coerce it.
59862
60158
  */
59863
- signalPercent: number().min(0).max(100).nullable(),
60159
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59864
60160
  /** Raw received signal strength in dBm, when the firmware reports one. */
59865
- rssiDbm: number().optional(),
60161
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
59866
60162
  /** Network name of a wireless link, when the firmware reports it. */
59867
60163
  ssid: string().optional(),
59868
60164
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
59869
- lastUpdated: number()
60165
+ lastUpdated: number().meta(readingMeta("none"))
59870
60166
  });
59871
60167
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
59872
60168
  deviceId: number(),
@@ -60185,7 +60481,7 @@ object({
60185
60481
  lastError: string().nullable(),
60186
60482
  /** Number of deliveries currently buffered server-side (rate-limited
60187
60483
  * notifiers may queue). 0 when the provider sends synchronously. */
60188
- queueDepth: number().int().nonnegative(),
60484
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
60189
60485
  /** Per-feature capability matrix — authoritative source for the
60190
60486
  * compose-form field gating. */
60191
60487
  supports: NotifierSupportsSchema
@@ -60235,15 +60531,8 @@ DeviceType.Notifier, method(NotifierSendInputSchema, NotifierSendResultSchema, {
60235
60531
  acceptedAt: number()
60236
60532
  });
60237
60533
  object({
60238
- value: number(),
60239
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
60240
- * Populated live from the upstream source on each state push.
60241
- * Absent for unitless sensors. */
60242
- unit: string().optional(),
60243
- /** Suggested decimal places for numeric display.
60244
- * Populated live from the upstream source when provided.
60245
- * Falls back to auto-formatting when absent. */
60246
- precision: number().int().min(0).max(10).optional(),
60534
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
60535
+ value: number().nullable().meta(readingMeta("device")),
60247
60536
  /** Ms epoch when the slice was last updated. */
60248
60537
  lastFetchedAt: number()
60249
60538
  });
@@ -60533,23 +60822,26 @@ object({
60533
60822
  /** Food currently in the bowl (grams). Null when the device has not
60534
60823
  * reported a reading yet. On dual-hopper models this is the combined
60535
60824
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
60536
- foodLevel: number().nullable(),
60825
+ foodLevel: number().nullable().meta(readingMeta("none")),
60537
60826
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
60538
60827
  * single-hopper models. */
60539
- food1: number().nullable(),
60828
+ food1: number().nullable().meta(readingMeta("none")),
60540
60829
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
60541
60830
  * single-hopper models. */
60542
- food2: number().nullable(),
60831
+ food2: number().nullable().meta(readingMeta("none")),
60543
60832
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
60544
60833
  * (`device_class: problem`, on = low). True when the bowl is empty /
60545
60834
  * below the feeder's low threshold. */
60546
60835
  lowFood: boolean(),
60547
60836
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
60548
60837
  * device has no battery reading. */
60549
- batteryPower: number().min(0).max(100).nullable(),
60550
- /** Days of desiccant life remaining. Null when the model has no
60551
- * desiccant sensor. */
60552
- desiccantLeftDays: number().nullable(),
60838
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60839
+ /**
60840
+ * Desiccant life remaining, stored in seconds (duration base, D944).
60841
+ * Providers convert from the days the feeder reports. Null when the
60842
+ * model has no desiccant sensor.
60843
+ */
60844
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
60553
60845
  /** True while a feed is in progress. */
60554
60846
  feeding: boolean(),
60555
60847
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -60560,7 +60852,7 @@ object({
60560
60852
  * with `errorCode` for consumers that want the raw integer. */
60561
60853
  error: string().nullable(),
60562
60854
  /** Raw device error code (0 / null = no error). */
60563
- errorCode: number().nullable(),
60855
+ errorCode: number().nullable().meta(readingMeta("none")),
60564
60856
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
60565
60857
  isDualHopper: boolean(),
60566
60858
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -60570,7 +60862,7 @@ object({
60570
60862
  /** Play a chime when dispensing. */
60571
60863
  feedSound: boolean(),
60572
60864
  /** Speaker / prompt volume level (device-scaled integer). */
60573
- volume: number(),
60865
+ volume: number().meta(readingMeta("none")),
60574
60866
  /** Ms epoch when the slice was last refreshed from the cloud. */
60575
60867
  lastFetchedAt: number()
60576
60868
  });
@@ -61007,36 +61299,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
61007
61299
  });
61008
61300
  object({
61009
61301
  /** Instantaneous power draw in watts. */
61010
- watts: number().optional().meta({
61011
- unit: "W",
61012
- label: "Power"
61013
- }),
61302
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
61014
61303
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
61015
- kwhTotal: number().optional().meta({
61016
- unit: "kWh",
61017
- label: "Energy"
61018
- }),
61304
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
61019
61305
  /** Voltage in volts. */
61020
- volts: number().optional(),
61306
+ volts: number().optional().meta(readingMeta("voltage")),
61021
61307
  /** Current in amperes. */
61022
- amps: number().optional(),
61023
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
61024
- va: number().optional(),
61308
+ amps: number().optional().meta(readingMeta("current")),
61309
+ /** Apparent power in volt-amperes. */
61310
+ va: number().optional().meta(readingMeta("apparent-power")),
61025
61311
  /** Power factor (0..1) if reported. */
61026
- powerFactor: number().min(0).max(1).optional(),
61312
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
61027
61313
  /** Ms epoch when the slice was last updated. */
61028
- lastFetchedAt: number(),
61029
- /** Live display unit of the single metric this slice carries (e.g. HA
61030
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
61031
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
61032
- * unit per slice is unambiguous. The UI prefers this over the role's
61033
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
61034
- unit: string().optional(),
61035
- /** Suggested decimal places for numeric display.
61036
- * Populated live from the upstream source when provided (e.g. HA
61037
- * `attributes.suggested_display_precision`). Falls back to
61038
- * auto-formatting when absent. */
61039
- precision: number().int().min(0).max(10).optional()
61314
+ lastFetchedAt: number()
61040
61315
  });
61041
61316
  DeviceType.Sensor;
61042
61317
  /**
@@ -61057,11 +61332,11 @@ DeviceType.Sensor;
61057
61332
  */
61058
61333
  var GpsLocationSchema = object({
61059
61334
  /** Latitude in decimal degrees, -90..90. */
61060
- latitude: number().min(-90).max(90),
61335
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
61061
61336
  /** Longitude in decimal degrees, -180..180. */
61062
- longitude: number().min(-180).max(180),
61337
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
61063
61338
  /** Reported accuracy in meters (lower = better). */
61064
- accuracyMeters: number().nonnegative()
61339
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
61065
61340
  });
61066
61341
  object({
61067
61342
  /** `home` / `not_home` / any user-defined zone name. */
@@ -61073,25 +61348,16 @@ object({
61073
61348
  gps: GpsLocationSchema.nullable(),
61074
61349
  /** Optional battery level of the tracking device (0..100). Null
61075
61350
  * when not reported. */
61076
- batteryPercent: number().min(0).max(100).nullable(),
61351
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61077
61352
  /** Ms epoch when the slice was last updated. */
61078
61353
  lastChangedAt: number()
61079
61354
  });
61080
61355
  DeviceType.Presence;
61081
61356
  object({
61082
- /** Current pressure in hPa. */
61083
- hpa: number(),
61357
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
61358
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
61084
61359
  /** Ms epoch when the slice was last updated. */
61085
- lastFetchedAt: number(),
61086
- /** Live display unit from the upstream source (e.g. HA
61087
- * `attributes.unit_of_measurement`). The UI prefers this over the
61088
- * role's canonical unit. Absent → fall back to the canonical unit. */
61089
- unit: string().optional(),
61090
- /** Suggested decimal places for numeric display.
61091
- * Populated live from the upstream source when provided (e.g. HA
61092
- * `attributes.suggested_display_precision`). Falls back to
61093
- * auto-formatting when absent. */
61094
- precision: number().int().min(0).max(10).optional()
61360
+ lastFetchedAt: number()
61095
61361
  });
61096
61362
  DeviceType.Sensor;
61097
61363
  /**
@@ -61424,8 +61690,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
61424
61690
  }), _void(), { kind: "mutation" });
61425
61691
  var PtzAutotrackSettingsSchema = object({
61426
61692
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
61427
- stopDelaySeconds: number().int().min(0).max(300),
61428
- disappearDelaySeconds: number().int().min(0).max(300)
61693
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
61694
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
61429
61695
  });
61430
61696
  /**
61431
61697
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -61482,7 +61748,7 @@ object({
61482
61748
  /** null = unknown — never coerced to false (D393). */
61483
61749
  running: boolean().nullable(),
61484
61750
  /** Epoch ms the current boolean began; null while `running` is null. */
61485
- since: number().nullable(),
61751
+ since: number().nullable().meta(readingMeta("none")),
61486
61752
  source: RunningStateSourceSchema.meta({ label: "Source" }),
61487
61753
  phase: string().nullable(),
61488
61754
  /** null until a reading has been published (D393). */
@@ -62680,9 +62946,9 @@ var RecordTriggerSchema = _enum([
62680
62946
  */
62681
62947
  var RecordWindowSchema = object({
62682
62948
  trigger: RecordTriggerSchema,
62683
- day: number().int().min(0).max(6),
62684
- startMinute: number().int().min(0).max(1439),
62685
- endMinute: number().int().min(1).max(1440)
62949
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
62950
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
62951
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
62686
62952
  });
62687
62953
  /** Status of one physical volume, as the camera itself describes it. */
62688
62954
  var OnboardStorageVolumeSchema = object({
@@ -62751,10 +63017,10 @@ object({
62751
63017
  /** Master "record to the card at all" switch. */
62752
63018
  enabled: boolean().nullable(),
62753
63019
  overwriteWhenFull: boolean().nullable(),
62754
- preRecordSec: number().nullable(),
62755
- postRecordSec: number().nullable(),
63020
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
63021
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
62756
63022
  /** Length of one recorded file, in minutes. */
62757
- segmentMinutes: number().nullable(),
63023
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
62758
63024
  /** The primary track's weekly windows, flattened. */
62759
63025
  windows: array(RecordWindowSchema),
62760
63026
  /**
@@ -62770,7 +63036,7 @@ object({
62770
63036
  * provider reporting a non-zero count also reports the schedule as not
62771
63037
  * writable.
62772
63038
  */
62773
- unreadableWindows: number(),
63039
+ unreadableWindows: number().meta(readingMeta("count")),
62774
63040
  /**
62775
63041
  * The camera is scheduled to record and has NO usable storage.
62776
63042
  *
@@ -63407,7 +63673,7 @@ var CamStreamDescriptorSchema = object({
63407
63673
  url: string().optional(),
63408
63674
  codec: string().optional(),
63409
63675
  resolution: CamStreamResolutionSchema.optional(),
63410
- fps: number().positive().optional(),
63676
+ fps: number().positive().optional().meta(readingMeta("rate")),
63411
63677
  label: string().optional(),
63412
63678
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
63413
63679
  deviceFeatures: array(string()).optional(),
@@ -63437,18 +63703,18 @@ var StreamProfileSchema = _enum([
63437
63703
  ]);
63438
63704
  /** The current encoding config of one stream profile. */
63439
63705
  var StreamProfileConfigSchema = object({
63440
- width: number(),
63441
- height: number(),
63706
+ width: number().meta(readingMeta("none")),
63707
+ height: number().meta(readingMeta("none")),
63442
63708
  codec: _enum(["h264", "h265"]),
63443
- framerate: number(),
63444
- bitrate: number(),
63709
+ framerate: number().meta(readingMeta("rate")),
63710
+ bitrate: number().meta(readingMeta("bitrate")),
63445
63711
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
63446
63712
  encoderProfile: _enum([
63447
63713
  "high",
63448
63714
  "main",
63449
63715
  "baseline"
63450
63716
  ]).optional(),
63451
- gop: number().optional(),
63717
+ gop: number().optional().meta(readingMeta("none")),
63452
63718
  /**
63453
63719
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
63454
63720
  *
@@ -64091,23 +64357,13 @@ object({
64091
64357
  });
64092
64358
  DeviceType.Sensor;
64093
64359
  object({
64094
- /** Current temperature in Celsius. */
64095
- celsius: number().meta({
64096
- unit: "°C",
64360
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
64361
+ celsius: number().nullable().meta(readingMeta("temperature", {
64362
+ precision: 1,
64097
64363
  label: "Temperature"
64098
- }),
64364
+ })),
64099
64365
  /** Ms epoch when the slice was last updated (push or poll). */
64100
- lastFetchedAt: number(),
64101
- /** Live display unit from the upstream source (e.g. HA
64102
- * `attributes.unit_of_measurement`). The UI prefers this over the
64103
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
64104
- * Absent → the UI falls back to the role's canonical unit. */
64105
- unit: string().optional(),
64106
- /** Suggested decimal places for numeric display.
64107
- * Populated live from the upstream source when provided (e.g. HA
64108
- * `attributes.suggested_display_precision`). Falls back to
64109
- * auto-formatting when absent. */
64110
- precision: number().int().min(0).max(10).optional()
64366
+ lastFetchedAt: number()
64111
64367
  });
64112
64368
  DeviceType.Sensor, DeviceType.Thermostat;
64113
64369
  /**
@@ -64359,16 +64615,16 @@ object({
64359
64615
  ]),
64360
64616
  /** 0..100 battery percentage. Null when the device has no battery
64361
64617
  * reading. */
64362
- batteryLevel: number().min(0).max(100).nullable(),
64618
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64363
64619
  /** Current fan-speed token (provider-verbatim). Null when unknown or
64364
64620
  * the vacuum has no speed control. */
64365
64621
  fanSpeed: string().nullable(),
64366
64622
  /** Speed tokens the hardware accepts — drives the UI selector. */
64367
64623
  availableFanSpeeds: array(string()),
64368
64624
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64369
- progressPercent: number().min(0).max(100).nullable(),
64625
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64370
64626
  /** Current error code (0 / null = no error). */
64371
- errorCode: number().nullable(),
64627
+ errorCode: number().nullable().meta(readingMeta("none")),
64372
64628
  /** Human label for {@link errorCode}, or null when none / undecodable. */
64373
64629
  errorLabel: string().nullable(),
64374
64630
  /** Ms epoch when the slice was last updated. */
@@ -64407,7 +64663,7 @@ object({
64407
64663
  ]),
64408
64664
  /** 0 = fully closed, 100 = fully open. Null when the device has no
64409
64665
  * intermediate position surface. */
64410
- position: number().min(0).max(100).nullable(),
64666
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64411
64667
  /** Ms epoch when the slice was last updated. */
64412
64668
  lastChangedAt: number()
64413
64669
  });
@@ -64435,9 +64691,9 @@ object({
64435
64691
  DeviceType.Sensor;
64436
64692
  object({
64437
64693
  /** Current measured temperature. Null when not reported. */
64438
- currentTemp: number().nullable(),
64694
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
64439
64695
  /** Target temperature setpoint. Null when no setpoint surface. */
64440
- targetTemp: number().nullable(),
64696
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
64441
64697
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
64442
64698
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
64443
64699
  * device reports an unknown state. */
@@ -64448,9 +64704,9 @@ object({
64448
64704
  * has no away surface. */
64449
64705
  away: boolean().nullable(),
64450
64706
  /** HA `min_temp` attribute. Null when not reported. */
64451
- minTemp: number().nullable(),
64707
+ minTemp: number().nullable().meta(readingMeta("temperature")),
64452
64708
  /** HA `max_temp` attribute. Null when not reported. */
64453
- maxTemp: number().nullable(),
64709
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
64454
64710
  /** Ms epoch when the slice was last updated. */
64455
64711
  lastChangedAt: number()
64456
64712
  });
@@ -64474,25 +64730,18 @@ DeviceType.WaterHeater, method(object({
64474
64730
  auth: "protected"
64475
64731
  });
64476
64732
  object({
64477
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
64478
- * when no condition has been reported yet. */
64733
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
64479
64734
  condition: string().nullable(),
64480
- /** Current temperature in the reported unit. Null when not provided. */
64481
- temperature: number().nullable(),
64482
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
64483
- temperatureUnit: string().nullable(),
64735
+ /** Current temperature in Celsius. Null when not provided or refused. */
64736
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
64484
64737
  /** Relative humidity (0..100). Null when not provided. */
64485
- humidity: number().min(0).max(100).nullable(),
64486
- /** Barometric pressure in the reported unit. Null when not provided. */
64487
- pressure: number().nullable(),
64488
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
64489
- pressureUnit: string().nullable(),
64490
- /** Wind speed in the reported unit. Null when not provided. */
64491
- windSpeed: number().nullable(),
64492
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
64493
- windSpeedUnit: string().nullable(),
64738
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
64739
+ /** Barometric pressure in hPa. Null when not provided or refused. */
64740
+ pressure: number().nullable().meta(readingMeta("pressure")),
64741
+ /** Wind speed in metres per second. Null when not provided or refused. */
64742
+ windSpeed: number().nullable().meta(readingMeta("speed")),
64494
64743
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
64495
- windBearing: number().nullable(),
64744
+ windBearing: number().nullable().meta(readingMeta("angle")),
64496
64745
  /** Ms epoch when the slice was last updated. */
64497
64746
  lastFetchedAt: number()
64498
64747
  });
@@ -68614,6 +68863,13 @@ Object.freeze({
68614
68863
  access: "view",
68615
68864
  role: "viewer"
68616
68865
  },
68866
+ "groupStatus.getStatus": {
68867
+ capName: "group-status",
68868
+ capScope: "device",
68869
+ addonId: null,
68870
+ access: "view",
68871
+ role: "viewer"
68872
+ },
68617
68873
  "heatingValve.getStatus": {
68618
68874
  capName: "heating-valve",
68619
68875
  capScope: "device",
@@ -78436,6 +78692,33 @@ var SENSOR_CAP_VOCABULARY = {
78436
78692
  icon: "flame"
78437
78693
  }]
78438
78694
  },
78695
+ "group-status": {
78696
+ scope: "device",
78697
+ noun: t("Group", "Gruppo"),
78698
+ state: [
78699
+ {
78700
+ kind: "numeric",
78701
+ field: "included",
78702
+ quantity: t("the members reporting", "i membri che rispondono"),
78703
+ label: t("Reporting", "Rispondono"),
78704
+ icon: "users"
78705
+ },
78706
+ {
78707
+ kind: "numeric",
78708
+ field: "excluded",
78709
+ quantity: t("the members left out", "i membri esclusi"),
78710
+ label: t("Excluded", "Esclusi"),
78711
+ icon: "users"
78712
+ },
78713
+ {
78714
+ kind: "numeric",
78715
+ field: "total",
78716
+ quantity: t("the member count", "il numero di membri"),
78717
+ label: t("Members", "Membri"),
78718
+ icon: "sigma"
78719
+ }
78720
+ ]
78721
+ },
78439
78722
  "humidity-sensor": {
78440
78723
  scope: "device",
78441
78724
  noun: t("Humidity", "Umidità"),
@@ -78469,7 +78752,6 @@ var SENSOR_CAP_VOCABULARY = {
78469
78752
  field: "value",
78470
78753
  quantity: t("the reading", "il valore"),
78471
78754
  label: t("Reading", "Valore"),
78472
- unitField: "unit",
78473
78755
  icon: "gauge"
78474
78756
  }]
78475
78757
  },
@@ -78605,7 +78887,7 @@ var SENSOR_CAP_VOCABULARY = {
78605
78887
  field: "temperature",
78606
78888
  quantity: t("the outdoor temperature", "la temperatura esterna"),
78607
78889
  label: t("Temperature", "Temperatura"),
78608
- unitField: "temperatureUnit",
78890
+ unit: "°C",
78609
78891
  icon: "thermometer"
78610
78892
  },
78611
78893
  {
@@ -78621,7 +78903,7 @@ var SENSOR_CAP_VOCABULARY = {
78621
78903
  field: "windSpeed",
78622
78904
  quantity: t("the wind speed", "la velocità del vento"),
78623
78905
  label: t("Wind", "Vento"),
78624
- unitField: "windSpeedUnit",
78906
+ unit: "m/s",
78625
78907
  icon: "wind"
78626
78908
  },
78627
78909
  {
@@ -78629,7 +78911,7 @@ var SENSOR_CAP_VOCABULARY = {
78629
78911
  field: "pressure",
78630
78912
  quantity: t("the air pressure", "la pressione atmosferica"),
78631
78913
  label: t("Pressure", "Pressione"),
78632
- unitField: "pressureUnit",
78914
+ unit: "hPa",
78633
78915
  icon: "gauge"
78634
78916
  }
78635
78917
  ]
@@ -79902,6 +80184,11 @@ Object.freeze({
79902
80184
  form: "single",
79903
80185
  optional: false
79904
80186
  }],
80187
+ "groupStatus.getStatus": [{
80188
+ name: "deviceId",
80189
+ form: "single",
80190
+ optional: false
80191
+ }],
79905
80192
  "heatingValve.getStatus": [{
79906
80193
  name: "deviceId",
79907
80194
  form: "single",