@camstack/addon-model-studio 1.1.296 → 1.1.298

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 (18) hide show
  1. package/dist/{MotionZonesSettings-DOhuE4t6.mjs → MotionZonesSettings-B9K4WWi1.mjs} +21 -21
  2. package/dist/{PrivacyMaskSettings-BtQIysUm.mjs → PrivacyMaskSettings-onRhNbik.mjs} +4 -4
  3. package/dist/{SceneMonitorEditor-B4cEP3UH.mjs → SceneMonitorEditor-DKUa9q8G.mjs} +71 -71
  4. package/dist/_stub.js +4255 -4182
  5. package/dist/{_virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-DoLsYz9K.mjs → _virtual_mf-localSharedImportMap___mfe_internal__addon_model_studio_page-DeNyK8AH.mjs} +3 -3
  6. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-Bkw5bZBe.mjs +26 -0
  7. package/dist/{hostInit-BfsN6Dh3.mjs → hostInit-ChdJLKG2.mjs} +2 -2
  8. package/dist/model-studio.addon.js +717 -430
  9. package/dist/model-studio.addon.mjs +717 -430
  10. package/dist/{player-overlays-BVHDRE8f.mjs → player-overlays-DAdFiwTN.mjs} +1 -1
  11. package/dist/remoteEntry.js +1 -1
  12. package/dist/{responsive-CI-NtcQi.mjs → responsive-D_nfEXa_.mjs} +13 -10
  13. package/dist/{scene-monitor-copy-CvzuJJ8Z.mjs → scene-monitor-copy-CnNe3kRP.mjs} +1 -1
  14. package/dist/{square-CsljmL2M.mjs → square-DqNJ_s4z.mjs} +1 -1
  15. package/dist/{trash-2-DwX9K1LA.mjs → trash-2-GWwk-1z0.mjs} +1 -1
  16. package/dist/{virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-CoN9WY8L.mjs → virtual_mf-REMOTE_ENTRY_ID___mfe_internal__addon_model_studio_page__remoteEntry_js-TjjXHsNj.mjs} +1 -1
  17. package/package.json +1 -1
  18. package/dist/_virtual_mf___mfe_internal__addon_model_studio_page__loadShare___mf_0_camstack_mf_1_types__loadShare__.js-CDZyIXC0.mjs +0 -26
@@ -5803,7 +5803,7 @@ var ZodIssueCode = {
5803
5803
  var ZodFirstPartyTypeKind;
5804
5804
  ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
5805
5805
  //#endregion
5806
- //#region ../types/dist/sleep-BErAxhul.mjs
5806
+ //#region ../types/dist/sleep-B81gwQuR.mjs
5807
5807
  /**
5808
5808
  * The audio chunk plane's byte format, and the ONE expansion from a coded
5809
5809
  * window to float samples (D455).
@@ -8290,6 +8290,99 @@ var RecordKindDeclarationSchema = object({
8290
8290
  }).strict();
8291
8291
  /** The kind names: lower-case, dash separated, short enough for an index name. */
8292
8292
  var RecordKindNameSchema = string().regex(/^[a-z][a-z0-9-]{0,47}$/);
8293
+ var READING_QUANTITIES = [
8294
+ "temperature",
8295
+ "pressure",
8296
+ "speed",
8297
+ "precipitation",
8298
+ "precipitation-rate",
8299
+ "length",
8300
+ "area",
8301
+ "illuminance",
8302
+ "irradiance",
8303
+ "power",
8304
+ "energy",
8305
+ "voltage",
8306
+ "current",
8307
+ "apparent-power",
8308
+ "humidity",
8309
+ "percentage",
8310
+ "concentration-volume",
8311
+ "concentration-mass",
8312
+ "concentration-ppb",
8313
+ "volume",
8314
+ "duration",
8315
+ "duration-ms",
8316
+ "information",
8317
+ "sound-level",
8318
+ "signal-level",
8319
+ "colour-temperature",
8320
+ "angle",
8321
+ "bitrate",
8322
+ "rate",
8323
+ "mass",
8324
+ "index",
8325
+ "ratio",
8326
+ "count",
8327
+ "none",
8328
+ "device"
8329
+ ];
8330
+ var ReadingQuantitySchema = _enum(READING_QUANTITIES);
8331
+ new Set(READING_QUANTITIES);
8332
+ /**
8333
+ * Stored unit symbol for a quantity. `none` means the number is dimensionless
8334
+ * (or an index / ratio / count). `device` means the cap has no single unit.
8335
+ */
8336
+ var QUANTITY_BASE = {
8337
+ temperature: "°C",
8338
+ pressure: "hPa",
8339
+ speed: "m/s",
8340
+ precipitation: "mm",
8341
+ "precipitation-rate": "mm/h",
8342
+ length: "m",
8343
+ area: "m²",
8344
+ illuminance: "lx",
8345
+ irradiance: "W/m²",
8346
+ power: "W",
8347
+ energy: "kWh",
8348
+ voltage: "V",
8349
+ current: "A",
8350
+ "apparent-power": "VA",
8351
+ humidity: "%",
8352
+ percentage: "%",
8353
+ "concentration-volume": "ppm",
8354
+ "concentration-mass": "µg/m³",
8355
+ "concentration-ppb": "ppb",
8356
+ volume: "L",
8357
+ duration: "s",
8358
+ "duration-ms": "ms",
8359
+ information: "B",
8360
+ "sound-level": "dBFS",
8361
+ "signal-level": "dBm",
8362
+ "colour-temperature": "mired",
8363
+ angle: "°",
8364
+ bitrate: "kbps",
8365
+ rate: "fps",
8366
+ mass: "kg",
8367
+ index: "none",
8368
+ ratio: "none",
8369
+ count: "none",
8370
+ none: "none",
8371
+ device: "device"
8372
+ };
8373
+ /** The stored unit, or `undefined` when the quantity has no unit symbol. */
8374
+ function baseUnitOf(quantity) {
8375
+ const symbol = QUANTITY_BASE[quantity];
8376
+ if (symbol === "none" || symbol === "device") return void 0;
8377
+ return symbol;
8378
+ }
8379
+ function readingMeta(quantity, extras) {
8380
+ return {
8381
+ quantity,
8382
+ ...extras?.precision !== void 0 ? { precision: extras.precision } : {},
8383
+ ...extras?.label !== void 0 ? { label: extras.label } : {}
8384
+ };
8385
+ }
8293
8386
  /** Shared Zod schemas used across streaming capabilities. */
8294
8387
  var CamProfileSchema = _enum([
8295
8388
  "high",
@@ -8317,8 +8410,8 @@ var CamStreamKindSchema = _enum([
8317
8410
  "derived"
8318
8411
  ]);
8319
8412
  var CamStreamResolutionSchema = object({
8320
- width: number().int().positive(),
8321
- height: number().int().positive()
8413
+ width: number().int().positive().meta(readingMeta("none")),
8414
+ height: number().int().positive().meta(readingMeta("none"))
8322
8415
  });
8323
8416
  var CameraStreamSchema = object({
8324
8417
  /** Stable, provider-assigned id unique within the (deviceId) scope. */
@@ -8913,6 +9006,7 @@ var CAP_NAMES = Object.freeze([
8913
9006
  "filesystem-browse",
8914
9007
  "flood",
8915
9008
  "gas",
9009
+ "group-status",
8916
9010
  "heating-valve",
8917
9011
  "humidifier",
8918
9012
  "humidity-sensor",
@@ -9042,14 +9136,6 @@ Object.freeze({
9042
9136
  {
9043
9137
  "path": "aqi",
9044
9138
  "valueType": "number"
9045
- },
9046
- {
9047
- "path": "unit",
9048
- "valueType": "string"
9049
- },
9050
- {
9051
- "path": "precision",
9052
- "valueType": "number"
9053
9139
  }
9054
9140
  ],
9055
9141
  "alarm-panel": [
@@ -9085,20 +9171,10 @@ Object.freeze({
9085
9171
  "valueType": "boolean"
9086
9172
  }
9087
9173
  ],
9088
- "ambient-light-sensor": [
9089
- {
9090
- "path": "lux",
9091
- "valueType": "number"
9092
- },
9093
- {
9094
- "path": "unit",
9095
- "valueType": "string"
9096
- },
9097
- {
9098
- "path": "precision",
9099
- "valueType": "number"
9100
- }
9101
- ],
9174
+ "ambient-light-sensor": [{
9175
+ "path": "lux",
9176
+ "valueType": "number"
9177
+ }],
9102
9178
  "audio-metrics": [
9103
9179
  {
9104
9180
  "path": "ts",
@@ -9178,6 +9254,10 @@ Object.freeze({
9178
9254
  "path": "lastUpdated",
9179
9255
  "valueType": "number"
9180
9256
  },
9257
+ {
9258
+ "path": "volts",
9259
+ "valueType": "number"
9260
+ },
9181
9261
  {
9182
9262
  "path": "lastContactAt",
9183
9263
  "valueType": "number"
@@ -9420,10 +9500,6 @@ Object.freeze({
9420
9500
  "datetime"
9421
9501
  ]
9422
9502
  },
9423
- {
9424
- "path": "unit",
9425
- "valueType": "string"
9426
- },
9427
9503
  {
9428
9504
  "path": "min",
9429
9505
  "valueType": "number"
@@ -9435,19 +9511,6 @@ Object.freeze({
9435
9511
  {
9436
9512
  "path": "step",
9437
9513
  "valueType": "number"
9438
- },
9439
- {
9440
- "path": "precision",
9441
- "valueType": "number"
9442
- },
9443
- {
9444
- "path": "format",
9445
- "valueType": "enum",
9446
- "enumValues": [
9447
- "date",
9448
- "time",
9449
- "datetime"
9450
- ]
9451
9514
  }
9452
9515
  ],
9453
9516
  "cover": [
@@ -9688,13 +9751,8 @@ Object.freeze({
9688
9751
  "path": "value",
9689
9752
  "valueType": "string"
9690
9753
  }, {
9691
- "path": "format",
9692
- "valueType": "enum",
9693
- "enumValues": [
9694
- "date",
9695
- "time",
9696
- "datetime"
9697
- ]
9754
+ "path": "timestamp",
9755
+ "valueType": "boolean"
9698
9756
  }],
9699
9757
  "event-emitter": [
9700
9758
  {
@@ -9767,6 +9825,20 @@ Object.freeze({
9767
9825
  "path": "detected",
9768
9826
  "valueType": "boolean"
9769
9827
  }],
9828
+ "group-status": [
9829
+ {
9830
+ "path": "included",
9831
+ "valueType": "number"
9832
+ },
9833
+ {
9834
+ "path": "excluded",
9835
+ "valueType": "number"
9836
+ },
9837
+ {
9838
+ "path": "total",
9839
+ "valueType": "number"
9840
+ }
9841
+ ],
9770
9842
  "heating-valve": [{
9771
9843
  "path": "position",
9772
9844
  "valueType": "number"
@@ -9801,20 +9873,10 @@ Object.freeze({
9801
9873
  "valueType": "number"
9802
9874
  }
9803
9875
  ],
9804
- "humidity-sensor": [
9805
- {
9806
- "path": "percent",
9807
- "valueType": "number"
9808
- },
9809
- {
9810
- "path": "unit",
9811
- "valueType": "string"
9812
- },
9813
- {
9814
- "path": "precision",
9815
- "valueType": "number"
9816
- }
9817
- ],
9876
+ "humidity-sensor": [{
9877
+ "path": "percent",
9878
+ "valueType": "number"
9879
+ }],
9818
9880
  "image": [{
9819
9881
  "path": "url",
9820
9882
  "valueType": "string"
@@ -10153,20 +10215,10 @@ Object.freeze({
10153
10215
  "valueType": "boolean"
10154
10216
  }
10155
10217
  ],
10156
- "numeric-sensor": [
10157
- {
10158
- "path": "value",
10159
- "valueType": "number"
10160
- },
10161
- {
10162
- "path": "unit",
10163
- "valueType": "string"
10164
- },
10165
- {
10166
- "path": "precision",
10167
- "valueType": "number"
10168
- }
10169
- ],
10218
+ "numeric-sensor": [{
10219
+ "path": "value",
10220
+ "valueType": "number"
10221
+ }],
10170
10222
  "pet-feeder": [
10171
10223
  {
10172
10224
  "path": "foodLevel",
@@ -10258,14 +10310,6 @@ Object.freeze({
10258
10310
  {
10259
10311
  "path": "powerFactor",
10260
10312
  "valueType": "number"
10261
- },
10262
- {
10263
- "path": "unit",
10264
- "valueType": "string"
10265
- },
10266
- {
10267
- "path": "precision",
10268
- "valueType": "number"
10269
10313
  }
10270
10314
  ],
10271
10315
  "presence": [
@@ -10294,20 +10338,10 @@ Object.freeze({
10294
10338
  "valueType": "number"
10295
10339
  }
10296
10340
  ],
10297
- "pressure-sensor": [
10298
- {
10299
- "path": "hpa",
10300
- "valueType": "number"
10301
- },
10302
- {
10303
- "path": "unit",
10304
- "valueType": "string"
10305
- },
10306
- {
10307
- "path": "precision",
10308
- "valueType": "number"
10309
- }
10310
- ],
10341
+ "pressure-sensor": [{
10342
+ "path": "hpa",
10343
+ "valueType": "number"
10344
+ }],
10311
10345
  "privacy-mask": [{
10312
10346
  "path": "enabled",
10313
10347
  "valueType": "boolean"
@@ -10618,20 +10652,10 @@ Object.freeze({
10618
10652
  "path": "tampered",
10619
10653
  "valueType": "boolean"
10620
10654
  }],
10621
- "temperature-sensor": [
10622
- {
10623
- "path": "celsius",
10624
- "valueType": "number"
10625
- },
10626
- {
10627
- "path": "unit",
10628
- "valueType": "string"
10629
- },
10630
- {
10631
- "path": "precision",
10632
- "valueType": "number"
10633
- }
10634
- ],
10655
+ "temperature-sensor": [{
10656
+ "path": "celsius",
10657
+ "valueType": "number"
10658
+ }],
10635
10659
  "update": [
10636
10660
  {
10637
10661
  "path": "currentVersion",
@@ -10742,10 +10766,6 @@ Object.freeze({
10742
10766
  "path": "temperature",
10743
10767
  "valueType": "number"
10744
10768
  },
10745
- {
10746
- "path": "temperatureUnit",
10747
- "valueType": "string"
10748
- },
10749
10769
  {
10750
10770
  "path": "humidity",
10751
10771
  "valueType": "number"
@@ -10754,18 +10774,10 @@ Object.freeze({
10754
10774
  "path": "pressure",
10755
10775
  "valueType": "number"
10756
10776
  },
10757
- {
10758
- "path": "pressureUnit",
10759
- "valueType": "string"
10760
- },
10761
10777
  {
10762
10778
  "path": "windSpeed",
10763
10779
  "valueType": "number"
10764
10780
  },
10765
- {
10766
- "path": "windSpeedUnit",
10767
- "valueType": "string"
10768
- },
10769
10781
  {
10770
10782
  "path": "windBearing",
10771
10783
  "valueType": "number"
@@ -11158,7 +11170,7 @@ var DeviceRole = /* @__PURE__ */ function(DeviceRole) {
11158
11170
  return DeviceRole;
11159
11171
  }({});
11160
11172
  //#endregion
11161
- //#region ../types/dist/composition-kEl9mevu.mjs
11173
+ //#region ../types/dist/composition-B85l9j1M.mjs
11162
11174
  /**
11163
11175
  * The ffmpeg build every CamStack node downloads, per platform and arch.
11164
11176
  *
@@ -11913,15 +11925,67 @@ function validateExpressionSource(src) {
11913
11925
  }
11914
11926
  return null;
11915
11927
  }
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;
11928
+ /**
11929
+ * A composition source identity: `(addonId, stableId)`, never a numeric device
11930
+ * id. Split out so helper schemas can name it without a cycle through
11931
+ * `composition.ts` (D659).
11932
+ */
11920
11933
  var CompositionSourceRefSchema = object({
11921
11934
  /** The owning addon; `stableId` is unique only within it. */
11922
11935
  addonId: string().min(1),
11923
11936
  stableId: string().min(1)
11924
11937
  });
11938
+ var GROUP_TEMPLATE_IDS = [
11939
+ "temperature",
11940
+ "humidity",
11941
+ "pressure",
11942
+ "illuminance",
11943
+ "power",
11944
+ "energy",
11945
+ "battery",
11946
+ "numeric",
11947
+ "motion",
11948
+ "contact",
11949
+ "leak",
11950
+ "smoke",
11951
+ "occupancy",
11952
+ "binary"
11953
+ ];
11954
+ var NUMERIC_AGGREGATES = [
11955
+ "mean",
11956
+ "median",
11957
+ "min",
11958
+ "max",
11959
+ "sum"
11960
+ ];
11961
+ var BINARY_AGGREGATES = ["any", "all"];
11962
+ var AGGREGATE_REDUCERS = [
11963
+ "mean",
11964
+ "median",
11965
+ "min",
11966
+ "max",
11967
+ "sum",
11968
+ "any",
11969
+ "all"
11970
+ ];
11971
+ var GroupTemplateIdSchema = _enum(GROUP_TEMPLATE_IDS);
11972
+ var AggregateReducerSchema = _enum(AGGREGATE_REDUCERS);
11973
+ /** One member. The field is stored so occupancy can name motion or binary. */
11974
+ var HelperMemberSchema = object({
11975
+ source: CompositionSourceRefSchema,
11976
+ cap: string().min(1),
11977
+ fieldPath: string().min(1)
11978
+ });
11979
+ var HelperSpecSchema = discriminatedUnion("kind", [object({
11980
+ kind: literal("group"),
11981
+ templateId: GroupTemplateIdSchema,
11982
+ aggregate: AggregateReducerSchema,
11983
+ members: array(HelperMemberSchema).min(1).max(64)
11984
+ })]);
11985
+ /** A composed device's stableId is this prefix plus the block id. */
11986
+ var COMPOSED_DEVICE_STABLE_ID_PREFIX = "composed-";
11987
+ /** Longest snippet a `code` source may carry (slice 4). */
11988
+ var MAX_COMPOSITION_CODE_LENGTH = 2e4;
11925
11989
  /** One field of one capability of one source device. */
11926
11990
  var CompositionFieldReadSchema = object({
11927
11991
  source: CompositionSourceRefSchema,
@@ -11965,7 +12029,33 @@ var CompositionCodeSourceSchema = object({
11965
12029
  var CompositionFieldSourceSchema = discriminatedUnion("kind", [
11966
12030
  CompositionFromSourceSchema,
11967
12031
  CompositionExpressionSourceSchema,
11968
- CompositionCodeSourceSchema
12032
+ CompositionCodeSourceSchema,
12033
+ object({
12034
+ kind: literal("aggregate"),
12035
+ reducer: _enum([
12036
+ "mean",
12037
+ "median",
12038
+ "min",
12039
+ "max",
12040
+ "sum",
12041
+ "any",
12042
+ "all"
12043
+ ]),
12044
+ reads: array(CompositionFieldReadSchema).min(1).max(64),
12045
+ /** `value` is the reading; the others are the member counts. */
12046
+ publish: _enum([
12047
+ "value",
12048
+ "included",
12049
+ "excluded",
12050
+ "total"
12051
+ ]),
12052
+ /** What a `value` publish does when nobody is included. Counts ignore this. */
12053
+ empty: _enum([
12054
+ "null",
12055
+ "omit",
12056
+ "offline"
12057
+ ])
12058
+ })
11969
12059
  ]);
11970
12060
  var CompositionCommandTargetSchema = discriminatedUnion("kind", [object({
11971
12061
  kind: literal("forward"),
@@ -12011,7 +12101,13 @@ var CompositionSchema = object({
12011
12101
  kind: literal("existing"),
12012
12102
  device: CompositionSourceRefSchema
12013
12103
  })]),
12014
- features: array(CompositionFeatureSchema).min(1).max(16)
12104
+ features: array(CompositionFeatureSchema).min(1).max(16),
12105
+ /**
12106
+ * Present when the block is a helper template. Absent on a free
12107
+ * composition (the room example, device 4603). The editor reads this,
12108
+ * never the compiled features.
12109
+ */
12110
+ helper: HelperSpecSchema.optional()
12015
12111
  }).superRefine((composition, ctx) => {
12016
12112
  const seen = /* @__PURE__ */ new Set();
12017
12113
  composition.features.forEach((feature, index) => {
@@ -18269,7 +18365,11 @@ var CompositionProblemSchema = object({
18269
18365
  "item-field-reserved",
18270
18366
  "target-missing",
18271
18367
  "target-unreadable",
18272
- "not-replaceable"
18368
+ "not-replaceable",
18369
+ "native-capability-replace",
18370
+ "mixed-quantity",
18371
+ "helper-mismatch",
18372
+ "member-constraint"
18273
18373
  ]),
18274
18374
  path: string(),
18275
18375
  message: string()
@@ -18290,7 +18390,8 @@ var CompositionUnavailableModeSchema = _enum([
18290
18390
  "null",
18291
18391
  "offline",
18292
18392
  "unknown",
18293
- "release"
18393
+ "release",
18394
+ "omit"
18294
18395
  ]);
18295
18396
  /**
18296
18397
  * `derived` reads a source · `clock` reads only `now`, recomputed when a sibling
@@ -18327,9 +18428,11 @@ var CompositionFieldPlanSchema = object({
18327
18428
  reads: array(CompositionFieldReadSchema)
18328
18429
  });
18329
18430
  /**
18330
- * What a feature does to its target (D663): `new` builds a capability on a new
18331
- * device; on an existing device it `add`s a capability the device lacks, or
18332
- * `replace`s named fields of one it has natively.
18431
+ * What a feature does to its target (D663, D944): `new` builds a capability on a
18432
+ * new device; on an existing device it `add`s a capability the device lacks.
18433
+ * Replacing a capability the device already serves is refused
18434
+ * (`native-capability-replace`), not planned. `replace` remains on the claim
18435
+ * wire for a claim the hub already holds.
18333
18436
  */
18334
18437
  var CompositionFeatureModeSchema = _enum([
18335
18438
  "new",
@@ -18502,6 +18605,235 @@ Object.freeze({
18502
18605
  result: ["number", "null"]
18503
18606
  }
18504
18607
  });
18608
+ /**
18609
+ * One catalog row per base reading a group helper can build (D944 §3).
18610
+ * Data, not a form default written into the block.
18611
+ */
18612
+ var NOW = {
18613
+ fieldPath: "lastFetchedAt",
18614
+ expr: "now"
18615
+ };
18616
+ var CHANGED = {
18617
+ fieldPath: "lastChangedAt",
18618
+ expr: "now"
18619
+ };
18620
+ function numeric(input) {
18621
+ return {
18622
+ ...input,
18623
+ family: "numeric",
18624
+ deviceType: DeviceType.Sensor,
18625
+ memberDeviceType: DeviceType.Sensor,
18626
+ baseUnit: baseUnitOf(input.quantity),
18627
+ aggregates: NUMERIC_AGGREGATES,
18628
+ accept: [{
18629
+ cap: input.outputCap,
18630
+ fieldPath: input.outputField
18631
+ }]
18632
+ };
18633
+ }
18634
+ function binary(input) {
18635
+ return {
18636
+ ...input,
18637
+ family: "binary",
18638
+ deviceType: DeviceType.Sensor,
18639
+ memberDeviceType: DeviceType.Sensor,
18640
+ quantity: "none",
18641
+ baseUnit: void 0,
18642
+ aggregates: BINARY_AGGREGATES,
18643
+ empty: "offline"
18644
+ };
18645
+ }
18646
+ var GROUP_TEMPLATES = [
18647
+ numeric({
18648
+ id: "temperature",
18649
+ summary: "Mean, median, min, max or sum of temperatures. Offline and unknown members are left out, never read as 0 °C.",
18650
+ role: DeviceRole.TemperatureSensor,
18651
+ outputCap: "temperature-sensor",
18652
+ outputField: "celsius",
18653
+ quantity: "temperature",
18654
+ defaultAggregate: "mean",
18655
+ empty: "null",
18656
+ companions: [NOW]
18657
+ }),
18658
+ numeric({
18659
+ id: "humidity",
18660
+ summary: "Humidity in percent. Offline and unknown members are left out, never read as 0 %.",
18661
+ role: DeviceRole.HumiditySensor,
18662
+ outputCap: "humidity-sensor",
18663
+ outputField: "percent",
18664
+ quantity: "humidity",
18665
+ defaultAggregate: "mean",
18666
+ empty: "null",
18667
+ companions: [NOW]
18668
+ }),
18669
+ numeric({
18670
+ id: "pressure",
18671
+ summary: "Pressure in hectopascals. Offline and unknown members are left out, never read as 0 hPa.",
18672
+ role: DeviceRole.PressureSensor,
18673
+ outputCap: "pressure-sensor",
18674
+ outputField: "hpa",
18675
+ quantity: "pressure",
18676
+ defaultAggregate: "mean",
18677
+ empty: "null",
18678
+ companions: [NOW]
18679
+ }),
18680
+ numeric({
18681
+ id: "illuminance",
18682
+ summary: "Illuminance in lux. Offline and unknown members are left out, never read as 0 lx.",
18683
+ role: DeviceRole.AmbientLightSensor,
18684
+ outputCap: "ambient-light-sensor",
18685
+ outputField: "lux",
18686
+ quantity: "illuminance",
18687
+ defaultAggregate: "mean",
18688
+ empty: "null",
18689
+ companions: [NOW]
18690
+ }),
18691
+ numeric({
18692
+ id: "power",
18693
+ summary: "Power in watts, summed by default. Nobody reporting omits the key rather than writing 0 W.",
18694
+ role: DeviceRole.PowerSensor,
18695
+ outputCap: "power-meter",
18696
+ outputField: "watts",
18697
+ quantity: "power",
18698
+ defaultAggregate: "sum",
18699
+ empty: "omit",
18700
+ companions: [NOW]
18701
+ }),
18702
+ numeric({
18703
+ id: "energy",
18704
+ summary: "Energy in kilowatt-hours, summed by default. Nobody reporting omits the key rather than writing 0 kWh.",
18705
+ role: DeviceRole.EnergySensor,
18706
+ outputCap: "power-meter",
18707
+ outputField: "kwhTotal",
18708
+ quantity: "energy",
18709
+ defaultAggregate: "sum",
18710
+ empty: "omit",
18711
+ companions: [NOW]
18712
+ }),
18713
+ numeric({
18714
+ id: "battery",
18715
+ summary: "Battery charge in percent. Offline and unknown members are left out, never read as 0 %.",
18716
+ role: DeviceRole.BatterySensor,
18717
+ outputCap: "battery",
18718
+ outputField: "percentage",
18719
+ quantity: "percentage",
18720
+ defaultAggregate: "mean",
18721
+ empty: "null",
18722
+ companions: [
18723
+ {
18724
+ fieldPath: "charging",
18725
+ expr: "\"unknown\""
18726
+ },
18727
+ {
18728
+ fieldPath: "sleeping",
18729
+ expr: "false"
18730
+ },
18731
+ {
18732
+ fieldPath: "lastUpdated",
18733
+ expr: "now"
18734
+ }
18735
+ ]
18736
+ }),
18737
+ numeric({
18738
+ id: "numeric",
18739
+ summary: "A numeric reading in the members’ shared quantity. Mixed quantities are refused. Nobody reporting publishes null, not 0.",
18740
+ role: DeviceRole.NumericSensor,
18741
+ outputCap: "numeric-sensor",
18742
+ outputField: "value",
18743
+ quantity: "device",
18744
+ defaultAggregate: "mean",
18745
+ empty: "null",
18746
+ companions: [NOW]
18747
+ }),
18748
+ binary({
18749
+ id: "motion",
18750
+ summary: "Any or all motion. Offline and unknown members are left out, never read as off. Nobody reporting takes the device offline.",
18751
+ role: DeviceRole.MotionSensor,
18752
+ outputCap: "motion",
18753
+ outputField: "detected",
18754
+ defaultAggregate: "any",
18755
+ accept: [{
18756
+ cap: "motion",
18757
+ fieldPath: "detected"
18758
+ }],
18759
+ companions: [{
18760
+ fieldPath: "lastDetectedAt",
18761
+ expr: "null"
18762
+ }, {
18763
+ fieldPath: "autoClearAfterMs",
18764
+ expr: "null"
18765
+ }]
18766
+ }),
18767
+ binary({
18768
+ id: "contact",
18769
+ summary: "Any or all open contacts. Offline and unknown members are left out, never read as closed.",
18770
+ role: DeviceRole.ContactSensor,
18771
+ outputCap: "contact",
18772
+ outputField: "entryOpen",
18773
+ defaultAggregate: "any",
18774
+ accept: [{
18775
+ cap: "contact",
18776
+ fieldPath: "entryOpen"
18777
+ }],
18778
+ companions: [CHANGED]
18779
+ }),
18780
+ binary({
18781
+ id: "leak",
18782
+ summary: "Any or all leak detections. Offline and unknown members are left out, never read as dry.",
18783
+ role: DeviceRole.LeakSensor,
18784
+ outputCap: "flood",
18785
+ outputField: "flooded",
18786
+ defaultAggregate: "any",
18787
+ accept: [{
18788
+ cap: "flood",
18789
+ fieldPath: "flooded"
18790
+ }],
18791
+ companions: [CHANGED]
18792
+ }),
18793
+ binary({
18794
+ id: "smoke",
18795
+ summary: "Any or all smoke detections. Offline and unknown members are left out, never read as clear.",
18796
+ role: DeviceRole.SmokeSensor,
18797
+ outputCap: "smoke",
18798
+ outputField: "detected",
18799
+ defaultAggregate: "any",
18800
+ accept: [{
18801
+ cap: "smoke",
18802
+ fieldPath: "detected"
18803
+ }],
18804
+ companions: [CHANGED]
18805
+ }),
18806
+ binary({
18807
+ id: "occupancy",
18808
+ summary: "Any or all of motion or binary sensors, published as a binary. Presence zones are not aggregated. Nobody reporting is offline, not off.",
18809
+ role: DeviceRole.BinarySensor,
18810
+ outputCap: "binary",
18811
+ outputField: "on",
18812
+ defaultAggregate: "any",
18813
+ accept: [{
18814
+ cap: "motion",
18815
+ fieldPath: "detected"
18816
+ }, {
18817
+ cap: "binary",
18818
+ fieldPath: "on"
18819
+ }],
18820
+ companions: [CHANGED]
18821
+ }),
18822
+ binary({
18823
+ id: "binary",
18824
+ summary: "Any or all generic binary sensors. Offline and unknown members are left out, never read as off.",
18825
+ role: DeviceRole.BinarySensor,
18826
+ outputCap: "binary",
18827
+ outputField: "on",
18828
+ defaultAggregate: "any",
18829
+ accept: [{
18830
+ cap: "binary",
18831
+ fieldPath: "on"
18832
+ }],
18833
+ companions: [CHANGED]
18834
+ })
18835
+ ];
18836
+ new Map(GROUP_TEMPLATES.map((template) => [template.id, template]));
18505
18837
  `${COMPOSED_DEVICE_STABLE_ID_PREFIX}`;
18506
18838
  /**
18507
18839
  * What a composition may BUILD on a device type (G2, D676): the caps a new
@@ -20243,12 +20575,12 @@ method(object({ deviceId: number().int().nonnegative() }), DeviceAdminLinkSchema
20243
20575
  * providers detect renames by matching `uniqueId` and patch `id`
20244
20576
  * in-place so dispatch keeps working.
20245
20577
  *
20246
- * **Rendering metadata is NOT stored here.** Display fields (`unit`,
20247
- * `precision`) live in the cap STATUS SLICE so they flow live with every
20248
- * state push and are the single source of truth. The `device_class` tag is
20249
- * an HA-specific input that is consumed at the discovery boundary
20250
- * (`ha-domain-mapping`) to derive the CamStack `DeviceRole`; it is never
20251
- * stored after that point.
20578
+ * **Display unit and precision are NOT stored here.** They are a read-time
20579
+ * preference (D944). A polymorphic reading stores its `quantity` here once,
20580
+ * at adopt, and a date/time value stores `dateTimeShape` the same way —
20581
+ * neither is repeated on each sample. The `device_class` tag is an
20582
+ * HA-specific input consumed at the discovery boundary (`ha-domain-mapping`)
20583
+ * to derive the CamStack `DeviceRole`; it is never stored after that point.
20252
20584
  *
20253
20585
  * The `raw` escape hatch carries provider-specific attributes that don't
20254
20586
  * fit the structured slots (HA `attributes` whitelist, Reolink `deviceCache`
@@ -20262,6 +20594,14 @@ var SourceInfoSchema = object({
20262
20594
  /** Immutable upstream identifier when available (HA `unique_id`, … ).
20263
20595
  * Used to detect rename when `id` changes. */
20264
20596
  uniqueId: string().optional(),
20597
+ /** Quantity of a polymorphic reading, written once at adopt (D944). */
20598
+ quantity: ReadingQuantitySchema.optional(),
20599
+ /** Date / time / datetime shape of a string reading, written once at adopt. */
20600
+ dateTimeShape: _enum([
20601
+ "date",
20602
+ "time",
20603
+ "datetime"
20604
+ ]).optional(),
20265
20605
  /** Provider-specific extras that don't fit the structured slots.
20266
20606
  * Whitelist what goes here per provider — do NOT dump entire HA
20267
20607
  * attribute blobs (would explode the meta JSON column). */
@@ -41174,7 +41514,8 @@ method(object({
41174
41514
  "enum"
41175
41515
  ]),
41176
41516
  enumValues: array(string()).optional(),
41177
- item: boolean().optional()
41517
+ item: boolean().optional(),
41518
+ quantity: ReadingQuantitySchema.optional()
41178
41519
  })).readonly(),
41179
41520
  itemArray: object({
41180
41521
  path: string(),
@@ -44980,8 +45321,8 @@ var MaskPolygonShapeSchema = object({
44980
45321
  /** Boolean cell grid — row-major, length === gridWidth*gridHeight. */
44981
45322
  var MaskGridShapeSchema = object({
44982
45323
  kind: literal("grid"),
44983
- gridWidth: number(),
44984
- gridHeight: number(),
45324
+ gridWidth: number().meta(readingMeta("none")),
45325
+ gridHeight: number().meta(readingMeta("none")),
44985
45326
  cells: array(boolean())
44986
45327
  });
44987
45328
  discriminatedUnion("kind", [
@@ -56375,46 +56716,27 @@ method(object({ nodeId: string().optional() }).optional(), array(AddonListItemSc
56375
56716
  }), LogStreamEntrySchema, { kind: "subscription" });
56376
56717
  object({
56377
56718
  /** Carbon dioxide concentration in ppm. */
56378
- co2Ppm: number().min(0).optional(),
56719
+ co2Ppm: number().min(0).optional().meta(readingMeta("concentration-volume")),
56379
56720
  /** Total volatile organic compounds in ppb. */
56380
- vocPpb: number().min(0).optional(),
56721
+ vocPpb: number().min(0).optional().meta(readingMeta("concentration-ppb")),
56381
56722
  /** Particulate matter ≤ 2.5 μm in µg/m³. */
56382
- pm25: number().min(0).optional(),
56723
+ pm25: number().min(0).optional().meta(readingMeta("concentration-mass")),
56383
56724
  /** Particulate matter ≤ 10 μm in µg/m³. */
56384
- pm10: number().min(0).optional(),
56725
+ pm10: number().min(0).optional().meta(readingMeta("concentration-mass")),
56385
56726
  /** Composite AQI value (typically 0..500). */
56386
- aqi: number().optional(),
56727
+ aqi: number().optional().meta(readingMeta("index")),
56387
56728
  /** Ms epoch when the slice was last updated. */
56388
- lastFetchedAt: number(),
56389
- /** Live display unit of the single metric this slice carries (e.g. HA
56390
- * `attributes.unit_of_measurement` → 'ppm' / 'ppb' / 'µg/m³'). Each
56391
- * upstream `sensor.*` entity surfaces ONE device_class, so one unit
56392
- * per slice is unambiguous. */
56393
- unit: string().optional(),
56394
- /** Suggested decimal places for numeric display.
56395
- * Populated live from the upstream source when provided (e.g. HA
56396
- * `attributes.suggested_display_precision`). Falls back to
56397
- * auto-formatting when absent. */
56398
- precision: number().int().min(0).max(10).optional()
56729
+ lastFetchedAt: number()
56399
56730
  });
56400
56731
  DeviceType.Sensor;
56401
56732
  object({
56402
- /** Current illuminance in lux (lx). */
56403
- lux: number().min(0).meta({
56404
- unit: "lx",
56733
+ /** Current illuminance in lux (lx). `null` when the sample was refused (D393). */
56734
+ lux: number().min(0).nullable().meta(readingMeta("illuminance", {
56735
+ precision: 0,
56405
56736
  label: "Illuminance"
56406
- }),
56737
+ })),
56407
56738
  /** Ms epoch when the slice was last updated. */
56408
- lastFetchedAt: number(),
56409
- /** Live display unit from the upstream source (e.g. HA
56410
- * `attributes.unit_of_measurement`). The UI prefers this over the
56411
- * role's canonical unit. Absent → fall back to the canonical unit. */
56412
- unit: string().optional(),
56413
- /** Suggested decimal places for numeric display.
56414
- * Populated live from the upstream source when provided (e.g. HA
56415
- * `attributes.suggested_display_precision`). Falls back to
56416
- * auto-formatting when absent. */
56417
- precision: number().int().min(0).max(10).optional()
56739
+ lastFetchedAt: number()
56418
56740
  });
56419
56741
  DeviceType.Sensor;
56420
56742
  /**
@@ -56559,10 +56881,7 @@ var BatteryStatusSchema = object({
56559
56881
  * Consumers must SKIP a null rather than coerce it — `battery-band` already
56560
56882
  * declines to band a non-finite reading, which is the correct shape.
56561
56883
  */
56562
- percentage: number().min(0).max(100).nullable().meta({
56563
- unit: "%",
56564
- label: "Charge"
56565
- }),
56884
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage", { label: "Charge" })),
56566
56885
  /**
56567
56886
  * Charging source. `'dc'` covers wall/USB adapters; `'solar'` is
56568
56887
  * Reolink-specific for the Solar Panel 2 accessory (will become
@@ -56589,7 +56908,14 @@ var BatteryStatusSchema = object({
56589
56908
  */
56590
56909
  sleeping: boolean().meta({ label: "Sleeping" }),
56591
56910
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
56592
- lastUpdated: number(),
56911
+ lastUpdated: number().meta(readingMeta("none")),
56912
+ /**
56913
+ * Battery voltage in volts, for a cell the firmware reports as voltage
56914
+ * rather than a percentage (Ecowitt capacitor packs). Absent on cameras
56915
+ * that only report charge. A second reading on this cap, not a second
56916
+ * `numeric-sensor` on the same device.
56917
+ */
56918
+ volts: number().nullable().optional().meta(readingMeta("voltage", { label: "Voltage" })),
56593
56919
  /**
56594
56920
  * Ms epoch of the last time the device PROVED it was reachable — a
56595
56921
  * completed firmware round-trip, an observed wake, or an inbound push
@@ -56649,7 +56975,7 @@ object({
56649
56975
  DeviceType.Sensor;
56650
56976
  object({
56651
56977
  /** Current level as 0..100 inclusive. Firmware-reported. */
56652
- percentage: number().min(0).max(100),
56978
+ percentage: number().min(0).max(100).meta(readingMeta("percentage")),
56653
56979
  /** Ms epoch of the last operator-driven change. Useful for UI freshness. */
56654
56980
  lastChangedAt: number()
56655
56981
  });
@@ -57034,21 +57360,21 @@ object({
57034
57360
  /** Single setpoint in Celsius. Used by single-target modes
57035
57361
  * (heat / cool / dry). Null when the device is in a range mode
57036
57362
  * (`heat_cool`) or has no setpoint at all (`fan_only`/`off`). */
57037
- target: number().nullable(),
57363
+ target: number().nullable().meta(readingMeta("temperature")),
57038
57364
  /** Upper bound of the dual setpoint in Celsius. Populated only
57039
57365
  * in `heat_cool` mode. */
57040
- targetHigh: number().nullable(),
57366
+ targetHigh: number().nullable().meta(readingMeta("temperature")),
57041
57367
  /** Lower bound of the dual setpoint in Celsius. Populated only
57042
57368
  * in `heat_cool` mode. */
57043
- targetLow: number().nullable(),
57369
+ targetLow: number().nullable().meta(readingMeta("temperature")),
57044
57370
  /** Target relative humidity (0..100). Null when the device has no
57045
57371
  * humidity control. */
57046
- targetHumidity: number().min(0).max(100).nullable(),
57372
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57047
57373
  /** Current measured temperature in Celsius. Read-only — pushed by
57048
57374
  * the device's internal sensor. */
57049
- currentTemp: number().nullable(),
57375
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
57050
57376
  /** Current measured relative humidity (0..100). Read-only. */
57051
- currentHumidity: number().min(0).max(100).nullable(),
57377
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
57052
57378
  /** Vertical louver swing on/off. Null when the device has no vertical
57053
57379
  * swing surface (gate with `DeviceFeature.ClimateSwingVertical`). */
57054
57380
  swingVertical: boolean().nullable(),
@@ -57056,11 +57382,11 @@ object({
57056
57382
  * horizontal swing surface (gate with `DeviceFeature.ClimateSwingHorizontal`). */
57057
57383
  swingHorizontal: boolean().nullable(),
57058
57384
  /** Lowest setpoint the device accepts, Celsius. Absent / null = not reported. */
57059
- minTemp: number().nullable().optional(),
57385
+ minTemp: number().nullable().optional().meta(readingMeta("temperature")),
57060
57386
  /** Highest setpoint the device accepts, Celsius. Absent / null = not reported. */
57061
- maxTemp: number().nullable().optional(),
57387
+ maxTemp: number().nullable().optional().meta(readingMeta("temperature")),
57062
57388
  /** Setpoint granularity, Celsius (0.5, 1). Absent / null = not reported. */
57063
- tempStep: number().positive().nullable().optional(),
57389
+ tempStep: number().positive().nullable().optional().meta(readingMeta("temperature")),
57064
57390
  /** Ms epoch when the slice was last updated (push or command). */
57065
57391
  lastFetchedAt: number()
57066
57392
  });
@@ -57139,17 +57465,17 @@ DeviceType.Thermostat, DeviceType.Climate, method(object({
57139
57465
  * modes to expose in the picker without a slice fetch.
57140
57466
  */
57141
57467
  var RgbTripletSchema = object({
57142
- r: number().int().min(0).max(255),
57143
- g: number().int().min(0).max(255),
57144
- b: number().int().min(0).max(255)
57468
+ r: number().int().min(0).max(255).meta(readingMeta("none")),
57469
+ g: number().int().min(0).max(255).meta(readingMeta("none")),
57470
+ b: number().int().min(0).max(255).meta(readingMeta("none"))
57145
57471
  });
57146
57472
  var HsvTripletSchema = object({
57147
57473
  /** Hue in degrees, 0..360. */
57148
- h: number().min(0).max(360),
57474
+ h: number().min(0).max(360).meta(readingMeta("none")),
57149
57475
  /** Saturation as 0..100 inclusive. */
57150
- s: number().min(0).max(100),
57476
+ s: number().min(0).max(100).meta(readingMeta("none")),
57151
57477
  /** Value/brightness as 0..100 inclusive. */
57152
- v: number().min(0).max(100)
57478
+ v: number().min(0).max(100).meta(readingMeta("none"))
57153
57479
  });
57154
57480
  var ColorInputSchema = discriminatedUnion("mode", [
57155
57481
  object({
@@ -57178,7 +57504,7 @@ object({
57178
57504
  /** Populated when `mode === 'hsv'`. */
57179
57505
  hsv: HsvTripletSchema.optional(),
57180
57506
  /** Populated when `mode === 'mired'`. */
57181
- mireds: number().int().optional(),
57507
+ mireds: number().int().optional().meta(readingMeta("colour-temperature")),
57182
57508
  /** Ms epoch of the last operator-driven change. */
57183
57509
  lastChangedAt: number()
57184
57510
  });
@@ -57302,18 +57628,12 @@ _enum([
57302
57628
  "empty",
57303
57629
  "full"
57304
57630
  ]);
57305
- /** Units a remaining-life counter is reported in. Closed: the panel renders
57306
- * each. */
57307
- var ConsumableRemainingUnitSchema = _enum([
57308
- "days",
57309
- "hours",
57310
- "cycles"
57311
- ]);
57312
- /** Remaining life as a COUNT, not a percentage (the PetKit desiccant reports
57313
- * days). `value` null = unknown (D393). */
57631
+ /** Whether `remaining.value` is a duration in seconds or a dimensionless count. */
57632
+ var ConsumableRemainingKindSchema = _enum(["duration", "count"]);
57633
+ /** Remaining life. `value` null = unknown (D393). Duration is seconds. */
57314
57634
  var ConsumableRemainingSchema = object({
57315
- value: number().min(0).nullable(),
57316
- unit: ConsumableRemainingUnitSchema
57635
+ value: number().min(0).nullable().meta(readingMeta("device")),
57636
+ kind: ConsumableRemainingKindSchema
57317
57637
  });
57318
57638
  var ConsumablesStatusSchema = object({
57319
57639
  items: array(object({
@@ -57322,7 +57642,7 @@ var ConsumablesStatusSchema = object({
57322
57642
  /** Display name. */
57323
57643
  label: string().min(1),
57324
57644
  /** Remaining life % (`wear`) or fill % (`supply`, `waste`) when known (0..100). */
57325
- level: number().min(0).max(100).nullable(),
57645
+ level: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57326
57646
  /** The provider's discrete verdict when known (binary mode). */
57327
57647
  status: ConsumableStatusSchema.nullable(),
57328
57648
  /** What the item is — decides whether low or full is bad. Absent = `wear`. */
@@ -57339,7 +57659,7 @@ var ConsumablesStatusSchema = object({
57339
57659
  })),
57340
57660
  lastChangedAt: number()
57341
57661
  });
57342
- Object.values(DeviceType), [...ConsumableRemainingUnitSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57662
+ Object.values(DeviceType), [...ConsumableRemainingKindSchema.options], [...ConsumableStatusSchema.options], [...ConsumableKindSchema.options], [...ConsumableFormSchema.options];
57343
57663
  Object.values(DeviceType), method(object({
57344
57664
  deviceId: number().int().nonnegative(),
57345
57665
  key: string().min(1)
@@ -57364,39 +57684,17 @@ object({
57364
57684
  /** Current value. Numeric kind → number; select/text/datetime → string.
57365
57685
  * Datetime values are ISO 8601 strings (full timestamp, date-only,
57366
57686
  * or time-only — HA accepts all three forms). */
57367
- value: union([number(), string()]),
57687
+ value: union([number().meta(readingMeta("device")), string()]),
57368
57688
  /** Acceptable values when `kind === 'select'`. Empty for other kinds. */
57369
57689
  options: array(string()),
57370
57690
  /** Ms epoch when the slice was last updated. */
57371
57691
  lastChangedAt: number(),
57372
- /** Display unit-of-measurement (e.g. '°C', '%', 'lx').
57373
- * Populated live from HA `attributes.unit_of_measurement` on each push.
57374
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57375
- unit: string().optional(),
57376
- /** Minimum allowed value. Populated live from HA `attributes.min`.
57377
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57378
- min: number().optional(),
57379
- /** Maximum allowed value. Populated live from HA `attributes.max`.
57380
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57381
- max: number().optional(),
57382
- /** Step increment. Populated live from HA `attributes.step`.
57383
- * Meaningful for `kind === 'numeric'`; absent for other kinds. */
57384
- step: number().optional(),
57385
- /** Suggested decimal places for numeric display. Populated live from HA
57386
- * `attributes.suggested_display_precision`, or derived from `step` when the
57387
- * HA attribute is absent. Meaningful for `kind === 'numeric'`; absent for
57388
- * other kinds. Falls back to auto-formatting when absent. */
57389
- precision: number().int().min(0).max(10).optional(),
57390
- /** Date-picker granularity for `kind === 'datetime'`. 'date' → date-only,
57391
- * 'time' → time-only, 'datetime' → full timestamp. Absent for other kinds
57392
- * (UI defaults to 'datetime'). Derived live from the device's HA domain
57393
- * (date.* / time.* / datetime.*) or, for input_datetime, from the entity's
57394
- * `has_date`/`has_time` attributes on each push. */
57395
- format: _enum([
57396
- "date",
57397
- "time",
57398
- "datetime"
57399
- ]).optional()
57692
+ /** Minimum allowed value, in the device quantity's base unit. */
57693
+ min: number().optional().meta(readingMeta("device")),
57694
+ /** Maximum allowed value, in the device quantity's base unit. */
57695
+ max: number().optional().meta(readingMeta("device")),
57696
+ /** Step increment, in the device quantity's base unit. */
57697
+ step: number().optional().meta(readingMeta("device"))
57400
57698
  });
57401
57699
  var ControlSetValueInputSchema = discriminatedUnion("kind", [
57402
57700
  object({
@@ -57434,9 +57732,9 @@ object({
57434
57732
  ]),
57435
57733
  /** 0 = fully closed, 100 = fully open. Null when the device has
57436
57734
  * no intermediate position surface. */
57437
- position: number().min(0).max(100).nullable(),
57735
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57438
57736
  /** 0 = closed slats, 100 = open slats. Null when no tilt support. */
57439
- tiltPosition: number().min(0).max(100).nullable(),
57737
+ tiltPosition: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57440
57738
  /** Ms epoch when the slice was last updated. */
57441
57739
  lastChangedAt: number()
57442
57740
  });
@@ -57494,9 +57792,9 @@ var NormalizedRangeSchema$1 = object({
57494
57792
  object({
57495
57793
  mode: DayNightModeSchema,
57496
57794
  /** IR-cut trigger sensitivity, NORMALIZED 0–100 (higher = switches to night sooner). */
57497
- sensitivity: number().optional(),
57795
+ sensitivity: number().optional().meta(readingMeta("none")),
57498
57796
  /** Delay before the IR-cut filter flips, in seconds. */
57499
- switchDelaySec: number().optional(),
57797
+ switchDelaySec: number().optional().meta(readingMeta("duration")),
57500
57798
  lastFetchedAt: number()
57501
57799
  });
57502
57800
  /**
@@ -57651,11 +57949,11 @@ object({
57651
57949
  /** Feature keys of the programs the appliance currently offers — drives the UI selector. */
57652
57950
  availablePrograms: array(string()),
57653
57951
  /** Seconds remaining on the active cycle, or null when not running / not known. */
57654
- remainingSeconds: number().int().nonnegative().nullable(),
57952
+ remainingSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57655
57953
  /** Seconds the delayed start is set to (time until the cycle starts), or null. */
57656
- delayedStartSeconds: number().int().nonnegative().nullable(),
57954
+ delayedStartSeconds: number().int().nonnegative().nullable().meta(readingMeta("duration")),
57657
57955
  /** Cycle completion 0..100, or null when not applicable. */
57658
- progressPercent: number().min(0).max(100).nullable(),
57956
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
57659
57957
  /** True when the door is reported open; null when the door state is not known. */
57660
57958
  doorOpen: boolean().nullable(),
57661
57959
  /** True when the door is latched (locked) during a cycle; null when not reported. */
@@ -57679,7 +57977,7 @@ object({
57679
57977
  /** True while the machine-care (descaling / cleaning-program) reminder is raised; false when clear; null when not reported. */
57680
57978
  machineCareDue: boolean().nullable(),
57681
57979
  /** Program runs left before the machine-care reminder is due; null when the appliance does not say. */
57682
- machineCareRemainingRuns: number().int().nonnegative().nullable(),
57980
+ machineCareRemainingRuns: number().int().nonnegative().nullable().meta(readingMeta("count")),
57683
57981
  /** Option states: null when the appliance does not expose the option or has not said. */
57684
57982
  halfLoad: boolean().nullable(),
57685
57983
  hygienePlus: boolean().nullable(),
@@ -57694,7 +57992,7 @@ object({
57694
57992
  /** Feature keys of the events currently raised (salt low, rinse aid low, program finished, …). */
57695
57993
  activeEvents: array(string()),
57696
57994
  /** Numeric error code when the appliance reports one; null otherwise. */
57697
- errorCode: number().nullable(),
57995
+ errorCode: number().nullable().meta(readingMeta("none")),
57698
57996
  /** Name of the raised alert-level event while the appliance is in error; null otherwise. */
57699
57997
  errorLabel: string().nullable(),
57700
57998
  /** Ms epoch of the last value received from the appliance; 0 = never. */
@@ -57840,24 +58138,14 @@ object({
57840
58138
  /** Ms epoch of the last press. null = never observed since this provider started. */
57841
58139
  lastPressedAt: number().nullable(),
57842
58140
  /** Counter since provider start. Resets on reboot. Useful for metrics/debug. */
57843
- pressCountSinceStart: number()
58141
+ pressCountSinceStart: number().meta(readingMeta("count"))
57844
58142
  });
57845
58143
  object({
57846
58144
  deviceId: number(),
57847
58145
  timestamp: number()
57848
58146
  });
57849
58147
  DeviceType.Button, DeviceType.Camera;
57850
- /**
57851
- * Enum-state sensor — a string value picked from a finite option set.
57852
- * Drives HA `sensor` entries with `state_class: enum` (HVAC action
57853
- * states, weather conditions, contact-source channels, mode strings).
57854
- * The option set is stable across observations; it is persisted in the
57855
- * device config blob (via the control slice at adoption) and is NOT
57856
- * stored in `sourceInfo`.
57857
- */
57858
- /** Locale-render shape for an ISO `value` carried by a `DateTimeSensor`-role
57859
- * enum sensor: `date` (date-only), `time` (time-only), `datetime` (both). */
57860
- var EnumSensorDateTimeFormatSchema = _enum([
58148
+ _enum([
57861
58149
  "date",
57862
58150
  "time",
57863
58151
  "datetime"
@@ -57865,12 +58153,11 @@ var EnumSensorDateTimeFormatSchema = _enum([
57865
58153
  object({
57866
58154
  value: string(),
57867
58155
  /**
57868
- * Set for `DateTimeSensor`-role sensors so the UI renders the ISO `value`
57869
- * as a locale date/time/datetime; absent for plain enum sensors. HA
57870
- * `device_class=timestamp` → `'datetime'`, `device_class=date` → `'date'`,
57871
- * a time-only sensor → `'time'`.
58156
+ * Set when `value` is a timestamp rather than an enum state, so history
58157
+ * does not learn ISO strings as states. The display shape (date / time /
58158
+ * datetime) lives on `SourceInfo.dateTimeShape`, written once at adopt.
57872
58159
  */
57873
- format: EnumSensorDateTimeFormatSchema.optional(),
58160
+ timestamp: boolean().optional(),
57874
58161
  /** Ms epoch when the slice was last updated. */
57875
58162
  lastFetchedAt: number()
57876
58163
  });
@@ -57889,13 +58176,13 @@ var EventFireSchema = object({
57889
58176
  deviceId: number(),
57890
58177
  eventType: string(),
57891
58178
  data: record(string(), unknown()).nullable(),
57892
- timestamp: number(),
57893
- seq: number()
58179
+ timestamp: number().meta(readingMeta("none")),
58180
+ seq: number().meta(readingMeta("count"))
57894
58181
  });
57895
58182
  object({
57896
58183
  eventTypes: array(string()),
57897
58184
  lastEvent: EventFireSchema.nullable(),
57898
- eventCountSinceStart: number()
58185
+ eventCountSinceStart: number().meta(readingMeta("count"))
57899
58186
  });
57900
58187
  DeviceType.EventEmitter;
57901
58188
  var IdentitySchema = object({
@@ -58284,10 +58571,10 @@ var FanDirectionSchema = _enum(["forward", "reverse"]);
58284
58571
  object({
58285
58572
  /** Active speed as 0..100 inclusive. Null when the device has no
58286
58573
  * speed surface (single-speed fan). */
58287
- percentage: number().min(0).max(100).nullable(),
58574
+ percentage: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58288
58575
  /** Speed granularity the device accepts (HA `percentage_step`, e.g. 25 for
58289
58576
  * a 4-speed fan). Absent when unknown — the UI then falls back to step 1. */
58290
- percentageStep: number().positive().optional(),
58577
+ percentageStep: number().positive().optional().meta(readingMeta("percentage")),
58291
58578
  /** Active preset mode (`auto` / `quiet` / `turbo` / vendor-specific)
58292
58579
  * — empty string when no preset is active or supported. */
58293
58580
  preset: string(),
@@ -58369,7 +58656,7 @@ var FeatureProbeStatusSchema = object({
58369
58656
  /** Camera/firmware model string. `null` when the firmware doesn't expose it. */
58370
58657
  model: string().nullable(),
58371
58658
  /** Channel count for NVR/Hub devices; `1` for standalone cameras; `null` pre-probe. */
58372
- channelCount: number().nullable(),
58659
+ channelCount: number().nullable().meta(readingMeta("count")),
58373
58660
  /**
58374
58661
  * Ms epoch of the last SUCCESSFUL probe. `0` before the first probe
58375
58662
  * completes — drivers' `getAccessoryChildren()` should treat zero as
@@ -58401,9 +58688,27 @@ object({
58401
58688
  lastChangedAt: number()
58402
58689
  });
58403
58690
  DeviceType.Sensor;
58691
+ object({
58692
+ /** Members whose current value entered the aggregate. */
58693
+ included: number().int().nonnegative().meta(readingMeta("count", {
58694
+ precision: 0,
58695
+ label: "Reporting"
58696
+ })),
58697
+ /** Members left out: offline, unknown, or not a finite reading (D393). */
58698
+ excluded: number().int().nonnegative().meta(readingMeta("count", {
58699
+ precision: 0,
58700
+ label: "Excluded"
58701
+ })),
58702
+ /** `included` + `excluded`. */
58703
+ total: number().int().nonnegative().meta(readingMeta("count", {
58704
+ precision: 0,
58705
+ label: "Members"
58706
+ }))
58707
+ });
58708
+ DeviceType.Sensor;
58404
58709
  object({
58405
58710
  /** Radiator valve opening 0 (closed) .. 100 (open). Null = not yet known. */
58406
- position: number().min(0).max(100).nullable(),
58711
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58407
58712
  /** Ms epoch when the slice was last updated (push or poll). */
58408
58713
  lastFetchedAt: number()
58409
58714
  });
@@ -58412,9 +58717,9 @@ object({
58412
58717
  /** Whether the humidifier is currently on. */
58413
58718
  on: boolean(),
58414
58719
  /** Current measured relative humidity (0..100). Null when not reported. */
58415
- currentHumidity: number().min(0).max(100).nullable(),
58720
+ currentHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58416
58721
  /** Target relative humidity (0..100). Null when no setpoint surface. */
58417
- targetHumidity: number().min(0).max(100).nullable(),
58722
+ targetHumidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
58418
58723
  /** Active mode (`auto` / `normal` / `baby` / vendor). Null when the
58419
58724
  * device has no mode surface. */
58420
58725
  mode: string().nullable(),
@@ -58424,9 +58729,9 @@ object({
58424
58729
  * `idle` / `off`). Null when the device doesn't report it. */
58425
58730
  action: string().nullable(),
58426
58731
  /** HA `min_humidity` attribute. Null → UI uses 0. */
58427
- minHumidity: number().nullable(),
58732
+ minHumidity: number().nullable().meta(readingMeta("humidity")),
58428
58733
  /** HA `max_humidity` attribute. Null → UI uses 100. */
58429
- maxHumidity: number().nullable(),
58734
+ maxHumidity: number().nullable().meta(readingMeta("humidity")),
58430
58735
  /** Ms epoch when the slice was last updated. */
58431
58736
  lastChangedAt: number()
58432
58737
  });
@@ -58450,22 +58755,13 @@ DeviceType.Humidifier, method(object({
58450
58755
  auth: "protected"
58451
58756
  });
58452
58757
  object({
58453
- /** Current relative humidity, 0..100. */
58454
- percent: number().min(0).max(100).meta({
58455
- unit: "%",
58758
+ /** Current relative humidity, 0..100. `null` when the sample was refused (D393). */
58759
+ percent: number().min(0).max(100).nullable().meta(readingMeta("humidity", {
58760
+ precision: 0,
58456
58761
  label: "Humidity"
58457
- }),
58762
+ })),
58458
58763
  /** Ms epoch when the slice was last updated. */
58459
- lastFetchedAt: number(),
58460
- /** Live display unit from the upstream source (e.g. HA
58461
- * `attributes.unit_of_measurement`). The UI prefers this over the
58462
- * role's canonical unit. Absent → fall back to the canonical unit. */
58463
- unit: string().optional(),
58464
- /** Suggested decimal places for numeric display.
58465
- * Populated live from the upstream source when provided (e.g. HA
58466
- * `attributes.suggested_display_precision`). Falls back to
58467
- * auto-formatting when absent. */
58468
- precision: number().int().min(0).max(10).optional()
58764
+ lastFetchedAt: number()
58469
58765
  });
58470
58766
  DeviceType.Sensor, DeviceType.Thermostat;
58471
58767
  object({
@@ -58473,7 +58769,7 @@ object({
58473
58769
  * entity exposes no `entity_picture` (yet). */
58474
58770
  url: string().nullable(),
58475
58771
  /** Ms epoch of the upstream last-updated timestamp. Null at cold-start. */
58476
- lastUpdated: number().nullable()
58772
+ lastUpdated: number().nullable().meta(readingMeta("none"))
58477
58773
  });
58478
58774
  DeviceType.Image;
58479
58775
  /**
@@ -58529,19 +58825,19 @@ var NormalizedRangeSchema = object({
58529
58825
  });
58530
58826
  object({
58531
58827
  /** Normalized 0–100. */
58532
- brightness: number().optional(),
58828
+ brightness: number().optional().meta(readingMeta("none")),
58533
58829
  /** Normalized 0–100. */
58534
- contrast: number().optional(),
58830
+ contrast: number().optional().meta(readingMeta("none")),
58535
58831
  /** Normalized 0–100. */
58536
- saturation: number().optional(),
58832
+ saturation: number().optional().meta(readingMeta("none")),
58537
58833
  /** Normalized 0–100. */
58538
- sharpness: number().optional(),
58834
+ sharpness: number().optional().meta(readingMeta("none")),
58539
58835
  mirror: boolean().optional(),
58540
58836
  flip: boolean().optional(),
58541
58837
  rotate: ImageRotateSchema.optional(),
58542
58838
  whiteBalance: WhiteBalanceModeSchema.optional(),
58543
58839
  /** Manual white-balance warmth, NORMALIZED 0–100. Meaningful only when `whiteBalance === 'manual'`. */
58544
- warmth: number().optional(),
58840
+ warmth: number().optional().meta(readingMeta("none")),
58545
58841
  exposureMode: ExposureModeSchema.optional(),
58546
58842
  backlightMode: BacklightModeSchema.optional(),
58547
58843
  lastFetchedAt: number()
@@ -58786,10 +59082,10 @@ var IntercomAbilitySchema = object({
58786
59082
  /** Codecs accepted by firmware, in preference order. Driver-authored strings. */
58787
59083
  codecs: array(string()),
58788
59084
  /** Sample rate (Hz) the firmware expects. Reolink typical: 16000. */
58789
- sampleRate: number(),
59085
+ sampleRate: number().meta(readingMeta("none")),
58790
59086
  duplex: _enum(["half", "full"]),
58791
59087
  /** Max in-flight audio backlog before drop (ms). Server uses this to bound latency. */
58792
- maxBacklogMs: number()
59088
+ maxBacklogMs: number().meta(readingMeta("duration-ms"))
58793
59089
  });
58794
59090
  var IntercomStatusSchema = object({
58795
59091
  /** True while a talk session is open. */
@@ -58900,13 +59196,13 @@ object({
58900
59196
  activity: LawnMowerActivitySchema,
58901
59197
  /** 0..100 battery percentage. Null when the device has no battery
58902
59198
  * reading. */
58903
- batteryLevel: number().min(0).max(100).nullable(),
59199
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58904
59200
  /** 0..100 mowing-completion percentage of the current task, or null when no
58905
59201
  * task is active / progress is unavailable. */
58906
- progressPercent: number().min(0).max(100).nullable(),
59202
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
58907
59203
  /** Current device/event code (dynamic — mostly status, sometimes an error),
58908
59204
  * or null when unknown. */
58909
- currentCode: number().nullable(),
59205
+ currentCode: number().nullable().meta(readingMeta("none")),
58910
59206
  /** Human label for {@link currentCode}, or null when undecodable. */
58911
59207
  currentCodeLabel: string().nullable(),
58912
59208
  /** Severity of {@link currentCode}. `error` (and often `warning`) warrants UI
@@ -59368,7 +59664,7 @@ object({
59368
59664
  state: MediaPlayerStateSchema,
59369
59665
  /** Volume as 0..100 inclusive. Null when the device has no volume
59370
59666
  * surface. Pair with `DeviceFeature.MediaPlayerVolume`. */
59371
- volumeLevel: number().min(0).max(100).nullable(),
59667
+ volumeLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
59372
59668
  /** Mute toggle distinct from volume=0. Null when no mute surface.
59373
59669
  * Pair with `DeviceFeature.MediaPlayerMute`. */
59374
59670
  isMuted: boolean().nullable(),
@@ -59381,10 +59677,10 @@ object({
59381
59677
  currentMedia: MediaInfoSchema.nullable(),
59382
59678
  /** Current playback position in ms. Null when not seekable or
59383
59679
  * nothing is playing. Pair with `DeviceFeature.MediaPlayerSeek`. */
59384
- positionMs: number().int().nonnegative().nullable(),
59680
+ positionMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59385
59681
  /** Total duration of the current media in ms. Null when unknown
59386
59682
  * (live stream) or nothing is playing. */
59387
- durationMs: number().int().nonnegative().nullable(),
59683
+ durationMs: number().int().nonnegative().nullable().meta(readingMeta("duration-ms")),
59388
59684
  /** Shuffle toggle. Null when no shuffle surface.
59389
59685
  * Pair with `DeviceFeature.MediaPlayerShuffle`. */
59390
59686
  shuffle: boolean().nullable(),
@@ -59684,7 +59980,7 @@ object({
59684
59980
  * it, every provider seeded it at boot, and every session left it
59685
59981
  * `detected: true` after its detach.
59686
59982
  */
59687
- autoClearAfterMs: number().nullable()
59983
+ autoClearAfterMs: number().nullable().meta(readingMeta("duration-ms"))
59688
59984
  });
59689
59985
  /**
59690
59986
  * Payload of `motion.onMotionChanged` event + the corresponding bus
@@ -59734,7 +60030,7 @@ var MotionZoneRegionSchema = object({
59734
60030
  });
59735
60031
  object({
59736
60032
  enabled: boolean(),
59737
- sensitivity: number(),
60033
+ sensitivity: number().meta(readingMeta("none")),
59738
60034
  /** Grid region(s). Today exactly one `grid` shape. */
59739
60035
  regions: array(MotionZoneRegionSchema),
59740
60036
  lastFetchedAt: number()
@@ -60038,7 +60334,7 @@ var NavigationStatusSchema = object({
60038
60334
  following: boolean(),
60039
60335
  flash: boolean(),
60040
60336
  lightMode: NavigationLightModeSchema,
60041
- lightLevel: number().min(40).max(100),
60337
+ lightLevel: number().min(40).max(100).meta(readingMeta("none")),
60042
60338
  /** Ms epoch when the slice was last updated. */
60043
60339
  lastChangedAt: number()
60044
60340
  });
@@ -60084,13 +60380,13 @@ var NetworkLinkStatusSchema = object({
60084
60380
  * one whose reading has not landed must not be drawn at 0 %. Consumers
60085
60381
  * SKIP a null rather than coerce it.
60086
60382
  */
60087
- signalPercent: number().min(0).max(100).nullable(),
60383
+ signalPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
60088
60384
  /** Raw received signal strength in dBm, when the firmware reports one. */
60089
- rssiDbm: number().optional(),
60385
+ rssiDbm: number().optional().meta(readingMeta("signal-level")),
60090
60386
  /** Network name of a wireless link, when the firmware reports it. */
60091
60387
  ssid: string().optional(),
60092
60388
  /** Ms epoch of the last observation. Lets consumers reason about freshness. */
60093
- lastUpdated: number()
60389
+ lastUpdated: number().meta(readingMeta("none"))
60094
60390
  });
60095
60391
  DeviceType.Camera, DeviceType.Sensor, DeviceType.Button, DeviceType.Switch, DeviceType.Light, DeviceType.Lock, DeviceType.Siren, object({
60096
60392
  deviceId: number(),
@@ -60409,7 +60705,7 @@ object({
60409
60705
  lastError: string().nullable(),
60410
60706
  /** Number of deliveries currently buffered server-side (rate-limited
60411
60707
  * notifiers may queue). 0 when the provider sends synchronously. */
60412
- queueDepth: number().int().nonnegative(),
60708
+ queueDepth: number().int().nonnegative().meta(readingMeta("count")),
60413
60709
  /** Per-feature capability matrix — authoritative source for the
60414
60710
  * compose-form field gating. */
60415
60711
  supports: NotifierSupportsSchema
@@ -60459,15 +60755,8 @@ DeviceType.Notifier, method(NotifierSendInputSchema, NotifierSendResultSchema, {
60459
60755
  acceptedAt: number()
60460
60756
  });
60461
60757
  object({
60462
- value: number(),
60463
- /** Display unit-of-measurement (e.g. 'dBm', 's', 'rpm', 'steps').
60464
- * Populated live from the upstream source on each state push.
60465
- * Absent for unitless sensors. */
60466
- unit: string().optional(),
60467
- /** Suggested decimal places for numeric display.
60468
- * Populated live from the upstream source when provided.
60469
- * Falls back to auto-formatting when absent. */
60470
- precision: number().int().min(0).max(10).optional(),
60758
+ /** Reading in the device quantity's base unit. `null` when refused (D393). */
60759
+ value: number().nullable().meta(readingMeta("device")),
60471
60760
  /** Ms epoch when the slice was last updated. */
60472
60761
  lastFetchedAt: number()
60473
60762
  });
@@ -60757,23 +61046,26 @@ object({
60757
61046
  /** Food currently in the bowl (grams). Null when the device has not
60758
61047
  * reported a reading yet. On dual-hopper models this is the combined
60759
61048
  * bowl reading; per-hopper levels live in `food1`/`food2`. */
60760
- foodLevel: number().nullable(),
61049
+ foodLevel: number().nullable().meta(readingMeta("none")),
60761
61050
  /** Hopper-1 food level (grams) on dual-hopper feeders; null on
60762
61051
  * single-hopper models. */
60763
- food1: number().nullable(),
61052
+ food1: number().nullable().meta(readingMeta("none")),
60764
61053
  /** Hopper-2 food level (grams) on dual-hopper feeders; null on
60765
61054
  * single-hopper models. */
60766
- food2: number().nullable(),
61055
+ food2: number().nullable().meta(readingMeta("none")),
60767
61056
  /** Derived low-food flag — mirrors the HA `food_level` binary_sensor
60768
61057
  * (`device_class: problem`, on = low). True when the bowl is empty /
60769
61058
  * below the feeder's low threshold. */
60770
61059
  lowFood: boolean(),
60771
61060
  /** Battery charge 0..100 (%). Null on mains-powered models or when the
60772
61061
  * device has no battery reading. */
60773
- batteryPower: number().min(0).max(100).nullable(),
60774
- /** Days of desiccant life remaining. Null when the model has no
60775
- * desiccant sensor. */
60776
- desiccantLeftDays: number().nullable(),
61062
+ batteryPower: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61063
+ /**
61064
+ * Desiccant life remaining, stored in seconds (duration base, D944).
61065
+ * Providers convert from the days the feeder reports. Null when the
61066
+ * model has no desiccant sensor.
61067
+ */
61068
+ desiccantLeftDays: number().nullable().meta(readingMeta("duration")),
60777
61069
  /** True while a feed is in progress. */
60778
61070
  feeding: boolean(),
60779
61071
  /** Decoded connectivity / power status (HA petkit device-status enum).
@@ -60784,7 +61076,7 @@ object({
60784
61076
  * with `errorCode` for consumers that want the raw integer. */
60785
61077
  error: string().nullable(),
60786
61078
  /** Raw device error code (0 / null = no error). */
60787
- errorCode: number().nullable(),
61079
+ errorCode: number().nullable().meta(readingMeta("none")),
60788
61080
  /** True for D4S/D4SH dual-hopper hardware — gates the per-hopper UI. */
60789
61081
  isDualHopper: boolean(),
60790
61082
  /** Child-lock (manual-lock) setting — buttons on the unit are disabled. */
@@ -60794,7 +61086,7 @@ object({
60794
61086
  /** Play a chime when dispensing. */
60795
61087
  feedSound: boolean(),
60796
61088
  /** Speaker / prompt volume level (device-scaled integer). */
60797
- volume: number(),
61089
+ volume: number().meta(readingMeta("none")),
60798
61090
  /** Ms epoch when the slice was last refreshed from the cloud. */
60799
61091
  lastFetchedAt: number()
60800
61092
  });
@@ -61231,36 +61523,19 @@ method(_void(), PlatformCapabilitiesSchema), method(_void(), HardwareInfoSchema)
61231
61523
  });
61232
61524
  object({
61233
61525
  /** Instantaneous power draw in watts. */
61234
- watts: number().optional().meta({
61235
- unit: "W",
61236
- label: "Power"
61237
- }),
61526
+ watts: number().optional().meta(readingMeta("power", { label: "Power" })),
61238
61527
  /** Cumulative energy in kilowatt-hours since the meter was reset. */
61239
- kwhTotal: number().optional().meta({
61240
- unit: "kWh",
61241
- label: "Energy"
61242
- }),
61528
+ kwhTotal: number().optional().meta(readingMeta("energy", { label: "Energy" })),
61243
61529
  /** Voltage in volts. */
61244
- volts: number().optional(),
61530
+ volts: number().optional().meta(readingMeta("voltage")),
61245
61531
  /** Current in amperes. */
61246
- amps: number().optional(),
61247
- /** Apparent power in volt-amperes (W × power-factor reciprocal). */
61248
- va: number().optional(),
61532
+ amps: number().optional().meta(readingMeta("current")),
61533
+ /** Apparent power in volt-amperes. */
61534
+ va: number().optional().meta(readingMeta("apparent-power")),
61249
61535
  /** Power factor (0..1) if reported. */
61250
- powerFactor: number().min(0).max(1).optional(),
61536
+ powerFactor: number().min(0).max(1).optional().meta(readingMeta("ratio")),
61251
61537
  /** Ms epoch when the slice was last updated. */
61252
- lastFetchedAt: number(),
61253
- /** Live display unit of the single metric this slice carries (e.g. HA
61254
- * `attributes.unit_of_measurement` → 'V' / 'A' / 'W' / 'kW' / 'kWh').
61255
- * Each upstream `sensor.*` entity surfaces ONE device_class, so one
61256
- * unit per slice is unambiguous. The UI prefers this over the role's
61257
- * canonical unit so a 'kW' / 'Wh' feed renders verbatim. */
61258
- unit: string().optional(),
61259
- /** Suggested decimal places for numeric display.
61260
- * Populated live from the upstream source when provided (e.g. HA
61261
- * `attributes.suggested_display_precision`). Falls back to
61262
- * auto-formatting when absent. */
61263
- precision: number().int().min(0).max(10).optional()
61538
+ lastFetchedAt: number()
61264
61539
  });
61265
61540
  DeviceType.Sensor;
61266
61541
  /**
@@ -61281,11 +61556,11 @@ DeviceType.Sensor;
61281
61556
  */
61282
61557
  var GpsLocationSchema = object({
61283
61558
  /** Latitude in decimal degrees, -90..90. */
61284
- latitude: number().min(-90).max(90),
61559
+ latitude: number().min(-90).max(90).meta(readingMeta("angle")),
61285
61560
  /** Longitude in decimal degrees, -180..180. */
61286
- longitude: number().min(-180).max(180),
61561
+ longitude: number().min(-180).max(180).meta(readingMeta("angle")),
61287
61562
  /** Reported accuracy in meters (lower = better). */
61288
- accuracyMeters: number().nonnegative()
61563
+ accuracyMeters: number().nonnegative().meta(readingMeta("length"))
61289
61564
  });
61290
61565
  object({
61291
61566
  /** `home` / `not_home` / any user-defined zone name. */
@@ -61297,25 +61572,16 @@ object({
61297
61572
  gps: GpsLocationSchema.nullable(),
61298
61573
  /** Optional battery level of the tracking device (0..100). Null
61299
61574
  * when not reported. */
61300
- batteryPercent: number().min(0).max(100).nullable(),
61575
+ batteryPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
61301
61576
  /** Ms epoch when the slice was last updated. */
61302
61577
  lastChangedAt: number()
61303
61578
  });
61304
61579
  DeviceType.Presence;
61305
61580
  object({
61306
- /** Current pressure in hPa. */
61307
- hpa: number(),
61581
+ /** Current pressure in hPa. `null` when the sample was refused (D393). */
61582
+ hpa: number().nullable().meta(readingMeta("pressure", { precision: 0 })),
61308
61583
  /** Ms epoch when the slice was last updated. */
61309
- lastFetchedAt: number(),
61310
- /** Live display unit from the upstream source (e.g. HA
61311
- * `attributes.unit_of_measurement`). The UI prefers this over the
61312
- * role's canonical unit. Absent → fall back to the canonical unit. */
61313
- unit: string().optional(),
61314
- /** Suggested decimal places for numeric display.
61315
- * Populated live from the upstream source when provided (e.g. HA
61316
- * `attributes.suggested_display_precision`). Falls back to
61317
- * auto-formatting when absent. */
61318
- precision: number().int().min(0).max(10).optional()
61584
+ lastFetchedAt: number()
61319
61585
  });
61320
61586
  DeviceType.Sensor;
61321
61587
  /**
@@ -61648,8 +61914,8 @@ DeviceType.Camera, method(PtzMoveCommandSchema.extend({ deviceId: number() }), _
61648
61914
  }), _void(), { kind: "mutation" });
61649
61915
  var PtzAutotrackSettingsSchema = object({
61650
61916
  targetType: string().describe("Vendor target string (people/vehicle/pet); empty = camera default"),
61651
- stopDelaySeconds: number().int().min(0).max(300),
61652
- disappearDelaySeconds: number().int().min(0).max(300)
61917
+ stopDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration")),
61918
+ disappearDelaySeconds: number().int().min(0).max(300).meta(readingMeta("duration"))
61653
61919
  });
61654
61920
  /**
61655
61921
  * One option the camera advertises as a valid `targetType`. Drivers
@@ -61706,7 +61972,7 @@ object({
61706
61972
  /** null = unknown — never coerced to false (D393). */
61707
61973
  running: boolean().nullable(),
61708
61974
  /** Epoch ms the current boolean began; null while `running` is null. */
61709
- since: number().nullable(),
61975
+ since: number().nullable().meta(readingMeta("none")),
61710
61976
  source: RunningStateSourceSchema.meta({ label: "Source" }),
61711
61977
  phase: string().nullable(),
61712
61978
  /** null until a reading has been published (D393). */
@@ -62904,9 +63170,9 @@ var RecordTriggerSchema = _enum([
62904
63170
  */
62905
63171
  var RecordWindowSchema = object({
62906
63172
  trigger: RecordTriggerSchema,
62907
- day: number().int().min(0).max(6),
62908
- startMinute: number().int().min(0).max(1439),
62909
- endMinute: number().int().min(1).max(1440)
63173
+ day: number().int().min(0).max(6).meta(readingMeta("none")),
63174
+ startMinute: number().int().min(0).max(1439).meta(readingMeta("none")),
63175
+ endMinute: number().int().min(1).max(1440).meta(readingMeta("none"))
62910
63176
  });
62911
63177
  /** Status of one physical volume, as the camera itself describes it. */
62912
63178
  var OnboardStorageVolumeSchema = object({
@@ -62975,10 +63241,10 @@ object({
62975
63241
  /** Master "record to the card at all" switch. */
62976
63242
  enabled: boolean().nullable(),
62977
63243
  overwriteWhenFull: boolean().nullable(),
62978
- preRecordSec: number().nullable(),
62979
- postRecordSec: number().nullable(),
63244
+ preRecordSec: number().nullable().meta(readingMeta("duration")),
63245
+ postRecordSec: number().nullable().meta(readingMeta("duration")),
62980
63246
  /** Length of one recorded file, in minutes. */
62981
- segmentMinutes: number().nullable(),
63247
+ segmentMinutes: number().nullable().meta(readingMeta("none")),
62982
63248
  /** The primary track's weekly windows, flattened. */
62983
63249
  windows: array(RecordWindowSchema),
62984
63250
  /**
@@ -62994,7 +63260,7 @@ object({
62994
63260
  * provider reporting a non-zero count also reports the schedule as not
62995
63261
  * writable.
62996
63262
  */
62997
- unreadableWindows: number(),
63263
+ unreadableWindows: number().meta(readingMeta("count")),
62998
63264
  /**
62999
63265
  * The camera is scheduled to record and has NO usable storage.
63000
63266
  *
@@ -63631,7 +63897,7 @@ var CamStreamDescriptorSchema = object({
63631
63897
  url: string().optional(),
63632
63898
  codec: string().optional(),
63633
63899
  resolution: CamStreamResolutionSchema.optional(),
63634
- fps: number().positive().optional(),
63900
+ fps: number().positive().optional().meta(readingMeta("rate")),
63635
63901
  label: string().optional(),
63636
63902
  /** Device-level features (e.g. `battery-operated`) — drives broker policy. */
63637
63903
  deviceFeatures: array(string()).optional(),
@@ -63661,18 +63927,18 @@ var StreamProfileSchema = _enum([
63661
63927
  ]);
63662
63928
  /** The current encoding config of one stream profile. */
63663
63929
  var StreamProfileConfigSchema = object({
63664
- width: number(),
63665
- height: number(),
63930
+ width: number().meta(readingMeta("none")),
63931
+ height: number().meta(readingMeta("none")),
63666
63932
  codec: _enum(["h264", "h265"]),
63667
- framerate: number(),
63668
- bitrate: number(),
63933
+ framerate: number().meta(readingMeta("rate")),
63934
+ bitrate: number().meta(readingMeta("bitrate")),
63669
63935
  bitrateMode: _enum(["vbr", "cbr"]).optional(),
63670
63936
  encoderProfile: _enum([
63671
63937
  "high",
63672
63938
  "main",
63673
63939
  "baseline"
63674
63940
  ]).optional(),
63675
- gop: number().optional(),
63941
+ gop: number().optional().meta(readingMeta("none")),
63676
63942
  /**
63677
63943
  * Whether THIS profile currently carries an audio track. READ-ONLY here.
63678
63944
  *
@@ -64315,23 +64581,13 @@ object({
64315
64581
  });
64316
64582
  DeviceType.Sensor;
64317
64583
  object({
64318
- /** Current temperature in Celsius. */
64319
- celsius: number().meta({
64320
- unit: "°C",
64584
+ /** Current temperature in Celsius. `null` when the sample was refused (D393). */
64585
+ celsius: number().nullable().meta(readingMeta("temperature", {
64586
+ precision: 1,
64321
64587
  label: "Temperature"
64322
- }),
64588
+ })),
64323
64589
  /** Ms epoch when the slice was last updated (push or poll). */
64324
- lastFetchedAt: number(),
64325
- /** Live display unit from the upstream source (e.g. HA
64326
- * `attributes.unit_of_measurement`). The UI prefers this over the
64327
- * role's canonical unit, so a Fahrenheit feed renders '°F' not '°C'.
64328
- * Absent → the UI falls back to the role's canonical unit. */
64329
- unit: string().optional(),
64330
- /** Suggested decimal places for numeric display.
64331
- * Populated live from the upstream source when provided (e.g. HA
64332
- * `attributes.suggested_display_precision`). Falls back to
64333
- * auto-formatting when absent. */
64334
- precision: number().int().min(0).max(10).optional()
64590
+ lastFetchedAt: number()
64335
64591
  });
64336
64592
  DeviceType.Sensor, DeviceType.Thermostat;
64337
64593
  /**
@@ -64583,16 +64839,16 @@ object({
64583
64839
  ]),
64584
64840
  /** 0..100 battery percentage. Null when the device has no battery
64585
64841
  * reading. */
64586
- batteryLevel: number().min(0).max(100).nullable(),
64842
+ batteryLevel: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64587
64843
  /** Current fan-speed token (provider-verbatim). Null when unknown or
64588
64844
  * the vacuum has no speed control. */
64589
64845
  fanSpeed: string().nullable(),
64590
64846
  /** Speed tokens the hardware accepts — drives the UI selector. */
64591
64847
  availableFanSpeeds: array(string()),
64592
64848
  /** 0..100 cleaning-completion percentage of the current task, or null. */
64593
- progressPercent: number().min(0).max(100).nullable(),
64849
+ progressPercent: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64594
64850
  /** Current error code (0 / null = no error). */
64595
- errorCode: number().nullable(),
64851
+ errorCode: number().nullable().meta(readingMeta("none")),
64596
64852
  /** Human label for {@link errorCode}, or null when none / undecodable. */
64597
64853
  errorLabel: string().nullable(),
64598
64854
  /** Ms epoch when the slice was last updated. */
@@ -64631,7 +64887,7 @@ object({
64631
64887
  ]),
64632
64888
  /** 0 = fully closed, 100 = fully open. Null when the device has no
64633
64889
  * intermediate position surface. */
64634
- position: number().min(0).max(100).nullable(),
64890
+ position: number().min(0).max(100).nullable().meta(readingMeta("percentage")),
64635
64891
  /** Ms epoch when the slice was last updated. */
64636
64892
  lastChangedAt: number()
64637
64893
  });
@@ -64659,9 +64915,9 @@ object({
64659
64915
  DeviceType.Sensor;
64660
64916
  object({
64661
64917
  /** Current measured temperature. Null when not reported. */
64662
- currentTemp: number().nullable(),
64918
+ currentTemp: number().nullable().meta(readingMeta("temperature")),
64663
64919
  /** Target temperature setpoint. Null when no setpoint surface. */
64664
- targetTemp: number().nullable(),
64920
+ targetTemp: number().nullable().meta(readingMeta("temperature")),
64665
64921
  /** Active operation mode = HA `state` (`eco` / `electric` / `gas` /
64666
64922
  * `heat_pump` / `high_demand` / `performance` / `off`). Null when the
64667
64923
  * device reports an unknown state. */
@@ -64672,9 +64928,9 @@ object({
64672
64928
  * has no away surface. */
64673
64929
  away: boolean().nullable(),
64674
64930
  /** HA `min_temp` attribute. Null when not reported. */
64675
- minTemp: number().nullable(),
64931
+ minTemp: number().nullable().meta(readingMeta("temperature")),
64676
64932
  /** HA `max_temp` attribute. Null when not reported. */
64677
- maxTemp: number().nullable(),
64933
+ maxTemp: number().nullable().meta(readingMeta("temperature")),
64678
64934
  /** Ms epoch when the slice was last updated. */
64679
64935
  lastChangedAt: number()
64680
64936
  });
@@ -64698,25 +64954,18 @@ DeviceType.WaterHeater, method(object({
64698
64954
  auth: "protected"
64699
64955
  });
64700
64956
  object({
64701
- /** Verbatim HA condition state (`sunny`, `cloudy`, `rainy`, …). Null
64702
- * when no condition has been reported yet. */
64957
+ /** Verbatim condition state (`sunny`, `cloudy`, `rainy`, …). Null when none yet. */
64703
64958
  condition: string().nullable(),
64704
- /** Current temperature in the reported unit. Null when not provided. */
64705
- temperature: number().nullable(),
64706
- /** Temperature unit string (e.g. `°C` / `°F`). Null when not provided. */
64707
- temperatureUnit: string().nullable(),
64959
+ /** Current temperature in Celsius. Null when not provided or refused. */
64960
+ temperature: number().nullable().meta(readingMeta("temperature", { precision: 1 })),
64708
64961
  /** Relative humidity (0..100). Null when not provided. */
64709
- humidity: number().min(0).max(100).nullable(),
64710
- /** Barometric pressure in the reported unit. Null when not provided. */
64711
- pressure: number().nullable(),
64712
- /** Pressure unit string (e.g. `hPa` / `inHg`). Null when not provided. */
64713
- pressureUnit: string().nullable(),
64714
- /** Wind speed in the reported unit. Null when not provided. */
64715
- windSpeed: number().nullable(),
64716
- /** Wind-speed unit string (e.g. `km/h` / `mph`). Null when not provided. */
64717
- windSpeedUnit: string().nullable(),
64962
+ humidity: number().min(0).max(100).nullable().meta(readingMeta("humidity")),
64963
+ /** Barometric pressure in hPa. Null when not provided or refused. */
64964
+ pressure: number().nullable().meta(readingMeta("pressure")),
64965
+ /** Wind speed in metres per second. Null when not provided or refused. */
64966
+ windSpeed: number().nullable().meta(readingMeta("speed")),
64718
64967
  /** Wind bearing in degrees (0..360, meteorological). Null when not provided. */
64719
- windBearing: number().nullable(),
64968
+ windBearing: number().nullable().meta(readingMeta("angle")),
64720
64969
  /** Ms epoch when the slice was last updated. */
64721
64970
  lastFetchedAt: number()
64722
64971
  });
@@ -68834,6 +69083,13 @@ Object.freeze({
68834
69083
  access: "view",
68835
69084
  role: "viewer"
68836
69085
  },
69086
+ "groupStatus.getStatus": {
69087
+ capName: "group-status",
69088
+ capScope: "device",
69089
+ addonId: null,
69090
+ access: "view",
69091
+ role: "viewer"
69092
+ },
68837
69093
  "heatingValve.getStatus": {
68838
69094
  capName: "heating-valve",
68839
69095
  capScope: "device",
@@ -78656,6 +78912,33 @@ var SENSOR_CAP_VOCABULARY = {
78656
78912
  icon: "flame"
78657
78913
  }]
78658
78914
  },
78915
+ "group-status": {
78916
+ scope: "device",
78917
+ noun: t("Group", "Gruppo"),
78918
+ state: [
78919
+ {
78920
+ kind: "numeric",
78921
+ field: "included",
78922
+ quantity: t("the members reporting", "i membri che rispondono"),
78923
+ label: t("Reporting", "Rispondono"),
78924
+ icon: "users"
78925
+ },
78926
+ {
78927
+ kind: "numeric",
78928
+ field: "excluded",
78929
+ quantity: t("the members left out", "i membri esclusi"),
78930
+ label: t("Excluded", "Esclusi"),
78931
+ icon: "users"
78932
+ },
78933
+ {
78934
+ kind: "numeric",
78935
+ field: "total",
78936
+ quantity: t("the member count", "il numero di membri"),
78937
+ label: t("Members", "Membri"),
78938
+ icon: "sigma"
78939
+ }
78940
+ ]
78941
+ },
78659
78942
  "humidity-sensor": {
78660
78943
  scope: "device",
78661
78944
  noun: t("Humidity", "Umidità"),
@@ -78689,7 +78972,6 @@ var SENSOR_CAP_VOCABULARY = {
78689
78972
  field: "value",
78690
78973
  quantity: t("the reading", "il valore"),
78691
78974
  label: t("Reading", "Valore"),
78692
- unitField: "unit",
78693
78975
  icon: "gauge"
78694
78976
  }]
78695
78977
  },
@@ -78825,7 +79107,7 @@ var SENSOR_CAP_VOCABULARY = {
78825
79107
  field: "temperature",
78826
79108
  quantity: t("the outdoor temperature", "la temperatura esterna"),
78827
79109
  label: t("Temperature", "Temperatura"),
78828
- unitField: "temperatureUnit",
79110
+ unit: "°C",
78829
79111
  icon: "thermometer"
78830
79112
  },
78831
79113
  {
@@ -78841,7 +79123,7 @@ var SENSOR_CAP_VOCABULARY = {
78841
79123
  field: "windSpeed",
78842
79124
  quantity: t("the wind speed", "la velocità del vento"),
78843
79125
  label: t("Wind", "Vento"),
78844
- unitField: "windSpeedUnit",
79126
+ unit: "m/s",
78845
79127
  icon: "wind"
78846
79128
  },
78847
79129
  {
@@ -78849,7 +79131,7 @@ var SENSOR_CAP_VOCABULARY = {
78849
79131
  field: "pressure",
78850
79132
  quantity: t("the air pressure", "la pressione atmosferica"),
78851
79133
  label: t("Pressure", "Pressione"),
78852
- unitField: "pressureUnit",
79134
+ unit: "hPa",
78853
79135
  icon: "gauge"
78854
79136
  }
78855
79137
  ]
@@ -80122,6 +80404,11 @@ Object.freeze({
80122
80404
  form: "single",
80123
80405
  optional: false
80124
80406
  }],
80407
+ "groupStatus.getStatus": [{
80408
+ name: "deviceId",
80409
+ form: "single",
80410
+ optional: false
80411
+ }],
80125
80412
  "heatingValve.getStatus": [{
80126
80413
  name: "deviceId",
80127
80414
  form: "single",